view minijss/jloadxm.c @ 2298:b5abfff07ca9

Add new DMGrowBuf helper functions dmGrowBufCopyOffsSize() and dmGrowBufConstCopyOffsSize().
author Matti Hamalainen <ccr@tnsp.org>
date Thu, 04 Jul 2019 10:54:16 +0300
parents b47109fce375
children e0f9200b94ad
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(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;
    int row, channel;
    assert(pattern != NULL);

    pnote = pattern->data;

    for (row = 0; row < pattern->nrows && size > 0; row++)
    for (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;
    int row, channel;
    assert(pattern != NULL);

    pnote = pattern->data;

    for (row = 0; row < pattern->nrows && size > 0; row++)
    for (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)
{
    int index, ret;
    XMPattern xmP;

    for (index = 0; index < module->npatterns; index++)
    {
        off_t remainder, pos = dmftell(inFile);
        Uint32 headSize;

        // 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
        {
            // 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:
                    ret = jssXMUnpackPattern104(inFile, xmP.size, module->patterns[index]);
                    break;
                case 0x0102:
                    ret = jssXMUnpackPattern102(inFile, xmP.size, module->patterns[index]);
                    break;
            }

            if (ret != 0)
                JSSERROR(ret, ret, "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[module->npatterns] = jssAllocatePattern(64, module->nchannels);

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

        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)
{
    int i;
    (void) name;
    (void) ninstr;

    // Convert envelope points
    for (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 (dmfread(inst->data, sizeof(Uint8), bsize, inFile) != 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;
}