view minijss/jloadxm.c @ 2576:812b16ee49db

I had been living under apparent false impression that "realfft.c" on which the FFT implementation in DMLIB was basically copied from was released in public domain at some point, but it could very well be that it never was. Correct license is (or seems to be) GNU GPL. Thus I removing the code from DMLIB, and profusely apologize to the author, Philip Van Baren. It was never my intention to distribute code based on his original work under a more liberal license than originally intended.
author Matti Hamalainen <ccr@tnsp.org>
date Fri, 11 Mar 2022 16:32:50 +0200
parents 0c0576544d41
children 9807ae37ad69
line wrap: on
line source

/*
 * miniJSS - Fast Tracker ][ (XM) module loader
 * Programmed and designed by Matti 'ccr' Hamalainen
 * (C) Copyright 2006-2015 Tecnic Software productions (TNSP)
 *
 * TO DO:
 * - Add support for 1.02/1.03 XM-format versions.
 *   (Not very useful, but if it's not too hard, then do it)
 */
#include "jssmod.h"


/* XM value limit definitions
 */
#define XM_MaxChannels      (32)
#define XM_MaxPatterns      (256)
#define XM_MaxOrders        (255)
#define XM_MaxInstruments   (128)
#define XM_MaxInstSamples   (16)
#define XM_MaxEnvPoints     (12)
#define XM_MaxNotes         (96)
#define XM_MaxSampleVolume  (64)
#define XM_HeaderSize       276


/* XM format structures
 */
typedef struct
{
    char    idMagic[17];       // XM header ID "Extended Module: "
    char    songName[20];      // Module song name
    Uint8   unUsed1A;          // ALWAYS 0x1a
    char    trackerName[20];   // ID-string of tracker software
    Uint16  version;           // XM-version tag
    Uint32  headSize;          // Module header size, FROM THIS POINT!
    Uint16  norders,           // Number of orders
            defRestartPos,     // Default song restart position
            nchannels,         // Number of channels
            npatterns,         // Number of patterns
            ninstruments,      // Number of instruments
            flags,             // Module flags:
                               // bit0: 0 = Amiga frequency table
                               //       1 = Linear frequency table
                               //
            defSpeed,          // Default speed
            defTempo;          // Default tempo
    Uint8   orderList[256];    // Order list
} XMHeader;


typedef struct
{
    Uint32  headSize;          // Instrument header size (see docs!)
    char    instName[22];      // Name/description
    Uint8   instType;          // Type
    Uint16  nsamples;          // Number of samples
} XMInstrument1;


typedef struct
{
    Uint16 frame, value;
} XMEnvPoint;


typedef struct
{
    Uint8 flags, npoints, sustain, loopS, loopE;
    XMEnvPoint points[XM_MaxEnvPoints];
} XMEnvelope;


typedef struct
{
    Uint32 headSize;                  // Header size
    Uint8 sNumForNotes[XM_MaxNotes];  // Sample numbers for notes
    XMEnvelope volumeEnv, panningEnv;
    Uint8 vibratoType, vibratoSweep, vibratoDepth, vibratoRate;

    Uint16 fadeOut, ARESERVED;
} XMInstrument2;


typedef struct
{
    Uint32 size, loopS, loopL;
    Uint8 volume;
    Sint8 fineTune;
    Uint8 type, panning;
    Sint8 relNote;
    Uint8 ARESERVED;
    char sampleName[22];
} XMSample;


typedef struct
{
    Uint32 headSize;
    Uint8 packing;
    Uint16 nrows, size;
} XMPattern;



/* Unpack XM-format pattern from file-stream into JSS-pattern structure
 */
#define JSGETBYTE(XV) \
do {                                                    \
    if (size <= 0)                                      \
        JSSERROR(DMERR_OUT_OF_DATA, DMERR_OUT_OF_DATA,  \
        "Unexpected end of packed pattern data.\n");    \
    size--;                                             \
    XV = dmfgetc(inFile);                               \
} while (0)


/* Convert XM note value to internal JSS note
 */
static int jssXMConvertNote(const int val)
{
    if (val < 1 || val > 97)
        return jsetNotSet;
    else
    if (val == 97)
        return jsetNoteOff;
    else
        return val - 1;
}


/* Unpack a XM pattern structure from resource to given JSS pattern
 */
static int jssXMUnpackPattern104(DMResource *inFile, int size, JSSPattern *pattern)
{
    JSSNote *pnote = pattern->data;

    for (int row = 0; row < pattern->nrows && size > 0; row++)
    for (int channel = 0; channel < pattern->nchannels && size > 0; channel++)
    {
        int packb, tmp;
        JSGETBYTE(packb);
        if (packb & 0x80)
        {
            if (packb & 0x01)
            {
                // PACK 0x01: Read note
                JSGETBYTE(tmp);
                pnote->note = jssXMConvertNote(tmp);
            }

            if (packb & 0x02)
            {
                // PACK 0x02: Read instrument
                JSGETBYTE(tmp);
                pnote->instrument = (tmp > 0) ? tmp - 1 : jsetNotSet;
            }

            if (packb & 0x04)
            {
                // PACK 0x04: Read volume
                JSGETBYTE(tmp);
                pnote->volume = (tmp >= 0x10) ? tmp - 0x10 : jsetNotSet;
            }

            if (packb & 0x08)
            {
                // PACK 0x08: Read effect
                JSGETBYTE(pnote->effect);
                pnote->param = 0;
            }

            if (packb & 0x10)
            {
                // PACK 0x10: Read effect parameter
                JSGETBYTE(pnote->param);
                if (pnote->effect == jsetNotSet && pnote->param != 0)
                    pnote->effect = 0;
            }
        }
        else
        {
            // All data available
            pnote->note = jssXMConvertNote(packb & 0x7f);

            // Get instrument
            JSGETBYTE(tmp);
            pnote->instrument = (tmp > 0) ? tmp - 1 : jsetNotSet;

            // Get volume
            JSGETBYTE(tmp);
            pnote->volume = (tmp >= 0x10) ? tmp - 0x10 : jsetNotSet;

            // Get effect
            JSGETBYTE(pnote->effect);

            // Get parameter
            JSGETBYTE(pnote->param);
            if (pnote->effect == 0 && pnote->param == 0)
                pnote->effect = pnote->param = jsetNotSet;
        }
        pnote++;
    }

    // Check the state
    if (size > 0)
    {
        // Some data left unparsed
        JSSWARNING(DMERR_EXTRA_DATA, DMERR_EXTRA_DATA,
            "Unparsed data after pattern (%d bytes), possibly broken file.\n",
            size);
    }

    return DMERR_OK;
}


static int jssXMUnpackPattern102(DMResource *inFile, int size, JSSPattern *pattern)
{
    JSSNote *pnote = pattern->data;

    for (int row = 0; row < pattern->nrows && size > 0; row++)
    for (int channel = 0; channel < pattern->nchannels && size > 0; channel++)
    {
        int packb, tmp;
        JSGETBYTE(packb);
        if (packb & 0x80)
        {
            if (packb & 0x01)
            {
                // PACK 0x01: Read note
                JSGETBYTE(tmp);
                pnote->note = jssXMConvertNote(tmp);
            }

            if (packb & 0x02)
            {
                // PACK 0x02: Read instrument
                JSGETBYTE(tmp);
                pnote->instrument = (tmp > 0) ? tmp - 1 : jsetNotSet;
            }

            if (packb & 0x04)
            {
                // PACK 0x04: Read volume
                JSGETBYTE(tmp);
                pnote->volume = (tmp >= 0x10) ? tmp - 0x10 : jsetNotSet;
            }

            if (packb & 0x08)
            {
                // PACK 0x08: Read effect
                JSGETBYTE(pnote->effect);
                pnote->param = 0;
            }

            if (packb & 0x10)
            {
                // PACK 0x10: Read effect parameter
                JSGETBYTE(pnote->param);
                if (pnote->effect == jsetNotSet && pnote->param != 0)
                    pnote->effect = 0;
            }
        }
        else
        {
            // All data available
            pnote->note = jssXMConvertNote(packb & 0x7f);

            // Get instrument
            JSGETBYTE(tmp);
            pnote->instrument = (tmp > 0) ? tmp - 1 : jsetNotSet;

            // Get volume
            JSGETBYTE(tmp);
            pnote->volume = (tmp >= 0x10) ? tmp - 0x10 : jsetNotSet;

            if (tmp & 0xc0)
            {
                JSGETBYTE(pnote->effect);
                JSGETBYTE(pnote->param);
            }
            else
            {
                JSGETBYTE(pnote->param);
                pnote->effect = tmp & 0xf;
            }

            if (pnote->effect == 0 && pnote->param == 0)
                pnote->effect = pnote->param = jsetNotSet;
        }
        pnote++;
    }

    // Check the state
    if (size > 0)
    {
        // Some data left unparsed
        JSSWARNING(DMERR_EXTRA_DATA, DMERR_EXTRA_DATA,
            "Unparsed data after pattern (%d bytes), possibly broken file.\n",
            size);
    }

    return DMERR_OK;
}


static int jssXMLoadPatterns(DMResource *inFile, JSSModule *module, XMHeader *xmH)
{
    for (int index = 0; index < module->npatterns; index++)
    {
        XMPattern xmP;
        off_t remainder, pos = dmftell(inFile);
        Uint32 headSize = 0;
        BOOL ret = FALSE;
        int res;

        // Get the pattern header size and packing
        if (!dmf_read_le32(inFile, &xmP.headSize) ||
            !dmf_read_byte(inFile, &xmP.packing))
        {
            JSSERROR(DMERR_FREAD, DMERR_FREAD,
                "Could not read pattern header #%d.\n",
                index);
        }

        // Different format versions have slightly different headers
        if (module->intVersion == 0x0102)
        {
            Uint8 tmp;
            // 0x0102 has one byte number of rows
            ret = dmf_read_byte(inFile, &tmp);
            xmP.nrows = ((Uint16) tmp) + 1;
            headSize = 4 + 1 + 1 + 2;
        }
        else
        if (module->intVersion == 0x0104)
        {
            // 0x0104 has 16-bit word for nrows
            ret = dmf_read_le16(inFile, &xmP.nrows);
            headSize = 4 + 1 + 2 + 2;
        }

        // Check header size against known values
        if (headSize != xmP.headSize)
        {
            JSSERROR(DMERR_NOT_SUPPORTED, DMERR_NOT_SUPPORTED,
                "Invalid pattern #%d header size %d, expected %d bytes.\n",
                index, xmP.headSize, headSize);
        }

        // Read rest of the header
        if (!ret || !dmf_read_le16(inFile, &xmP.size))
        {
            JSSERROR(DMERR_FREAD, DMERR_FREAD,
                "Could not read pattern header data #%d.\n",
                index);
        }

        // Sanity-check rest of the header data
        if (xmP.packing != 0)
        {
            JSSERROR(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
                "Pattern #%d packing type unsupported (%d)\n",
                index, xmP.packing);
        }

        if (xmP.nrows == 0)
        {
            JSSERROR(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
                "Pattern #%d has %d rows, invalid data.\n",
                index, xmP.nrows);
        }

        if (xmP.size > 0)
        {
            // Allocate and unpack pattern
            module->patterns[index] = jssAllocatePattern(xmP.nrows, module->nchannels);
            if (module->patterns[index] == NULL)
                JSSERROR(DMERR_MALLOC, DMERR_MALLOC,
                "Could not allocate memory for pattern #%d\n", index);

            switch (module->intVersion)
            {
                case 0x0104:
                    res = jssXMUnpackPattern104(inFile, xmP.size, module->patterns[index]);
                    break;
                case 0x0102:
                    res = jssXMUnpackPattern102(inFile, xmP.size, module->patterns[index]);
                    break;
            }

            if (res != DMERR_OK)
                JSSERROR(res, res, "Error in unpacking pattern #%d data\n", index);
        }

        // Skip extra data if there is any .. shouldn't usually happen tho.
        remainder = xmP.headSize - (dmftell(inFile) - pos);
        if (remainder > 0)
        {
            JSSDEBUG("xmP Skipping: %li\n", remainder);
            dmfseek(inFile, remainder, SEEK_CUR);
        }
    }

    // Allocate the empty pattern
    module->patterns[jsetMaxPatterns] = jssAllocatePattern(64, module->nchannels);

    /* Convert song orders list by replacing nonexisting
     * pattern numbers with pattern number jsetMaxPatterns.
     */
    for (int index = 0; index < module->norders; index++)
    {
        int tmp = xmH->orderList[index];
        if (tmp >= module->npatterns ||
            module->patterns[tmp] == NULL)
            tmp = jsetMaxPatterns;

        module->orderList[index] = tmp;
    }

    return DMERR_OK;
}


/* Convert XM envelope structure to JSS envelope structure
 */
static int jssXMConvertEnvelope(
    JSSEnvelope *dst, XMEnvelope *src,
    const char *name, const int ninstr)
{
    (void) name;
    (void) ninstr;

    // Convert envelope points
    for (int i = 0; i < XM_MaxEnvPoints; i++)
    {
        dst->points[i].frame = src->points[i].frame;
        dst->points[i].value = src->points[i].value;
    }

    // Convert other values
    dst->npoints = src->npoints;
    dst->sustain = src->sustain;
    dst->loopS   = src->loopS;
    dst->loopE   = src->loopE;

    // Check if the envelope is used
    if (src->flags & 0x01)
    {
        // Convert envelope flags
        dst->flags = jenvfUsed;
        if (src->flags & 0x02)
            dst->flags |= jenvfSustain;

        if (src->flags & 0x04)
            dst->flags |= jenvfLooped;

        if (src->flags & 0xf0)
        {
            JSSWARNING(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
                "Inst#%d/%s-env: Uses unsupported flags values, 0x%02x.\n",
                ninstr, name, src->flags);
        }

        // Check other values
        if (src->npoints > XM_MaxEnvPoints)
        {
            JSSWARNING(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
                "Inst#%d/%s-env: npoints > MAX, possibly broken file.\n",
                ninstr, name);
            dst->npoints = XM_MaxEnvPoints;
        }

        if ((dst->flags & jenvfSustain) && src->sustain > src->npoints)
        {
            JSSWARNING(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
                "Inst#%d/%s-env: sustain > npoints (%d > %d), possibly broken file.\n",
                ninstr, name, src->sustain, src->npoints);
            dst->sustain = src->npoints;
        }

        if ((dst->flags & jenvfLooped) && src->loopE > src->npoints)
        {
            JSSWARNING(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
                "Inst#%d/%s-env: loopE > npoints (%d > %d), possibly broken file.\n",
                ninstr, name, src->loopE, src->npoints);
            dst->loopE = src->npoints;
        }

        if ((dst->flags & jenvfLooped) && src->loopS > src->loopE)
        {
            JSSWARNING(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
                "Inst#%d/%s-env: loopS > loopE (%d > %d), possibly broken file.\n",
                ninstr, name, src->loopS, src->loopE);
            dst->loopS = src->loopE;
        }
    }

    return DMERR_OK;
}


static int jssXMLoadSampleInstrument(
    DMResource *inFile, JSSModule *module,
    JSSExtInstrument *einst, int neinst, int nsample)
{
    XMSample xmS;
    JSSInstrument *inst;

    (void) neinst;

    // Read header data
    if (!dmf_read_le32(inFile, &xmS.size) ||
        !dmf_read_le32(inFile, &xmS.loopS) ||
        !dmf_read_le32(inFile, &xmS.loopL) ||
        !dmf_read_byte(inFile, &xmS.volume) ||
        !dmf_read_byte(inFile, (Uint8 *) &xmS.fineTune) ||
        !dmf_read_byte(inFile, &xmS.type) ||
        !dmf_read_byte(inFile, &xmS.panning) ||
        !dmf_read_byte(inFile, (Uint8 *) &xmS.relNote) ||
        !dmf_read_byte(inFile, &xmS.ARESERVED) ||
        !dmf_read_str(inFile, &xmS.sampleName, sizeof(xmS.sampleName)))
    {
        JSSERROR(DMERR_FREAD, DMERR_FREAD,
            "Error reading instrument sample header #%d/%d [%d]\n",
            neinst, nsample, module->ninstruments);
    }

    if (xmS.size <= 0)
        return DMERR_OK;

    if (xmS.size > 16 * 1024 * 1024)
    {
        JSSERROR(DMERR_FREAD, DMERR_FREAD,
            "Sample instrument #%d/%d [%d] too large, %d bytes.\n",
            neinst, nsample, module->ninstruments, xmS.size);
    }

    // Allocate sample instrument
    JSSDEBUG("Allocating sample #%d/%d [%d]\n",
        neinst, nsample, module->ninstruments);

    einst->instConvTable[nsample] = module->ninstruments;
    inst = module->instruments[module->ninstruments] = jssAllocateInstrument();
    if (inst == NULL)
    {
        JSSERROR(DMERR_MALLOC, DMERR_MALLOC,
            "Could not allocate sample #%d/%d [%d]\n",
            neinst, nsample, module->ninstruments);
    }
    module->ninstruments++;

    // Copy values
    if (xmS.volume > XM_MaxSampleVolume)
    {
        JSSWARNING(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
            "Samp #%d/%d: volume > MAX\n", neinst, nsample);
        xmS.volume = XM_MaxSampleVolume;
    }

    inst->volume    = xmS.volume;
    inst->ERelNote  = xmS.relNote;
    inst->EFineTune = xmS.fineTune;
    inst->EPanning  = xmS.panning;
#ifndef JSS_LIGHT
    inst->desc      = jssASCIItoStr(xmS.sampleName, 0, sizeof(xmS.sampleName));
#endif

    // Convert flags
    switch (xmS.type & 0x03)
    {
        case 0: inst->flags = 0; break;
        case 1: inst->flags = jsfLooped; break;
        default:
                // Basically 2 is the value for bidi-loop, and
                // 3 is undefined, but some module writers might've
                // assumed that this is a bit-field, e.g. 1 | 2 = 3
                inst->flags = jsfLooped | jsfBiDi; break;
            break;
    }

    if (xmS.type & 0x10)
    {
        // 16-bit sample
        JSFSET(inst->flags, jsf16bit);
        inst->size  = xmS.size / sizeof(Uint16);
        inst->loopS = xmS.loopS / sizeof(Uint16);
        inst->loopE = (xmS.loopS + xmS.loopL) / sizeof(Uint16);
        if ((xmS.size & 1) || (xmS.loopS & 1) || (xmS.loopL & 1))
        {
            JSSWARNING(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
                "Samp #%d/%d: size=%d, loopS=%d or loopL=%d not divisible by 2 for 16-bit sample.\n",
                neinst, nsample, xmS.size, xmS.loopS, xmS.loopL);
        }
    }
    else
    {
        // 8-bit sample
        inst->size  = xmS.size;
        inst->loopS = xmS.loopS;
        inst->loopE = xmS.loopS + xmS.loopL;
    }

    if (xmS.loopL == 0)
    {
        // Always unset loop, if loop length is zero
        JSFUNSET(inst->flags, jsfLooped);
    }

    if (inst->flags & jsfLooped)
    {
        if (inst->loopS >= inst->size)
        {
            JSSWARNING(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
                "Samp #%d/%d: loopS >= size (%d >= %d)\n",
                neinst, nsample, inst->loopS, inst->size);
            JSFUNSET(inst->flags, jsfLooped);
        }

        if (inst->loopE > inst->size)
        {
            JSSWARNING(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
                "Samp #%d/%d: loopE > size (%d > %d)\n",
                neinst, nsample, inst->loopE, inst->size);
            JSFUNSET(inst->flags, jsfLooped);
        }
    }

    return DMERR_OK;
}


static int jssXMLoadSampleData(DMResource *inFile, JSSInstrument *inst, int ninst, int nsample)
{
    int ret;
    size_t bsize;

    (void) ninst;
    (void) nsample;

    JSSDEBUG(
        "desc....: '%s'\n"
        "size....: %d\n"
        "loopS...: %d\n"
        "loopE...: %d\n"
        "volume..: %d\n"
        "flags...: %x\n",
        inst->desc,
        inst->size, inst->loopS, inst->loopE,
        inst->volume, inst->flags);

    // Allocate memory for sample data
    bsize  = (inst->flags & jsf16bit) ? sizeof(Uint16) : sizeof(Uint8);
    bsize *= inst->size;

    if ((inst->data = dmMalloc(bsize)) == NULL)
    {
        JSSERROR(DMERR_MALLOC, DMERR_MALLOC,
            "Could not allocate %d bytes of sample data for instrument/sample #%d/%d.\n",
            bsize, ninst, nsample);
    }

    // Read sampledata
    if (!dmf_read_str(inFile, inst->data, bsize))
    {
        JSSERROR(DMERR_FREAD, DMERR_FREAD,
            "Error reading sample data for instrument #%d/%d, %d bytes.\n",
            ninst, nsample, bsize);
    }

    // Convert the sample data
    if (inst->flags & jsf16bit)
    {
        ret = jssDecodeSample16(
            (Uint16 *) inst->data, inst->size,
#if (SDL_BYTEORDER == SDL_BIG_ENDIAN)
            (jsampDelta | jsampSwapEndianess)
#else
            (jsampDelta)
#endif
            );
    }
    else
    {
        ret = jssDecodeSample8(
            (Uint8 *) inst->data, inst->size,
            (jsampDelta | jsampFlipSign));
    }

    return ret;
}


static int jssXMLoadInstrumentSamples(
    DMResource *inFile, JSSModule *module,
    JSSExtInstrument *einst, int neinst)
{
    int nsample, ret;

    for (nsample = 0; nsample < einst->nsamples; nsample++)
    if (einst->instConvTable[nsample] != jsetNotSet)
    {
        JSSInstrument *inst = module->instruments[einst->instConvTable[nsample]];
        if ((ret = jssXMLoadSampleData(inFile, inst, neinst, nsample)) != DMERR_OK)
            return ret;
    }

    return DMERR_OK;
}


static BOOL jssXMLoadEnvelopePoints(DMResource *inFile, XMEnvelope *env)
{
    int i;
    for (i = 0; i < XM_MaxEnvPoints; i++)
    {
        if (!dmf_read_le16(inFile, &(env->points[i].frame)) ||
            !dmf_read_le16(inFile, &(env->points[i].value)))
            return FALSE;
    }
    return TRUE;
}


static BOOL jssXMLoadEnvelopeData(DMResource *inFile, XMEnvelope *env)
{
    return
        dmf_read_byte(inFile, &(env->sustain)) &&
        dmf_read_byte(inFile, &(env->loopS)) &&
        dmf_read_byte(inFile, &(env->loopE));
}


/* Load XM-format extended instrument from file-stream into JSS module's given inst
 */
static int jssXMLoadExtInstrument(DMResource *inFile, int neinst, JSSModule *module)
{
    XMInstrument1 xmI1;
    off_t remainder, pos = dmftell(inFile);
    JSSExtInstrument *einst;
    XMInstrument2 xmI2;
    int i, nsample, ret;

    // Get instrument header #1
    if (!dmf_read_le32(inFile, &xmI1.headSize) ||
        !dmf_read_str(inFile, &xmI1.instName, sizeof(xmI1.instName)) ||
        !dmf_read_byte(inFile, &xmI1.instType) ||
        !dmf_read_le16(inFile, &xmI1.nsamples))
    {
        JSSERROR(DMERR_FREAD, DMERR_FREAD,
            "Failed to read primary extended instrument header #%d.\n",
            neinst);
    }

    // If there are samples, there is header #2
    if (xmI1.nsamples == 0)
    {
        // We may STILL need to skip extra data after 1st instr. header
        remainder = xmI1.headSize - (dmftell(inFile) - pos);
        if (remainder > 0)
        {
            JSSDEBUG("xmI1#2 Skipping: %li\n", remainder);
            dmfseek(inFile, remainder, SEEK_CUR);
        }
        return DMERR_OK;
    }

    if (xmI1.nsamples > XM_MaxInstSamples)
    {
        JSSERROR(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
            "Extended instrument #%d has invalid number of samples %d.\n",
            neinst, xmI1.nsamples);
    }

    // Allocate instrument
    JSSDEBUG("Allocating extended instrument #%d [%d]\n",
        neinst, module->nextInstruments);

    if ((einst = jssAllocateExtInstrument()) == NULL)
    {
        JSSERROR(DMERR_MALLOC, DMERR_MALLOC,
            "Could not allocate extended instrument #%d.\n",
            neinst);
    }

    module->extInstruments[neinst] = einst;

    // Get instrument header #2
    if (!dmf_read_le32(inFile, &xmI2.headSize) ||
        !dmf_read_str(inFile, &xmI2.sNumForNotes, sizeof(xmI2.sNumForNotes)))
    {
        JSSERROR(DMERR_FREAD, DMERR_FREAD,
            "Could not read secondary instrument header part #1 for #%d.\n",
            neinst);
    }

    if (!jssXMLoadEnvelopePoints(inFile, &xmI2.volumeEnv) ||
        !jssXMLoadEnvelopePoints(inFile, &xmI2.panningEnv))
    {
        JSSERROR(DMERR_FREAD, DMERR_FREAD,
            "Could not read envelope point data for instrument #%d.\n",
            neinst);
    }

    if (!dmf_read_byte(inFile, &xmI2.volumeEnv.npoints) ||
        !dmf_read_byte(inFile, &xmI2.panningEnv.npoints) ||

        !jssXMLoadEnvelopeData(inFile, &xmI2.volumeEnv) ||
        !jssXMLoadEnvelopeData(inFile, &xmI2.panningEnv) ||

        !dmf_read_byte(inFile, &xmI2.volumeEnv.flags) ||
        !dmf_read_byte(inFile, &xmI2.panningEnv.flags) ||

        !dmf_read_byte(inFile, &xmI2.vibratoType) ||
        !dmf_read_byte(inFile, &xmI2.vibratoSweep) ||
        !dmf_read_byte(inFile, &xmI2.vibratoDepth) ||
        !dmf_read_byte(inFile, &xmI2.vibratoRate) ||

        !dmf_read_le16(inFile, &xmI2.fadeOut) ||
        !dmf_read_le16(inFile, &xmI2.ARESERVED))
    {
        JSSERROR(DMERR_FREAD, DMERR_FREAD,
            "Could not read secondary instrument header part #2 for #%d.\n",
            neinst);
    }

    // Skip the extra data after header #2
    remainder = xmI1.headSize - (dmftell(inFile) - pos);
    if (remainder > 0)
    {
        JSSDEBUG("xmI1#1 Skipping: %li\n", remainder);
        dmfseek(inFile, remainder, SEEK_CUR);
    }

    // Check and convert all ext instrument information
#ifndef JSS_LIGHT
    einst->desc = jssASCIItoStr(xmI1.instName, 0, sizeof(xmI1.instName));
#endif
    jssXMConvertEnvelope(&einst->volumeEnv, &xmI2.volumeEnv, "vol", neinst);
    jssXMConvertEnvelope(&einst->panningEnv, &xmI2.panningEnv, "pan", neinst);

    switch (xmI2.vibratoType)
    {
        case 0: einst->vibratoType = jvibSine; break;
        case 1: einst->vibratoType = jvibRamp; break;
        case 2: einst->vibratoType = jvibSquare; break;
        case 3: einst->vibratoType = jvibRandom; break;
        default:
            einst->vibratoType = jvibSine;
            JSSWARNING(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
                "Invalid extinstrument vibrato type %d for inst #%d\n",
                neinst);
            break;
    }
    einst->vibratoSweep = xmI2.vibratoSweep;
    einst->vibratoDepth = xmI2.vibratoDepth;
    einst->vibratoRate  = xmI2.vibratoRate;
    einst->fadeOut      = xmI2.fadeOut;
    einst->nsamples     = xmI1.nsamples;

    // Initialize the SNumForNotes conversion table
    if ((einst->instConvTable = dmCalloc(XM_MaxInstruments + 1, sizeof(int))) == NULL)
    {
        JSSERROR(DMERR_MALLOC, DMERR_MALLOC,
            "Could not allocate memory for instConvTable of instrument #%d.\n",
            neinst);
    }

    for (i = 0; i < XM_MaxInstruments; i++)
        einst->instConvTable[i] = jsetNotSet;

    // Read sample headers
    for (nsample = 0; nsample < einst->nsamples; nsample++)
    {
        if ((ret = jssXMLoadSampleInstrument(inFile, module, einst, neinst, nsample)) != DMERR_OK)
            return ret;
    }

    // Apply new values to sNumForNotes values
    for (i = 0; i < XM_MaxNotes; i++)
    {
        int tmp = xmI2.sNumForNotes[i];
        if (tmp >= 0 && tmp < xmI1.nsamples)
            einst->sNumForNotes[i] = einst->instConvTable[tmp];
        else
        {
            einst->sNumForNotes[i] = jsetNotSet;
            JSSWARNING(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
                "Ext.instrument #%d sNumForNotes[%d] out of range (%d).\n",
                neinst, i, tmp);
        }
    }

    // Read sample data if needed
    if (module->intVersion == 0x0104)
    {
        if ((ret = jssXMLoadInstrumentSamples(inFile, module, einst, neinst)) != DMERR_OK)
            return ret;
    }

    return DMERR_OK;
}


static int jssXMLoadInstruments(DMResource *inFile, JSSModule *module)
{
    int index;
    for (index = 0; index < module->nextInstruments; index++)
    {
        int result = jssXMLoadExtInstrument(inFile, index, module);
        if (result != 0)
            JSSERROR(result, result, "Errors while reading instrument #%d\n",
            index);
    }
    return DMERR_OK;
}


/* Load XM-format module from given file-stream
 */
int jssLoadXM(DMResource *inFile, JSSModule **pmodule, BOOL probe)
{
    JSSModule *module;
    XMHeader xmH;
    int index, ret = DMERR_OK;

    *pmodule = NULL;

    // Try to read the XM header
    if (!dmf_read_str(inFile, &xmH.idMagic, sizeof(xmH.idMagic)) ||
        !dmf_read_str(inFile, &xmH.songName, sizeof(xmH.songName)) ||
        !dmf_read_byte(inFile, &xmH.unUsed1A) ||
        !dmf_read_str(inFile, &xmH.trackerName, sizeof(xmH.trackerName)) ||
        !dmf_read_le16(inFile, &xmH.version) ||
        !dmf_read_le32(inFile, &xmH.headSize) ||
        !dmf_read_le16(inFile, &xmH.norders) ||
        !dmf_read_le16(inFile, &xmH.defRestartPos) ||
        !dmf_read_le16(inFile, &xmH.nchannels) ||
        !dmf_read_le16(inFile, &xmH.npatterns) ||
        !dmf_read_le16(inFile, &xmH.ninstruments) ||
        !dmf_read_le16(inFile, &xmH.flags) ||
        !dmf_read_le16(inFile, &xmH.defSpeed) ||
        !dmf_read_le16(inFile, &xmH.defTempo))
        return DMERR_FREAD;

    // Check the fields, none of these are considered fatal
    if (strncmp(xmH.idMagic, "Extended Module: ", 17) != 0)
    {
        if (probe)
            return DMERR_NOT_SUPPORTED;
        else
            JSSERROR(DMERR_NOT_SUPPORTED, DMERR_NOT_SUPPORTED,
                "Not a FT2 Extended Module (XM), header signature mismatch!\n");
    }

    if (xmH.unUsed1A != 0x1a)
    {
        if (!probe)
            JSSWARNING(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
                "Possibly modified or corrupted XM [%x]\n",
                xmH.unUsed1A);
    }

    if (xmH.norders > XM_MaxOrders)
    {
        if (!probe)
            JSSERROR(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
                "Number of orders %d > %d, possibly broken module.\n",
                xmH.norders, XM_MaxOrders);
    }

    if (xmH.norders == 0)
    {
        if (!probe)
            JSSWARNING(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
                "Number of orders was zero.\n");
    }

    if (xmH.npatterns > XM_MaxPatterns)
    {
        if (!probe)
            JSSERROR(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
                "Number of patterns %d > %d, possibly broken module.\n",
                xmH.npatterns, XM_MaxPatterns);
    }

    if (xmH.npatterns == 0)
    {
        if (!probe)
            JSSERROR(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
                "Number of patterns was zero.\n");
    }

    if (xmH.nchannels <= 0 || xmH.nchannels > XM_MaxChannels)
    {
        if (!probe)
            JSSERROR(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
                "Number of channels was invalid, %d (should be 1 - %d).\n",
                xmH.nchannels, XM_MaxChannels);
    }

    if (xmH.ninstruments <= 0 || xmH.ninstruments > XM_MaxInstruments)
    {
        if (!probe)
            JSSERROR(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
                "Number of instruments was invalid, %d (should be 1 - %d).\n",
                xmH.ninstruments, XM_MaxInstruments);
    }

    if (xmH.headSize < XM_HeaderSize)
    {
        if (probe)
            return DMERR_NOT_SUPPORTED;
        else
            JSSERROR(DMERR_NOT_SUPPORTED, DMERR_NOT_SUPPORTED,
                "XM header size less than %d bytes (%d bytes).\n",
                XM_HeaderSize, xmH.headSize);
    }

    switch (xmH.version)
    {
        case 0x0104:
        case 0x0102:
            break;

        default:
            if (!probe)
                JSSWARNING(DMERR_NOT_SUPPORTED, DMERR_NOT_SUPPORTED,
                    "Unsupported version of XM format 0x%04x.\n",
                    xmH.version);
    }

    if (!dmf_read_str(inFile, &xmH.orderList, sizeof(xmH.orderList)))
    {
        JSSERROR(DMERR_FREAD, DMERR_FREAD,
            "Error reading pattern order list.\n");
    }

    if (xmH.headSize > 276)
    {
        JSSWARNING(DMERR_NOT_SUPPORTED, DMERR_NOT_SUPPORTED,
            "XM header size > %d bytes, skipping the rest.\n",
            XM_HeaderSize, xmH.headSize);

        dmfseek(inFile, xmH.headSize - XM_HeaderSize, SEEK_CUR);
    }

    if (probe)
        return DMERR_OK;

    // Okay, allocate a module structure
    if ((*pmodule = module = jssAllocateModule()) == NULL)
    {
        JSSERROR(DMERR_MALLOC, DMERR_MALLOC,
            "Could not allocate memory for module structure.\n");
    }

    // Convert the module header data
    module->moduleType      = jmdtXM;
    module->intVersion      = xmH.version;
#ifndef JSS_LIGHT
    module->moduleName      = jssASCIItoStr(xmH.songName, 0, sizeof(xmH.songName));
    module->trackerName     = jssASCIItoStr(xmH.trackerName, 0, sizeof(xmH.trackerName));
#endif
    module->defSpeed        = xmH.defSpeed;
    module->defTempo        = xmH.defTempo;
    module->nextInstruments = xmH.ninstruments;
    module->ninstruments    = 0;
    module->npatterns       = xmH.npatterns;
    module->norders         = xmH.norders;
    module->nchannels       = xmH.nchannels;
    module->defFlags        = jmdfStereo | jmdfFT2Replay;
    module->defRestartPos   = xmH.defRestartPos;

    if ((xmH.flags & 1) == 0)
        module->defFlags |= jmdfAmigaPeriods;

    // Setup the default pannings
    for (index = 0; index < module->nchannels; index++)
        module->defPanning[index] = jchPanMiddle;

    // Load rest of the module
    switch (xmH.version)
    {
        case 0x0104:
            if ((ret = jssXMLoadPatterns(inFile, module, &xmH)) != DMERR_OK)
                goto out;

            if ((ret = jssXMLoadInstruments(inFile, module)) != DMERR_OK)
                goto out;
            break;

        case 0x0102:
            if ((ret = jssXMLoadInstruments(inFile, module)) != DMERR_OK)
                goto out;

            if ((ret = jssXMLoadPatterns(inFile, module, &xmH)) != DMERR_OK)
                goto out;

            // Read sample data if needed
            for (index = 0; index < module->nextInstruments; index++)
            {
                JSSExtInstrument *einst = module->extInstruments[index];
                if (einst != NULL && (ret = jssXMLoadInstrumentSamples(inFile, module, einst, index)) != DMERR_OK)
                    goto out;
            }
            break;
    }

out:
    return ret;
}