view minijss/jloadjss.c @ 2278:40ccc09f09be

Implement empty channel removal in xm2jss and make JSSMOD format support channel remapping for this.
author Matti Hamalainen <ccr@tnsp.org>
date Tue, 18 Jun 2019 12:12:51 +0300
parents ca9fe688ab6b
children fc58f62f100c
line wrap: on
line source

/*
 * miniJSS - JSSMOD module loader
 * Programmed and designed by Matti 'ccr' Hamalainen
 * (C) Copyright 2007-2015 Tecnic Software productions (TNSP)
 */
#include "jssmod.h"


// If all pattern modes are required, enable them here
#ifdef JM_SUP_PATMODE_ALL
#    define JM_SUP_PATMODE_1 1
#    define JM_SUP_PATMODE_2 1
#    define JM_SUP_PATMODE_3 1
#    define JM_SUP_PATMODE_4 1
#    define JM_SUP_PATMODE_5 1
#endif


static inline JSSNote * jssGetNotePtr(JSSPattern *pattern, const int channel, const int row)
{
    return pattern->data + (pattern->nchannels * row) + pattern->map[channel];
}


// Short helper macros for reading data
#define JSGETBYTE(XV) \
    if (!dmf_read_byte(inFile, XV)) \
        return DMERR_OUT_OF_DATA


static int jssDoGetConvertedNote(DMResource *inFile, JSSNote *pnote, const Uint8 note)
{
    Uint8 tmp;

    if (note == 127)
        pnote->note = jsetNoteOff;
    else
    if (note == 0)
        pnote->note = jsetNotSet;
    else
        pnote->note = note - 1;

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

    JSGETBYTE(&tmp);
    pnote->volume = (tmp > 0) ? tmp - 1 : jsetNotSet;

    JSGETBYTE(&tmp);
    pnote->effect = (tmp > 0) ? tmp - 1 : jsetNotSet;

    JSGETBYTE(&tmp);
    pnote->param = (tmp == 0 && pnote->effect == jsetNotSet) ? jsetNotSet : tmp;

    return DMERR_OK;
}


static inline int jssGetConvertedNote(DMResource *inFile,
    JSSPattern *pattern, const int channel, const int row)
{
    Uint8 tmp;
    int res;

    JSGETBYTE(&tmp);
    if ((res = jssDoGetConvertedNote(inFile,
        jssGetNotePtr(pattern, channel, row), tmp)) != DMERR_OK)
        JSSERROR(res, res, "Error converting note on row=%d, chn=%d\n",
        row, channel);

    return res;
}


#if defined(JM_SUP_PATMODE_2) || defined(JM_SUP_PATMODE_4)
static int jssGetCompressedNoteDo(DMResource *inFile,
    JSSPattern *pattern, const int channel, const int row)
{
    JSSNote *pnote = jssGetNotePtr(pattern, channel, row);
    Uint8 packb, tmp;
    int res = DMERR_OK;

    JSGETBYTE(&packb);
    if (packb & 0x80)
    {
        if (packb & JM_COMP_NOTE)
        {
            JSGETBYTE(&tmp);
            if (tmp == 127)
                pnote->note = jsetNoteOff;
            else
                pnote->note = tmp;
        }

        if (packb & JM_COMP_INSTRUMENT)
        {
            JSGETBYTE(&tmp);
            pnote->instrument = tmp;
        }

        if (packb & JM_COMP_VOLUME)
        {
            JSGETBYTE(&tmp);
            pnote->volume = tmp;
        }

        if (packb & JM_COMP_EFFECT)
        {
            JSGETBYTE(&tmp);
            pnote->effect = tmp;
            pnote->param = 0;
        }

        if (packb & JM_COMP_PARAM)
        {
            JSGETBYTE(&tmp);
            pnote->param = tmp;
        }
    }
    else
    {
        res = jssDoGetConvertedNote(inFile, pnote, packb);
    }

    return res;
}


static int jssGetCompressedNote(DMResource *inFile,
    JSSPattern *pattern, const int channel, const int row)
{
    int res = jssGetCompressedNoteDo(inFile, pattern, channel, row);

    if (res != DMERR_OK)
        JSSERROR(res, res, "Error uncompressing note on row=%d, chn=%d\n",
        row, channel);

    return res;
}
#endif


#ifdef JM_SUP_PATMODE_2
static int jssGetPatternCompHoriz(DMResource *inFile, JSSPattern *pattern)
{
    for (int row = 0; row < pattern->nrows; row++)
    for (int channel = 0; channel < pattern->nmap; channel++)
    {
        int res = jssGetCompressedNote(inFile, pattern, channel, row);
        if (res != DMERR_OK)
            return res;
    }

    return DMERR_OK;
}
#endif


#ifdef JM_SUP_PATMODE_4
static int jssGetPatternCompVert(DMResource *inFile, JSSPattern *pattern)
{
    for (int channel = 0; channel < pattern->nmap; channel++)
    for (int row = 0; row < pattern->nrows; row++)
    {
        int res = jssGetCompressedNote(inFile, pattern, channel, row);
        if (res != DMERR_OK)
            return res;
    }

    return DMERR_OK;
}
#endif


#ifdef JM_SUP_PATMODE_1
static int jssGetPatternRawHoriz(DMResource *inFile, JSSPattern *pattern)
{
    for (int row = 0; row < pattern->nrows; row++)
    for (int channel = 0; channel < pattern->nmap; channel++)
    {
        int res = jssGetConvertedNote(inFile, pattern, channel, row);
        if (res != DMERR_OK)
            return res;
    }

    return DMERR_OK;
}
#endif


#ifdef JM_SUP_PATMODE_3
static int jssGetPatternRawVert(DMResource *inFile, JSSPattern *pattern)
{
    for (int channel = 0; channel < pattern->nmap; channel++)
    for (int row = 0; row < pattern->nrows; row++)
    {
        int res = jssGetConvertedNote(inFile, pattern, channel, row);
        if (res != DMERR_OK)
            return res;
    }

    return DMERR_OK;
}
#endif


#ifdef JM_SUP_PATMODE_5

#undef JSGETBYTE
#define JSGETBYTE(XV) if (!dmf_read_byte(inFile, XV)) return DMERR_OUT_OF_DATA

#define JSFOREACHNOTE1 \
    for (int channel = 0; channel < pattern->nmap; channel++) \
    for (int row = 0; row < pattern->nrows; row++) { \
        JSSNote *pnote = pattern->data + (pattern->nchannels * row) + pattern->map[channel];

#define JSFOREACHNOTE2 }


static int jssGetPatternRawVertElem(DMResource *inFile, JSSPattern *pattern)
{
    Uint8 tmp;

    JSFOREACHNOTE1
    JSGETBYTE(&tmp);
    if (tmp == 0)
        pnote->note = jsetNotSet;
    else if (tmp == 127)
        pnote->note = jsetNoteOff;
    else
        pnote->note = tmp - 1;
    JSFOREACHNOTE2

    JSFOREACHNOTE1
    JSGETBYTE(&tmp);
    pnote->instrument = (tmp > 0) ? tmp - 1 : jsetNotSet;
    JSFOREACHNOTE2

    JSFOREACHNOTE1
    JSGETBYTE(&tmp);
    pnote->volume = (tmp > 0) ? tmp - 1 : jsetNotSet;
    JSFOREACHNOTE2

    JSFOREACHNOTE1
    JSGETBYTE(&tmp);
    pnote->effect = (tmp > 0) ? tmp - 1 : jsetNotSet;
    JSFOREACHNOTE2

    JSFOREACHNOTE1
    JSGETBYTE(&tmp);
    pnote->param = (tmp == 0 && pnote->effect == jsetNotSet) ? jsetNotSet : tmp;
    JSFOREACHNOTE2

    return DMERR_OK;
}
#endif


#ifdef JM_SUP_EXT_INSTR
static int jssMODLoadEnvelope(DMResource *inFile,
    JSSEnvelope *env, const char *name, const int ninst)
{
    JSSMODEnvelope jssEnv;

    (void) name;
    (void) ninst;

    // Read envelope data
    if (!dmf_read_byte(inFile, &jssEnv.flags) ||
        !dmf_read_byte(inFile, &jssEnv.npoints) ||
        !dmf_read_byte(inFile, &jssEnv.sustain) ||
        !dmf_read_byte(inFile, &jssEnv.loopS) ||
        !dmf_read_byte(inFile, &jssEnv.loopE))
    {
        JSSERROR(DMERR_FREAD, DMERR_FREAD,
        "Failed to read %s-envelope data for instrument #%d.\n",
        name, ninst);
    }

    // Do some sanity checking
    if (jssEnv.npoints > jsetMaxEnvPoints ||
        jssEnv.loopS > jssEnv.loopE ||
        jssEnv.loopS > jssEnv.npoints ||
        jssEnv.loopE > jssEnv.npoints)
    {
        JSSERROR(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
        "Invalid values in %s-envelope for instrument #%d.\n",
        name, ninst);
    }

    // Copy data
    env->flags   = jssEnv.flags;
    env->npoints = jssEnv.npoints;
    env->sustain = jssEnv.sustain;
    env->loopS   = jssEnv.loopS;
    env->loopE   = jssEnv.loopE;

    for (int i = 0; i < jssEnv.npoints; i++)
    {
        Uint16 frame, value;
        if (!dmf_read_le16(inFile, &frame) ||
            !dmf_read_le16(inFile, &value))
        {
            JSSERROR(DMERR_FREAD, DMERR_FREAD,
            "Failed to read %s-envelope values (%d) for instrument #%d.\n",
            name, i, ninst);
        }

        env->points[i].frame = frame;
        env->points[i].value = value;
    }

    return DMERR_OK;
}
#endif


int jssLoadJSSMOD(DMResource *inFile, JSSModule **pmodule, BOOL probe)
{
    JSSModule *module;
    JSSMODHeader jssH;
    int index, ret = DMERR_OK;

    *pmodule = NULL;

    // Check the JSSMOD header
    if (!dmf_read_str(inFile, &jssH.idMagic, sizeof(jssH.idMagic)) ||
        !dmf_read_byte(inFile, &jssH.idVersion))
        JSSERROR(DMERR_FREAD, DMERR_FREAD,
        "Failed to read JSSMOD header #1.\n");

    if (memcmp(jssH.idMagic, "JM", 2) != 0)
    {
        if (probe)
            return DMERR_NOT_SUPPORTED;
        else
        {
            JSSERROR(DMERR_NOT_SUPPORTED, DMERR_NOT_SUPPORTED,
            "Not a JSSMOD file, header signature mismatch!\n");
        }
    }

    if (jssH.idVersion != JSSMOD_VERSION)
    {
        if (probe)
            return DMERR_VERSION;
        else
        {
            JSSERROR(DMERR_VERSION, DMERR_VERSION,
            "Unsupported version of JSSMOD 0x%2x, this version only supports 0x%2x!\n",
            jssH.idVersion, JSSMOD_VERSION);
        }
    }

    // Read rest of the header
    if (!dmf_read_le16(inFile, &jssH.defFlags) ||
        !dmf_read_le16(inFile, &jssH.intVersion) ||
        !dmf_read_le16(inFile, &jssH.norders) ||
        !dmf_read_le16(inFile, &jssH.npatterns) ||
        !dmf_read_le16(inFile, &jssH.nextInstruments) ||
        !dmf_read_le16(inFile, &jssH.ninstruments) ||
        !dmf_read_le16(inFile, &jssH.defRestartPos) ||

        !dmf_read_byte(inFile, &jssH.nchannels) ||
        !dmf_read_byte(inFile, &jssH.defSpeed) ||
        !dmf_read_byte(inFile, &jssH.defTempo) ||
        !dmf_read_byte(inFile, &jssH.patMode))
    {
        JSSERROR(DMERR_FREAD, DMERR_FREAD,
        "Failed to read JSSMOD header #2.\n");
    }

    if (probe)
        return DMERR_OK;

    // Check some of the things for sanity
    if (jssH.nchannels > jsetMaxChannels ||
        jssH.nchannels == 0 ||
        jssH.norders > jsetMaxOrders ||
        jssH.npatterns > jsetMaxPatterns ||
        jssH.nextInstruments > jsetMaxInstruments ||
        jssH.ninstruments > jsetMaxInstruments)
    {
        JSSERROR(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
        "Invalid values in JSSMOD header.\n");
    }

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

    // Copy header information
    module->defFlags        = jssH.defFlags;
    module->intVersion      = jssH.intVersion;

    module->norders         = jssH.norders;
    module->npatterns       = jssH.npatterns;
    module->nchannels       = jssH.nchannels;
    module->nextInstruments = jssH.nextInstruments;
    module->ninstruments    = jssH.ninstruments;
    module->defRestartPos   = jssH.defRestartPos;
    module->defSpeed        = jssH.defSpeed;
    module->defTempo        = jssH.defTempo;

    // Get the orders list
    for (index = 0; index < module->norders; index++)
    {
        Uint16 tmp;
        if (!dmf_read_le16(inFile, &tmp))
            JSSERROR(DMERR_FREAD, DMERR_FREAD,
            "Failed to read orders list entry #%d.\n",
            index);

        if (tmp != 0xffff && tmp > jssH.npatterns)
            JSSERROR(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
            "Invalid orders list entry #%d value %d > %d.\n",
            index, tmp, jssH.npatterns);

        module->orderList[index] = (tmp == 0xffff) ? jsetNotSet : tmp;
    }

    // Parse the patterns
    for (index = 0; index < module->npatterns; index++)
    {
        JSSPattern *pattern;
        JSSMODPattern jssP;

        // Read pattern header
        if (!dmf_read_le32(inFile, &jssP.size) ||
            !dmf_read_le16(inFile, &jssP.nrows) ||
            !dmf_read_le16(inFile, &jssP.nchannels))
            JSSERROR(DMERR_FREAD, DMERR_FREAD,
            "Failed to read JSSMOD pattern header #%d.\n",
            index);

        // Validate
        if (jssP.nrows == 0 || jssP.nrows > jsetMaxRows)
            JSSERROR(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
            "Invalid number of rows in pattern #%d: %d.\n",
            index, jssP.nrows);

        if (jssP.nchannels == 0 || jssP.nchannels > module->nchannels)
            JSSERROR(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
            "Invalid number of channels in pattern #%d: %d.\n",
            index, jssP.nchannels);

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

        // Read channel mappings, if any
        pattern->nmap = jssP.nchannels;
        if (jssP.nchannels != module->nchannels)
        {
            if (!dmf_read_str(inFile, pattern->map,
                sizeof(pattern->map[0]) * jssP.nchannels))
                JSSERROR(DMERR_FREAD, DMERR_FREAD,
                "Failed to read JSSMOD channel mapping data for pattern #%d.\n",
                index);

            // Check mapping
            for (int nch = 0; nch < jssP.nchannels; nch++)
            {
                if (pattern->map[nch] >= module->nchannels)
                {
                    JSSERROR(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
                    "Invalid channel mapping in pattern #%d: chn %d -> %d.\n",
                    index, nch, pattern->map[nch]);
                }
            }
        }

        // Get pattern data
        switch (jssH.patMode)
        {
#ifdef JM_SUP_PATMODE_1
            case PATMODE_RAW_HORIZ:
                ret = jssGetPatternRawHoriz(inFile, pattern);
                break;
#endif
#ifdef JM_SUP_PATMODE_2
            case PATMODE_COMP_HORIZ:
                ret = jssGetPatternCompHoriz(inFile, pattern);
                break;
#endif
#ifdef JM_SUP_PATMODE_3
            case PATMODE_RAW_VERT:
                ret = jssGetPatternRawVert(inFile, pattern);
                break;
#endif
#ifdef JM_SUP_PATMODE_4
            case PATMODE_COMP_VERT:
                ret = jssGetPatternCompVert(inFile, pattern);
                break;
#endif
#ifdef JM_SUP_PATMODE_5
            case PATMODE_RAW_ELEM:
                ret = jssGetPatternRawVertElem(inFile, pattern);
                break;
#endif
            default:
                JSSERROR(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
                "Unsupported pattern mode %d. Check compilation options.",
                jssH.patMode);
                break;
        }

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

#ifdef JM_SUP_EXT_INSTR
    // Read extended instruments
    for (index = 0; index < module->nextInstruments; index++)
    {
        JSSMODExtInstrument jssE;
        JSSExtInstrument *einst;
        int i;

        // Read header data
        if (!dmf_read_byte(inFile, &jssE.nsamples) ||
            !dmf_read_byte(inFile, &jssE.vibratoType) ||
            !dmf_read_le16(inFile, &jssE.vibratoSweep) ||
            !dmf_read_le16(inFile, &jssE.vibratoDepth) ||
            !dmf_read_le16(inFile, &jssE.vibratoRate) ||
            !dmf_read_le16(inFile, &jssE.fadeOut))
        {
            JSSERROR(DMERR_FREAD, DMERR_FREAD,
            "Failed to read ext.instrument #%d header.\n",
            index);
        }

        // Allocate instrument
        einst = module->extInstruments[index] = jssAllocateExtInstrument();
        if (einst == NULL)
        {
            JSSERROR(DMERR_MALLOC, DMERR_MALLOC,
            "Could not allocate extended instrument structure #%d\n", index);
        }

        einst->nsamples     = jssE.nsamples;
        einst->vibratoType  = jssE.vibratoType;
        einst->vibratoSweep = jssE.vibratoSweep;
        einst->vibratoDepth = jssE.vibratoDepth;
        einst->vibratoRate  = jssE.vibratoRate;
        einst->fadeOut      = jssE.fadeOut;

        // Read and somewhat validate sNumForNotes
        for (i = 0; i < jsetNNotes; i++)
        {
            int snum;
            Uint32 tmp;

            if (!dmf_read_le32(inFile, &tmp))
            {
                JSSERROR(DMERR_FREAD, DMERR_FREAD,
                "Failed to read ext.instrument #%d sNumForNotes[%d].\n",
                index, i);
            }

            einst->sNumForNotes[i] = snum = (tmp > 0) ? ((int) tmp - 1) : jsetNotSet;

            if (snum != jsetNotSet && snum > module->ninstruments)
            {
                JSSERROR(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
                "Ext.instrument #%d has invalid sNumForNotes[%d] = %d > %d max.\n",
                index, i, snum, module->ninstruments);
            }
        }

        // Read and validate envelopes
        if ((ret = jssMODLoadEnvelope(inFile, &einst->volumeEnv, "volume", index)) != DMERR_OK ||
            (ret = jssMODLoadEnvelope(inFile, &einst->panningEnv, "panning", index)) != DMERR_OK)
            return ret;
    }

#ifdef JM_SUP_INSTR
    // Read sample instrument headers
    for (index = 0; index < module->ninstruments; index++)
    {
        JSSMODInstrument jssI;
        JSSInstrument *inst;

        // Read header data
        if (!dmf_read_le32(inFile, &jssI.size) ||
            !dmf_read_le32(inFile, &jssI.loopS) ||
            !dmf_read_le32(inFile, &jssI.loopE) ||
            !dmf_read_le16(inFile, &jssI.flags) ||
            !dmf_read_le16(inFile, &jssI.C4BaseSpeed) ||
            !dmf_read_le16(inFile, (Uint16 *) &jssI.ERelNote) ||
            !dmf_read_le16(inFile, (Uint16 *) &jssI.EFineTune) ||
            !dmf_read_le16(inFile, (Uint16 *) &jssI.EPanning) ||
            !dmf_read_byte(inFile, &jssI.volume) ||
            !dmf_read_byte(inFile, &jssI.convFlags))
        {
            JSSERROR(DMERR_FREAD, DMERR_FREAD,
            "Failed to read sample instrument #%d header.\n",
            index);
        }

        // Validate what we can
        if (jssI.loopS > jssI.size ||
            jssI.loopE > jssI.size ||
            jssI.loopS > jssI.loopE)
        {
            JSSERROR(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
            "Invalid or corrupted sample instrument #%d.\n",
            index);
        }

        // Allocate instrument
        inst = module->instruments[index] = jssAllocateInstrument();
        if (inst == NULL)
        {
            JSSERROR(DMERR_MALLOC, DMERR_MALLOC,
            "Could not allocate sample instrument structure #%d\n",
            index);
        }

        // Copy data
        inst->size          = jssI.size;
        inst->loopS         = jssI.loopS;
        inst->loopE         = jssI.loopE;
        inst->flags         = jssI.flags;
        inst->C4BaseSpeed   = jssI.C4BaseSpeed;
        inst->ERelNote      = jssI.ERelNote;
        inst->EFineTune     = jssI.EFineTune;
        inst->EPanning      = jssI.EPanning;
        inst->volume        = jssI.volume;
        inst->convFlags     = jssI.convFlags;
    }

#ifdef JM_SUP_SAMPLES
    // Read sample data
    for (index = 0; index < module->ninstruments; index++)
    {
        JSSInstrument *inst = module->instruments[index];
        if (inst != NULL && (inst->convFlags & jsampHasData) != 0)
        {
            int ret;
            size_t bsize = (inst->flags & jsf16bit) ? sizeof(Uint16) : sizeof(Uint8);
            bsize *= inst->size;

            // Allocate sample data memory
            if ((inst->data = dmMalloc(bsize)) == NULL)
            {
                JSSERROR(DMERR_MALLOC, DMERR_MALLOC,
                "Could not allocate %d bytes of sample data #%d\n",
                bsize, index);
            }

            // Read data
            if (!dmf_read_str(inFile, inst->data, bsize))
            {
                JSSERROR(DMERR_FREAD, DMERR_FREAD,
                "Could not read %d bytes of sample data for #%d\n",
                bsize, index);
            }

            // Convert, if needed
            if (inst->flags & jsf16bit)
                ret = jssDecodeSample16(inst->data, inst->size, inst->convFlags);
            else
                ret = jssDecodeSample8(inst->data, inst->size, inst->convFlags);

            if (ret != DMERR_OK)
            {
                JSSERROR(ret, ret,
                "Failed to decode sample data for #%d\n", index);
            }
        }
    }
#else
#    warning Not including JSSMOD sample loading!
#endif  // JM_SUP_SAMPLES
#else
#    warning Not including JSSMOD instrument loading!
#endif  // JM_SUP_INSTR
#else
#    warning Not including JSSMOD ext.instrument loading!
#endif  // JM_SUP_EXT_INSTR

    return DMERR_OK;
}