rockbox/apps/plugins/mikmod/load_med.c
Solomon Peachy b4e70422a3 mikmod: Upgrade mikmod core from v3.2.0 to v3.3.11
* Get rid of the non-functional GT2 loader
 * Add the UMX loader
 * Add HQ mixer routines (and make it configurable)
 * Allow samplerate to be configured at run/playtime
 * Support >64KHz mixing/playback
 * Correctly restore non-boost status

(The diff to upstream is much smaller now too!)

Change-Id: Iaa4ac901ba9cd4123bb225656976e78271353a72
2020-08-11 03:29:12 +00:00

759 lines
18 KiB
C

/* MikMod sound library
(c) 1998, 1999, 2000, 2001, 2002 Miodrag Vallat and others - see file
AUTHORS for complete list.
This library is free software; you can redistribute it and/or modify
it under the terms of the GNU Library General Public License as
published by the Free Software Foundation; either version 2 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
02111-1307, USA.
*/
/*==============================================================================
$Id$
Amiga MED module loader
==============================================================================*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <stdio.h>
#ifdef HAVE_MEMORY_H
#include <memory.h>
#endif
#include <string.h>
#include "mikmod_internals.h"
#ifdef SUNOS
extern int fprintf(FILE *, const char *, ...);
#endif
/*========== Module information */
typedef struct MEDHEADER {
ULONG id;
ULONG modlen;
ULONG MEDSONGP; /* struct MEDSONG *song; */
UWORD psecnum; /* for the player routine, MMD2 only */
UWORD pseq; /* " " " " */
ULONG MEDBlockPP; /* struct MEDBlock **blockarr; */
ULONG reserved1;
ULONG MEDINSTHEADERPP; /* struct MEDINSTHEADER **smplarr; */
ULONG reserved2;
ULONG MEDEXPP; /* struct MEDEXP *expdata; */
ULONG reserved3;
UWORD pstate; /* some data for the player routine */
UWORD pblock;
UWORD pline;
UWORD pseqnum;
SWORD actplayline;
UBYTE counter;
UBYTE extra_songs; /* number of songs - 1 */
} MEDHEADER;
typedef struct MEDSAMPLE {
UWORD rep, replen; /* offs: 0(s), 2(s) */
UBYTE midich; /* offs: 4(s) */
UBYTE midipreset; /* offs: 5(s) */
UBYTE svol; /* offs: 6(s) */
SBYTE strans; /* offs: 7(s) */
} MEDSAMPLE;
typedef struct MEDSONG {
MEDSAMPLE sample[63]; /* 63 * 8 bytes = 504 bytes */
UWORD numblocks; /* offs: 504 */
UWORD songlen; /* offs: 506 */
UBYTE playseq[256]; /* offs: 508 */
UWORD deftempo; /* offs: 764 */
SBYTE playtransp; /* offs: 766 */
UBYTE flags; /* offs: 767 */
UBYTE flags2; /* offs: 768 */
UBYTE tempo2; /* offs: 769 */
UBYTE trkvol[16]; /* offs: 770 */
UBYTE mastervol; /* offs: 786 */
UBYTE numsamples; /* offs: 787 */
} MEDSONG;
typedef struct MEDEXP {
ULONG nextmod; /* pointer to next module */
ULONG exp_smp; /* pointer to MEDINSTEXT array */
UWORD s_ext_entries;
UWORD s_ext_entrsz;
ULONG annotxt; /* pointer to annotation text */
ULONG annolen;
ULONG iinfo; /* pointer to MEDINSTINFO array */
UWORD i_ext_entries;
UWORD i_ext_entrsz;
ULONG jumpmask;
ULONG rgbtable;
ULONG channelsplit;
ULONG n_info;
ULONG songname; /* pointer to songname */
ULONG songnamelen;
ULONG dumps;
ULONG reserved2[7];
} MEDEXP;
typedef struct MMD0NOTE {
UBYTE a, b, c;
} MMD0NOTE;
typedef struct MMD1NOTE {
UBYTE a, b, c, d;
} MMD1NOTE;
typedef struct MEDINSTHEADER {
ULONG length;
SWORD type;
/* Followed by actual data */
} MEDINSTHEADER;
typedef struct MEDINSTEXT {
UBYTE hold;
UBYTE decay;
UBYTE suppress_midi_off;
SBYTE finetune;
} MEDINSTEXT;
typedef struct MEDINSTINFO {
UBYTE name[40];
} MEDINSTINFO;
/*========== Loader variables */
#define MMD0_string 0x4D4D4430
#define MMD1_string 0x4D4D4431
static MEDHEADER *mh = NULL;
static MEDSONG *ms = NULL;
static MEDEXP *me = NULL;
static ULONG *ba = NULL;
static MMD0NOTE *mmd0pat = NULL;
static MMD1NOTE *mmd1pat = NULL;
static int decimalvolumes;
static int bpmtempos;
#define d0note(row,col) mmd0pat[((row)*(UWORD)of.numchn)+(col)]
#define d1note(row,col) mmd1pat[((row)*(UWORD)of.numchn)+(col)]
static CHAR MED_Version[] = "OctaMED (MMDx)";
/*========== Loader code */
static int MED_Test(void)
{
UBYTE id[4];
if (!_mm_read_UBYTES(id, 4, modreader))
return 0;
if ((!memcmp(id, "MMD0", 4)) || (!memcmp(id, "MMD1", 4)))
return 1;
return 0;
}
static int MED_Init(void)
{
if (!(me = (MEDEXP *)MikMod_malloc(sizeof(MEDEXP))))
return 0;
if (!(mh = (MEDHEADER *)MikMod_malloc(sizeof(MEDHEADER))))
return 0;
if (!(ms = (MEDSONG *)MikMod_malloc(sizeof(MEDSONG))))
return 0;
return 1;
}
static void MED_Cleanup(void)
{
MikMod_free(me);
MikMod_free(mh);
MikMod_free(ms);
MikMod_free(ba);
MikMod_free(mmd0pat);
MikMod_free(mmd1pat);
me = NULL;
mh = NULL;
ms = NULL;
ba = NULL;
mmd0pat = NULL;
mmd1pat = NULL;
}
static void EffectCvt(UBYTE eff, UBYTE dat)
{
switch (eff) {
/* 0x0 0x1 0x2 0x3 0x4 PT effects */
case 0x5: /* PT vibrato with speed/depth nibbles swapped */
UniPTEffect(0x4, (dat >> 4) | ((dat & 0xf) << 4));
break;
/* 0x6 0x7 not used */
case 0x6:
case 0x7:
break;
case 0x8: /* midi hold/decay */
break;
case 0x9:
if (bpmtempos) {
if (!dat)
dat = of.initspeed;
UniEffect(UNI_S3MEFFECTA, dat);
} else {
if (dat <= 0x20) {
if (!dat)
dat = of.initspeed;
else
dat /= 4;
UniPTEffect(0xf, dat);
} else
UniEffect(UNI_MEDSPEED, ((UWORD)dat * 125) / (33 * 4));
}
break;
/* 0xa 0xb PT effects */
case 0xc:
if (decimalvolumes)
dat = (dat >> 4) * 10 + (dat & 0xf);
UniPTEffect(0xc, dat);
break;
case 0xd: /* same as PT volslide */
UniPTEffect(0xa, dat);
break;
case 0xe: /* synth jmp - midi */
break;
case 0xf:
switch (dat) {
case 0: /* patternbreak */
UniPTEffect(0xd, 0);
break;
case 0xf1: /* play note twice */
UniWriteByte(UNI_MEDEFFECTF1);
break;
case 0xf2: /* delay note */
UniWriteByte(UNI_MEDEFFECTF2);
break;
case 0xf3: /* play note three times */
UniWriteByte(UNI_MEDEFFECTF3);
break;
case 0xfe: /* stop playing */
UniPTEffect(0xb, of.numpat);
break;
case 0xff: /* note cut */
UniPTEffect(0xc, 0);
break;
default:
if (dat <= 10)
UniPTEffect(0xf, dat);
else if (dat <= 240) {
if (bpmtempos)
UniPTEffect(0xf, (dat < 32) ? 32 : dat);
else
UniEffect(UNI_MEDSPEED, ((UWORD)dat * 125) / 33);
}
}
break;
default: /* all normal PT effects are handled here */
UniPTEffect(eff, dat);
break;
}
}
static UBYTE *MED_Convert1(int count, int col)
{
int t;
UBYTE inst, note, eff, dat;
MMD1NOTE *n;
UniReset();
for (t = 0; t < count; t++) {
n = &d1note(t, col);
note = n->a & 0x7f;
inst = n->b & 0x3f;
eff = n->c & 0xf;
dat = n->d;
if (inst)
UniInstrument(inst - 1);
if (note)
UniNote(note + 3 * OCTAVE - 1);
EffectCvt(eff, dat);
UniNewline();
}
return UniDup();
}
static UBYTE *MED_Convert0(int count, int col)
{
int t;
UBYTE a, b, inst, note, eff, dat;
MMD0NOTE *n;
UniReset();
for (t = 0; t < count; t++) {
n = &d0note(t, col);
a = n->a;
b = n->b;
note = a & 0x3f;
a >>= 6;
a = ((a & 1) << 1) | (a >> 1);
inst = (b >> 4) | (a << 4);
eff = b & 0xf;
dat = n->c;
if (inst)
UniInstrument(inst - 1);
if (note)
UniNote(note + 3 * OCTAVE - 1);
EffectCvt(eff, dat);
UniNewline();
}
return UniDup();
}
static int LoadMEDPatterns(void)
{
int t, row, col;
UWORD numtracks, numlines, maxlines = 0, track = 0;
MMD0NOTE *mmdp;
/* first, scan patterns to see how many channels are used */
for (t = 0; t < of.numpat; t++) {
_mm_fseek(modreader, ba[t], SEEK_SET);
numtracks = _mm_read_UBYTE(modreader);
numlines = _mm_read_UBYTE(modreader);
if (numtracks > of.numchn)
of.numchn = numtracks;
if (numlines > maxlines)
maxlines = numlines;
/* sanity check */
if (numtracks > 64)
return 0;
}
/* sanity check */
if (! of.numchn) /* docs say 4, 8, 12 or 16 */
return 0;
of.numtrk = of.numpat * of.numchn;
if (!AllocTracks())
return 0;
if (!AllocPatterns())
return 0;
if (!(mmd0pat = (MMD0NOTE *)MikMod_calloc(of.numchn * (maxlines + 1), sizeof(MMD0NOTE))))
return 0;
/* second read: read and convert patterns */
for (t = 0; t < of.numpat; t++) {
_mm_fseek(modreader, ba[t], SEEK_SET);
numtracks = _mm_read_UBYTE(modreader);
numlines = _mm_read_UBYTE(modreader);
of.pattrows[t] = ++numlines;
memset(mmdp = mmd0pat, 0, of.numchn * maxlines * sizeof(MMD0NOTE));
for (row = numlines; row; row--) {
for (col = numtracks; col; col--, mmdp++) {
mmdp->a = _mm_read_UBYTE(modreader);
mmdp->b = _mm_read_UBYTE(modreader);
mmdp->c = _mm_read_UBYTE(modreader);
}
}
for (col = 0; col < of.numchn; col++)
of.tracks[track++] = MED_Convert0(numlines, col);
}
return 1;
}
static int LoadMMD1Patterns(void)
{
int t, row, col;
UWORD numtracks, numlines, maxlines = 0, track = 0;
MMD1NOTE *mmdp;
/* first, scan patterns to see how many channels are used */
for (t = 0; t < of.numpat; t++) {
_mm_fseek(modreader, ba[t], SEEK_SET);
numtracks = _mm_read_M_UWORD(modreader);
numlines = _mm_read_M_UWORD(modreader);
if (numtracks > of.numchn)
of.numchn = numtracks;
if (numlines > maxlines)
maxlines = numlines;
/* sanity check */
if (numtracks > 64)
return 0;
if (numlines >= 3200) /* per docs */
return 0;
}
/* sanity check */
if (! of.numchn) /* docs say 4, 8, 12 or 16 */
return 0;
of.numtrk = of.numpat * of.numchn;
if (!AllocTracks())
return 0;
if (!AllocPatterns())
return 0;
if (!(mmd1pat = (MMD1NOTE *)MikMod_calloc(of.numchn * (maxlines + 1), sizeof(MMD1NOTE))))
return 0;
/* second read: really read and convert patterns */
for (t = 0; t < of.numpat; t++) {
_mm_fseek(modreader, ba[t], SEEK_SET);
numtracks = _mm_read_M_UWORD(modreader);
numlines = _mm_read_M_UWORD(modreader);
_mm_fseek(modreader, sizeof(ULONG), SEEK_CUR);
of.pattrows[t] = ++numlines;
memset(mmdp = mmd1pat, 0, of.numchn * maxlines * sizeof(MMD1NOTE));
for (row = numlines; row; row--) {
for (col = numtracks; col; col--, mmdp++) {
mmdp->a = _mm_read_UBYTE(modreader);
mmdp->b = _mm_read_UBYTE(modreader);
mmdp->c = _mm_read_UBYTE(modreader);
mmdp->d = _mm_read_UBYTE(modreader);
}
}
for (col = 0; col < of.numchn; col++)
of.tracks[track++] = MED_Convert1(numlines, col);
}
return 1;
}
static int MED_Load(int curious)
{
int t;
ULONG sa[64];
MEDINSTHEADER s;
SAMPLE *q;
MEDSAMPLE *mss;
/* try to read module header */
mh->id = _mm_read_M_ULONG(modreader);
mh->modlen = _mm_read_M_ULONG(modreader);
mh->MEDSONGP = _mm_read_M_ULONG(modreader);
mh->psecnum = _mm_read_M_UWORD(modreader);
mh->pseq = _mm_read_M_UWORD(modreader);
mh->MEDBlockPP = _mm_read_M_ULONG(modreader);
mh->reserved1 = _mm_read_M_ULONG(modreader);
mh->MEDINSTHEADERPP = _mm_read_M_ULONG(modreader);
mh->reserved2 = _mm_read_M_ULONG(modreader);
mh->MEDEXPP = _mm_read_M_ULONG(modreader);
mh->reserved3 = _mm_read_M_ULONG(modreader);
mh->pstate = _mm_read_M_UWORD(modreader);
mh->pblock = _mm_read_M_UWORD(modreader);
mh->pline = _mm_read_M_UWORD(modreader);
mh->pseqnum = _mm_read_M_UWORD(modreader);
mh->actplayline = _mm_read_M_SWORD(modreader);
mh->counter = _mm_read_UBYTE(modreader);
mh->extra_songs = _mm_read_UBYTE(modreader);
/* Seek to MEDSONG struct */
_mm_fseek(modreader, mh->MEDSONGP, SEEK_SET);
/* Load the MED Song Header */
mss = ms->sample; /* load the sample data first */
for (t = 63; t; t--, mss++) {
mss->rep = _mm_read_M_UWORD(modreader);
mss->replen = _mm_read_M_UWORD(modreader);
mss->midich = _mm_read_UBYTE(modreader);
mss->midipreset = _mm_read_UBYTE(modreader);
mss->svol = _mm_read_UBYTE(modreader);
mss->strans = _mm_read_SBYTE(modreader);
}
ms->numblocks = _mm_read_M_UWORD(modreader);
ms->songlen = _mm_read_M_UWORD(modreader);
_mm_read_UBYTES(ms->playseq, 256, modreader);
/* sanity check */
if (ms->numblocks > 255 || ms->songlen > 256) {
_mm_errno = MMERR_NOT_A_MODULE;
return 0;
}
ms->deftempo = _mm_read_M_UWORD(modreader);
ms->playtransp = _mm_read_SBYTE(modreader);
ms->flags = _mm_read_UBYTE(modreader);
ms->flags2 = _mm_read_UBYTE(modreader);
ms->tempo2 = _mm_read_UBYTE(modreader);
_mm_read_UBYTES(ms->trkvol, 16, modreader);
ms->mastervol = _mm_read_UBYTE(modreader);
ms->numsamples = _mm_read_UBYTE(modreader);
/* sanity check */
if (ms->numsamples > 64) {
_mm_errno = MMERR_NOT_A_MODULE;
return 0;
}
/* check for a bad header */
if (_mm_eof(modreader)) {
_mm_errno = MMERR_LOADING_HEADER;
return 0;
}
/* load extension structure */
if (mh->MEDEXPP) {
_mm_fseek(modreader, mh->MEDEXPP, SEEK_SET);
me->nextmod = _mm_read_M_ULONG(modreader);
me->exp_smp = _mm_read_M_ULONG(modreader);
me->s_ext_entries = _mm_read_M_UWORD(modreader);
me->s_ext_entrsz = _mm_read_M_UWORD(modreader);
me->annotxt = _mm_read_M_ULONG(modreader);
me->annolen = _mm_read_M_ULONG(modreader);
me->iinfo = _mm_read_M_ULONG(modreader);
me->i_ext_entries = _mm_read_M_UWORD(modreader);
me->i_ext_entrsz = _mm_read_M_UWORD(modreader);
me->jumpmask = _mm_read_M_ULONG(modreader);
me->rgbtable = _mm_read_M_ULONG(modreader);
me->channelsplit = _mm_read_M_ULONG(modreader);
me->n_info = _mm_read_M_ULONG(modreader);
me->songname = _mm_read_M_ULONG(modreader);
me->songnamelen = _mm_read_M_ULONG(modreader);
me->dumps = _mm_read_M_ULONG(modreader);
/* sanity check */
if (me->annolen > 0xffff) {
_mm_errno = MMERR_NOT_A_MODULE;
return 0;
}
/* truncate insane songnamelen (fail instead??) */
if (me->songnamelen > 256)
me->songnamelen = 256;
}
/* seek to and read the samplepointer array */
_mm_fseek(modreader, mh->MEDINSTHEADERPP, SEEK_SET);
if (!_mm_read_M_ULONGS(sa, ms->numsamples, modreader)) {
_mm_errno = MMERR_LOADING_HEADER;
return 0;
}
/* alloc and read the blockpointer array */
if (!(ba = (ULONG *)MikMod_calloc(ms->numblocks, sizeof(ULONG))))
return 0;
_mm_fseek(modreader, mh->MEDBlockPP, SEEK_SET);
if (!_mm_read_M_ULONGS(ba, ms->numblocks, modreader)) {
_mm_errno = MMERR_LOADING_HEADER;
return 0;
}
/* copy song positions */
if (!AllocPositions(ms->songlen))
return 0;
for (t = 0; t < ms->songlen; t++) {
of.positions[t] = ms->playseq[t];
if (of.positions[t]>ms->numblocks) { /* SANITIY CHECK */
/* fprintf(stderr,"positions[%d]=%d > numpat=%d\n",t,of.positions[t],ms->numblocks);*/
_mm_errno = MMERR_LOADING_HEADER;
return 0;
}
}
decimalvolumes = (ms->flags & 0x10) ? 0 : 1;
bpmtempos = (ms->flags2 & 0x20) ? 1 : 0;
if (bpmtempos) {
int bpmlen = (ms->flags2 & 0x1f) + 1;
of.initspeed = ms->tempo2;
of.inittempo = ms->deftempo * bpmlen / 4;
if (bpmlen != 4) {
/* Let's do some math : compute GCD of BPM beat length and speed */
int a, b;
a = bpmlen;
b = ms->tempo2;
if (a > b) {
t = b;
b = a;
a = t;
}
while ((a != b) && (a)) {
t = a;
a = b - a;
b = t;
if (a > b) {
t = b;
b = a;
a = t;
}
}
of.initspeed /= b;
of.inittempo = ms->deftempo * bpmlen / (4 * b);
}
} else {
of.initspeed = ms->tempo2;
of.inittempo = ms->deftempo ? ((UWORD)ms->deftempo * 125) / 33 : 128;
if ((ms->deftempo <= 10) && (ms->deftempo))
of.inittempo = (of.inittempo * 33) / 6;
of.flags |= UF_HIGHBPM;
}
MED_Version[12] = mh->id;
of.modtype = MikMod_strdup(MED_Version);
of.numchn = 0; /* will be counted later */
of.numpat = ms->numblocks;
of.numpos = ms->songlen;
of.numins = ms->numsamples;
of.numsmp = of.numins;
of.reppos = 0;
if ((mh->MEDEXPP) && (me->songname) && (me->songnamelen)) {
char *name;
_mm_fseek(modreader, me->songname, SEEK_SET);
name = (char *) MikMod_malloc(me->songnamelen);
_mm_read_UBYTES(name, me->songnamelen, modreader);
of.songname = DupStr(name, me->songnamelen, 1);
MikMod_free(name);
} else
of.songname = DupStr(NULL, 0, 0);
if ((mh->MEDEXPP) && (me->annotxt) && (me->annolen)) {
_mm_fseek(modreader, me->annotxt, SEEK_SET);
ReadComment(me->annolen);
}
if (!AllocSamples())
return 0;
q = of.samples;
for (t = 0; t < of.numins; t++) {
q->flags = SF_SIGNED;
q->volume = 64;
if (sa[t]) {
_mm_fseek(modreader, sa[t], SEEK_SET);
s.length = _mm_read_M_ULONG(modreader);
s.type = _mm_read_M_SWORD(modreader);
if (s.type) {
#ifdef MIKMOD_DEBUG
fprintf(stderr, "\rNon-sample instruments not supported in MED loader yet\n");
#endif
if (!curious) {
_mm_errno = MMERR_MED_SYNTHSAMPLES;
return 0;
}
s.length = 0;
}
if (_mm_eof(modreader)) {
_mm_errno = MMERR_LOADING_SAMPLEINFO;
return 0;
}
q->length = s.length;
q->seekpos = _mm_ftell(modreader);
q->loopstart = ms->sample[t].rep << 1;
q->loopend = q->loopstart + (ms->sample[t].replen << 1);
if (ms->sample[t].replen > 1)
q->flags |= SF_LOOP;
/* don't load sample if length>='MMD0'...
such kluges make libmikmod's code unique !!! */
if (q->length >= MMD0_string)
q->length = 0;
} else
q->length = 0;
if ((mh->MEDEXPP) && (me->exp_smp) &&
(t < me->s_ext_entries) && (me->s_ext_entrsz >= 4)) {
MEDINSTEXT ie;
_mm_fseek(modreader, me->exp_smp + t * me->s_ext_entrsz,
SEEK_SET);
ie.hold = _mm_read_UBYTE(modreader);
ie.decay = _mm_read_UBYTE(modreader);
ie.suppress_midi_off = _mm_read_UBYTE(modreader);
ie.finetune = _mm_read_SBYTE(modreader);
q->speed = finetune[ie.finetune & 0xf];
} else
q->speed = 8363;
if ((mh->MEDEXPP) && (me->iinfo) &&
(t < me->i_ext_entries) && (me->i_ext_entrsz >= 40)) {
MEDINSTINFO ii;
_mm_fseek(modreader, me->iinfo + t * me->i_ext_entrsz, SEEK_SET);
_mm_read_UBYTES(ii.name, 40, modreader);
q->samplename = DupStr((char*)ii.name, 40, 1);
} else
q->samplename = NULL;
q++;
}
if (mh->id == MMD0_string) {
if (!LoadMEDPatterns()) {
_mm_errno = MMERR_LOADING_PATTERN;
return 0;
}
} else if (mh->id == MMD1_string) {
if (!LoadMMD1Patterns()) {
_mm_errno = MMERR_LOADING_PATTERN;
return 0;
}
} else {
_mm_errno = MMERR_NOT_A_MODULE;
return 0;
}
return 1;
}
static CHAR *MED_LoadTitle(void)
{
ULONG posit, namelen;
CHAR *name, *retvalue = NULL;
_mm_fseek(modreader, 0x20, SEEK_SET);
posit = _mm_read_M_ULONG(modreader);
if (posit) {
_mm_fseek(modreader, posit + 0x2C, SEEK_SET);
posit = _mm_read_M_ULONG(modreader);
namelen = _mm_read_M_ULONG(modreader);
_mm_fseek(modreader, posit, SEEK_SET);
name = (CHAR*) MikMod_malloc(namelen);
_mm_read_UBYTES(name, namelen, modreader);
retvalue = DupStr(name, namelen, 1);
MikMod_free(name);
}
return retvalue;
}
/*========== Loader information */
MIKMODAPI MLOADER load_med = {
NULL,
"MED",
"MED (OctaMED)",
MED_Init,
MED_Test,
MED_Load,
MED_Cleanup,
MED_LoadTitle
};
/* ex:set ts=4: */