view tools/xm2jss.c @ 2482:7151597d8ec6

Cosmetic.
author Matti Hamalainen <ccr@tnsp.org>
date Tue, 28 Apr 2020 01:39:29 +0300
parents 90eec3e1f85f
children d56a0e86067a
line wrap: on
line source

/*
 * xm2jss - Convert XM module to JSSMOD
 * Programmed and designed by Matti 'ccr' Hamalainen
 * (C) Copyright 2006-2019 Tecnic Software productions (TNSP)
 *
 * Please read file 'COPYING' for information on license and distribution.
 */
#include "dmtool.h"
#include <stdio.h>
#include "jss.h"
#include "jssmod.h"
#include "jssplr.h"
#include "dmlib.h"
#include "dmargs.h"
#include "dmres.h"
#include "dmfile.h"
#include "dmmutex.h"


#define jmpNMODEffectTable (36)
static const char jmpMODEffectTable[jmpNMODEffectTable] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
#define JM_SAMPLE_MODE_MASK (jsampFlipSign | jsampSwapEndianess | jsampSplit | jsampDelta)


char  *optInFilename = NULL, *optOutFilename = NULL;
BOOL  optIgnoreErrors = FALSE,
      optStripExtInstr = FALSE,
      optStripInstr = FALSE,
      optStripSamples = FALSE,
      optOptimize = FALSE;

int   optPatternMode = PATMODE_COMP_HORIZ,
      optSampMode16 = jsampDelta,
      optSampMode8 = jsampFlipSign | jsampDelta;


static const char* patModeTable[PATMODE_LAST] =
{
    "Raw horizontal",
    "Compressed horizontal (similar to XM modules)",
    "Raw vertical",
    "Compressed vertical",
    "Raw horizontal for each element",
    "Raw vertical for each element",
};


static const DMOptArg optList[] =
{
    {  0, '?', "help"            , "Show this help", OPT_NONE },
    {  1,   0, "license"         , "Print out this program's license agreement", OPT_NONE },
    {  2, 'v', "verbose"         , "Be more verbose", OPT_NONE },

    { 10, 'i', "ignore"          , "Ignore errors", OPT_NONE },
    { 12, 'p', "patterns"        , "Pattern storage mode", OPT_ARGREQ },
    { 14, 'E', "strip-ext-instr" , "Strip ext. instruments (implies -I -S)", OPT_NONE },
    { 16, 'I', "strip-instr"     , "Strip instruments (implies -S)", OPT_NONE },
    { 18, 'S', "strip-samples"   , "Strip instr. sampledata", OPT_NONE },
    { 20, '8', "smode8"          , "8-bit sample conversion flags", OPT_ARGREQ },
    { 22, '1', "smode16"         , "16-bit sample conversion flags", OPT_ARGREQ },
    { 24, 'O', "optimize"        , "Optimize module", OPT_NONE },
};

const int optListN = sizeof(optList) / sizeof(optList[0]);


void argShowHelp()
{
    dmPrintBanner(stdout, dmProgName, "[options] <input.xm> <output.jmod>");
    dmArgsPrintHelp(stdout, optList, optListN, 0, 80 - 2);

    printf("\n"
    "Pattern storage modes:\n");

    for (int i = 1; i < PATMODE_LAST; i++)
        printf("  %d = %s\n", i, patModeTable[i-1]);

    printf(
    "\n"
    "Sample data conversion flags (summative):\n"
    "  1 = Delta encoding (DEF 8 & 16)\n"
    "  2 = Flip signedness (DEF 8)\n"
    "  4 = Swap endianess (affects 16-bit only)\n"
    "  8 = Split and de-interleave hi/lo bytes (affects 16-bit only)\n"
    "\n"
    );
}

BOOL argHandleOpt(const int optN, char *optArg, char *currArg)
{
    switch (optN)
    {
        case 0:
            argShowHelp();
            exit(0);
            break;

        case 1:
            dmPrintLicense(stdout);
            exit(0);
            break;

        case 2:
            dmVerbosity++;
            break;

        case 10:
            optIgnoreErrors = TRUE;
            break;

        case 12:
            optPatternMode = atoi(optArg);
            if (optPatternMode <= 0 || optPatternMode >= PATMODE_LAST)
            {
                dmErrorMsg("Unknown pattern conversion mode %d\n", optPatternMode);
                return FALSE;
            }
            break;

        case 14: optStripExtInstr = TRUE; break;
        case 16: optStripInstr = TRUE; break;
        case 18: optStripSamples = TRUE; break;

        case 20: optSampMode8 = atoi(optArg) & JM_SAMPLE_MODE_MASK; break;
        case 22: optSampMode16 = atoi(optArg) & JM_SAMPLE_MODE_MASK; break;

        case 24: optOptimize = TRUE; break;

        default:
            dmErrorMsg("Unimplemented option argument '%s'.\n", currArg);
            return FALSE;
    }

    return TRUE;
}


BOOL argHandleFile(char *currArg)
{
    // Was not option argument
    if (!optInFilename)
        optInFilename = currArg;
    else
    if (!optOutFilename)
        optOutFilename = currArg;
    else
    {
        dmErrorMsg("Too many filename arguments specified, '%s'.\n", currArg);
        return FALSE;
    }

    return TRUE;
}


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


/* These functions and the macro mess are meant to make the
 * conversion routines themselves clearer and simpler.
 */
BOOL jsPutByte(Uint8 *patBuf, size_t patBufSize, size_t *npatBuf, Uint8 val)
{
    if (*npatBuf >= patBufSize)
        return FALSE;
    else
    {
        patBuf[*npatBuf] = val;
        (*npatBuf)++;
        return TRUE;
    }
}

#define JSPUTBYTE(x) do { if (!jsPutByte(patBuf, patBufSize, patSize, x)) return DMERR_BOUNDS; } while (0)

#define JSCOMP(x,z) do { if ((x) != jsetNotSet) { qflags |= (z); qcomp++; } } while (0)

#define JSCOMPPUT(xf,xv,qv)  do {                       \
    if (qflags & (xf)) {                                \
        if ((xv) < 0 || (xv) > 255)                     \
            JSSERROR(DMERR_BOUNDS, DMERR_BOUNDS,        \
            "%s value out of bounds %d.\n", qv, (xv));  \
        JSPUTBYTE(xv);                                  \
    }                                                   \
} while (0)

#define JSCONVPUT(xv,qv)    do {                        \
    if ((xv) != jsetNotSet) {                           \
        if ((xv) < 0 || (xv) > 254)                     \
            JSSERROR(DMERR_BOUNDS, DMERR_BOUNDS,        \
            "%s value out of bounds %d.\n", qv, (xv));  \
        JSPUTBYTE((xv) + 1);                            \
    } else {                                            \
        JSPUTBYTE(0);                                   \
    }                                                   \
} while (0)


/* Convert a note
 */
static int jssDoConvertNote(
    Uint8 *patBuf, const size_t patBufSize,
    size_t *patSize, const JSSNote *pnote)
{
    Uint8 tmp;
    if (pnote->note == jsetNotSet)
        tmp = 0;
    else
    if (pnote->note == jsetNoteOff)
        tmp = 127;
    else
        tmp = pnote->note + 1;

    if (tmp > 0x7f)
        JSSERROR(DMERR_BOUNDS, DMERR_BOUNDS, "Note value out of bounds %d > 0x7f.\n", tmp);

    JSPUTBYTE(tmp & 0x7f);

    JSCONVPUT(pnote->instrument, "Instrument");
    JSCONVPUT(pnote->volume, "Volume");
    JSCONVPUT(pnote->effect, "Effect");

    tmp = (pnote->param != jsetNotSet) ? pnote->param : 0;
    JSPUTBYTE(tmp);

    return DMERR_OK;
}


/* Compress a note
 */
static int jssDoCompressNote(
    Uint8 *patBuf, const size_t patBufSize,
    size_t *patSize, const JSSNote *pnote)
{
    Uint8 qflags = 0;
    int qcomp = 0;

    // Determine what would get stored,
    // aka actually how much space we use
    JSCOMP(pnote->note,       JM_COMP_NOTE);
    JSCOMP(pnote->instrument, JM_COMP_INSTRUMENT);
    JSCOMP(pnote->volume,     JM_COMP_VOLUME);
    JSCOMP(pnote->effect,     JM_COMP_EFFECT);
    if (pnote->param != jsetNotSet && pnote->param != 0)
    {
        qflags |= JM_COMP_PARAM;
        qcomp++;
    }

    if (qcomp < 4)
    {
        // Okay, it's less than 4 bytes, so use compressed
        JSPUTBYTE(qflags | 0x80);

        if (pnote->note != jsetNotSet)
        {
            Uint8 tmp = (pnote->note != jsetNoteOff) ? pnote->note : 127;
            if (tmp > 0x7f)
                JSSERROR(DMERR_BOUNDS, DMERR_BOUNDS, "Note value out of bounds %d > 0x7f.\n", tmp);
            JSPUTBYTE(tmp);
        }

        JSCOMPPUT(JM_COMP_INSTRUMENT, pnote->instrument, "Instrument");
        JSCOMPPUT(JM_COMP_VOLUME, pnote->volume, "Volume");
        JSCOMPPUT(JM_COMP_EFFECT, pnote->effect, "Effect");
        JSCOMPPUT(JM_COMP_PARAM, pnote->param, "Param");

        return DMERR_OK;
    }
    else
    {
        // Was 4 bytes or more, just dump it all in ..
        return jssDoConvertNote(patBuf, patBufSize, patSize, pnote);
    }
}


static int jssCompressNote(
    Uint8 *patBuf, const size_t patBufSize,
    size_t *patSize, const JSSPattern *pattern,
    const int channel, const int row)
{
    const JSSNote *pnote = jssGetNotePtr(pattern, channel, row);
    if (pnote != NULL)
    {
        int res = jssDoCompressNote(patBuf, patBufSize, patSize, pnote);
        if (res != DMERR_OK)
        {
            JSSERROR(res, res, "Note compression failed [patBuf=%p, patBufSize=%d, patSize=%d, row=%d, chn=%d]\n",
            patBuf, patBufSize, *patSize, row, channel);
            return res;
        }
    }
    return DMERR_OK;
}


/* Compress pattern
 */
static int jssConvertPatternCompHoriz(
    Uint8 *patBuf, const size_t patBufSize,
    size_t *patSize, const JSSPattern *pattern)
{
    *patSize = 0;

    for (int row = 0; row < pattern->nrows; row++)
    for (int channel = 0; channel < pattern->nchannels; channel++)
    {
        int res = jssCompressNote(patBuf, patBufSize, patSize, pattern, channel, row);
        if (res != DMERR_OK)
            return res;
    }

    return DMERR_OK;
}


static int jssConvertPatternCompVert(
    Uint8 *patBuf, const size_t patBufSize,
    size_t *patSize, const JSSPattern *pattern)
{
    *patSize = 0;

    for (int channel = 0; channel < pattern->nchannels; channel++)
    for (int row = 0; row < pattern->nrows; row++)
    {
        int res = jssCompressNote(patBuf, patBufSize, patSize, pattern, channel, row);
        if (res != DMERR_OK)
            return res;
    }

    return DMERR_OK;
}


/* Convert a pattern
 */
static int jssConvertNote(
    Uint8 *patBuf, const size_t patBufSize,
    size_t *patSize, const JSSPattern *pattern,
    const int channel, const int row)
{
    const JSSNote *pnote = jssGetNotePtr(pattern, channel, row);
    if (pnote != NULL)
    {
        int res = jssDoConvertNote(patBuf, patBufSize, patSize, pnote);
        if (res != DMERR_OK)
        {
            JSSERROR(res, res, "Note conversion failed [patBuf=%p, patBufSize=%d, patSize=%d, row=%d, chn=%d]\n",
            patBuf, patBufSize, *patSize, row, channel);
            return res;
        }
    }
    return DMERR_OK;
}


static int jssConvertPatternRawHoriz(
    Uint8 *patBuf, const size_t patBufSize,
    size_t *patSize, const JSSPattern *pattern)
{
    *patSize = 0;

    for (int row = 0; row < pattern->nrows; row++)
    for (int channel = 0; channel < pattern->nchannels; channel++)
    {
        int res = jssConvertNote(patBuf, patBufSize, patSize, pattern, channel, row);
        if (res != DMERR_OK)
            return res;
    }

    return DMERR_OK;
}


static int jssConvertPatternRawVert(
    Uint8 *patBuf, const size_t patBufSize,
    size_t *patSize, const JSSPattern *pattern)
{
    *patSize = 0;

    for (int channel = 0; channel < pattern->nchannels; channel++)
    for (int row = 0; row < pattern->nrows; row++)
    {
        int res = jssConvertNote(patBuf, patBufSize, patSize, pattern, channel, row);
        if (res != DMERR_OK)
            return res;
    }

    return DMERR_OK;
}


#define JSFOREACHNOTE1                                         \
  for (channel = 0; channel < pattern->nchannels; channel++)   \
  for (row = 0; row < pattern->nrows; row++) {                 \
  const JSSNote *pnote = jssGetNotePtr(pattern, channel, row); \
  if (pnote != NULL) {

#define JSFOREACHNOTE2 } }


static int jssConvertPatternRawElemVert(
    Uint8 *patBuf, const size_t patBufSize,
    size_t *patSize, const JSSPattern *pattern)
{
    Uint8 tmp;
    int row, channel;
    *patSize = 0;

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

    if (tmp > 0x7f)
        JSSERROR(DMERR_BOUNDS, DMERR_BOUNDS, "Note value out of bounds %d > 0x7f.\n", tmp);

    JSPUTBYTE(tmp);
    JSFOREACHNOTE2;

    JSFOREACHNOTE1;
    JSCONVPUT(pnote->instrument, "Instrument");
    JSFOREACHNOTE2;

    JSFOREACHNOTE1;
    JSCONVPUT(pnote->volume, "Volume");
    JSFOREACHNOTE2;

    JSFOREACHNOTE1;
    JSCONVPUT(pnote->effect, "Effect");
    JSFOREACHNOTE2;

    JSFOREACHNOTE1;
    tmp = (pnote->param != jsetNotSet) ? pnote->param : 0;
    JSPUTBYTE(tmp);
    JSFOREACHNOTE2;

    return DMERR_OK;
}

#undef JSFOREACHNOTE1
#undef JSFOREACHNOTE2


#define JSFOREACHNOTE1                                         \
  for (row = 0; row < pattern->nrows; row++)                   \
  for (channel = 0; channel < pattern->nchannels; channel++) { \
  const JSSNote *pnote = jssGetNotePtr(pattern, channel, row); \
  if (pnote != NULL) {

#define JSFOREACHNOTE2 } }


static int jssConvertPatternRawElemHoriz(
    Uint8 *patBuf, const size_t patBufSize,
    size_t *patSize, const JSSPattern *pattern)
{
    Uint8 tmp;
    int row, channel;
    *patSize = 0;

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

    if (tmp > 0x7f)
        JSSERROR(DMERR_BOUNDS, DMERR_BOUNDS, "Note value out of bounds %d > 0x7f.\n", tmp);

    JSPUTBYTE(tmp);
    JSFOREACHNOTE2;

    JSFOREACHNOTE1;
    JSCONVPUT(pnote->instrument, "Instrument");
    JSFOREACHNOTE2;

    JSFOREACHNOTE1;
    JSCONVPUT(pnote->volume, "Volume");
    JSFOREACHNOTE2;

    JSFOREACHNOTE1;
    JSCONVPUT(pnote->effect, "Effect");
    JSFOREACHNOTE2;

    JSFOREACHNOTE1;
    tmp = (pnote->param != jsetNotSet) ? pnote->param : 0;
    JSPUTBYTE(tmp);
    JSFOREACHNOTE2;

    return DMERR_OK;
}

#undef JSFOREACHNOTE1
#undef JSFOREACHNOTE2


static BOOL jssMODWriteEnvelope(DMResource *outFile, const JSSEnvelope *env, const char *name, const int ninst)
{
    BOOL ok =
        dmf_write_byte(outFile, env->flags) &&
        dmf_write_byte(outFile, env->npoints) &&
        dmf_write_byte(outFile, env->sustain) &&
        dmf_write_byte(outFile, env->loopS) &&
        dmf_write_byte(outFile, env->loopE);

    for (int i = 0; ok && i < env->npoints; i++)
    {
        ok =
            dmf_write_le16(outFile, env->points[i].frame) &&
            dmf_write_le16(outFile, env->points[i].value);
    }

    if (!ok)
    {
        JSSERROR(DMERR_FWRITE, ok,
        "Failed to write JSSMOD %s-envelope for instrument #%d.\n",
        name, ninst);
    }

    return ok;
}


/* Save a JSSMOD file
 */
int jssSaveJSSMOD(DMResource *outFile, const JSSModule *module,
    const int patMode, const int flags8, const int flags16)
{
    JSSMODHeader jssH;
    const size_t patBufSize = 512*1024; // 256kB pattern buffer
    Uint8 *patBuf;
    size_t totalSize;
    int index, res;

    // Check the module
    if (module == NULL)
    {
        JSSERROR(DMERR_NULLPTR, DMERR_NULLPTR,
        "Module pointer was NULL\n");
    }

    if (module->nchannels < 1 || module->npatterns < 1 || module->norders < 1 ||
        module->nchannels > jsetMaxChannels ||
        module->npatterns > jsetMaxPatterns ||
        module->norders > jsetMaxOrders)
    {
        JSSERROR(DMERR_BOUNDS, DMERR_BOUNDS,
        "Module had invalid values (nchannels=%d, npatterns=%d, norders=%d)\n",
        module->nchannels, module->npatterns, module->norders);
    }

    // Create the JSSMOD header
    jssH.idMagic[0]         = 'J';
    jssH.idMagic[1]         = 'M';
    jssH.idVersion          = JSSMOD_VERSION;
    jssH.defFlags           = module->defFlags;
    jssH.intVersion         = module->intVersion;
    jssH.norders            = module->norders;
    jssH.npatterns          = module->npatterns;
    jssH.nextInstruments    = module->nextInstruments;
    jssH.ninstruments       = module->ninstruments;
    jssH.defRestartPos      = module->defRestartPos;

    jssH.nchannels          = module->nchannels;
    jssH.defSpeed           = module->defSpeed;
    jssH.defTempo           = module->defTempo;
    jssH.patMode            = patMode;

    // Write header
    if (!dmf_write_str(outFile, jssH.idMagic, sizeof(jssH.idMagic)) ||
        !dmf_write_byte(outFile, jssH.idVersion) ||

        !dmf_write_le16(outFile, jssH.defFlags) ||
        !dmf_write_le16(outFile, jssH.intVersion) ||
        !dmf_write_le16(outFile, jssH.norders) ||
        !dmf_write_le16(outFile, jssH.npatterns) ||
        !dmf_write_le16(outFile, jssH.nextInstruments) ||
        !dmf_write_le16(outFile, jssH.ninstruments) ||
        !dmf_write_le16(outFile, jssH.defRestartPos) ||

        !dmf_write_byte(outFile, jssH.nchannels) ||
        !dmf_write_byte(outFile, jssH.defSpeed) ||
        !dmf_write_byte(outFile, jssH.defTempo) ||
        !dmf_write_byte(outFile, jssH.patMode))
    {
        JSSERROR(DMERR_FWRITE, DMERR_FWRITE,
        "Error writing JSSMOD header!\n");
    }

    totalSize = sizeof(jssH);
    dmMsg(1," * JSSMOD-header 0x%04x, %" DM_PRIu_SIZE_T " bytes.\n",
        JSSMOD_VERSION, totalSize);

    // Write orders list
    for (totalSize = index = 0; index < module->norders; index++)
    {
        int tmp = module->orderList[index];
        if (tmp != jsetNotSet && tmp > module->npatterns)
        {
            JSSERROR(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
            "Orderlist entry #%d has invalid value %d.\n",
            index, tmp);
        }

        if (tmp == jsetNotSet)
            tmp = 0xffff;

        if (!dmf_write_le16(outFile, tmp))
            JSSERROR(DMERR_FWRITE, DMERR_FWRITE,
            "Could not write JSSMOD orders list entry #%d (%d).\n",
            index, tmp);

        totalSize += sizeof(Uint16);
    }

    dmMsg(1," * %d item orders list, %" DM_PRIu_SIZE_T " bytes.\n",
        module->norders, totalSize);

    // Allocate pattern compression buffer
    if ((patBuf = dmMalloc(patBufSize)) == NULL)
    {
        JSSERROR(DMERR_MALLOC, DMERR_MALLOC,
        "Error allocating memory for pattern compression buffer.\n");
    }

    // Convert and write patterns
    for (totalSize = index = 0; index < module->npatterns; index++)
    {
        JSSPattern *pattern = module->patterns[index];
        size_t dataSize = 0;
        int ret;

        if (pattern == NULL)
        {
            dmMsg(1,
            "Pattern #%d is NULL.\n", index);
            pattern = module->patterns[module->npatterns];
        }

        if (pattern->nrows > jsetMaxRows)
        {
            JSSERROR(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
            "Pattern #%d has %d rows > %d max.\n",
            index, pattern->nrows, jsetMaxRows);
        }

        switch (patMode)
        {
            case PATMODE_RAW_HORIZ:
                ret = jssConvertPatternRawHoriz(patBuf, patBufSize, &dataSize, pattern);
                break;
            case PATMODE_COMP_HORIZ:
                ret = jssConvertPatternCompHoriz(patBuf, patBufSize, &dataSize, pattern);
                break;
            case PATMODE_RAW_VERT:
                ret = jssConvertPatternRawVert(patBuf, patBufSize, &dataSize, pattern);
                break;
            case PATMODE_COMP_VERT:
                ret = jssConvertPatternCompVert(patBuf, patBufSize, &dataSize, pattern);
                break;
            case PATMODE_RAW_ELEM_HORIZ:
                ret = jssConvertPatternRawElemHoriz(patBuf, patBufSize, &dataSize, pattern);
                break;
            case PATMODE_RAW_ELEM_VERT:
                ret = jssConvertPatternRawElemVert(patBuf, patBufSize, &dataSize, pattern);
                break;
            default:
                JSSERROR(DMERR_INVALID_DATA, DMERR_INVALID_DATA,
                "Unsupported pattern conversion mode %d for pattern #%d.\n",
                patMode, index);
        }

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

        dmMsg(3, " - Pattern %d size %" DM_PRIu_SIZE_T " bytes\n",
            index, dataSize);

        totalSize += dataSize + sizeof(JSSMODPattern);

        if (!dmf_write_le32(outFile, dataSize) ||
            !dmf_write_le16(outFile, pattern->nrows) ||
            !dmf_write_le16(outFile, pattern->nmap))
        {
            JSSERROR(DMERR_FWRITE, DMERR_FWRITE,
            "Error writing JSSMOD pattern header #%d.\n",
            index);
        }

        if (pattern->nmap != pattern->nchannels)
        {
            if (!dmf_write_str(outFile, pattern->map,
                 sizeof(pattern->map[0]) * pattern->nmap))
            {
                JSSERROR(DMERR_FWRITE, DMERR_FWRITE,
                "Error writing JSSMOD channel map for pattern #%d.\n",
                index);
            }
        }

        if (!dmf_write_str(outFile, patBuf, dataSize))
        {
            JSSERROR(DMERR_FWRITE, DMERR_FWRITE,
            "Error writing JSSMOD pattern data #%d.\n",
            index);
        }
    }

    dmFree(patBuf);
    dmMsg(1," * %d patterns, %" DM_PRIu_SIZE_T " bytes.\n",
        module->npatterns, totalSize);

    // Write extended instruments
    for (totalSize = index = 0; index < module->nextInstruments; index++)
    {
        JSSExtInstrument *einst = module->extInstruments[index];
        JSSExtInstrument tmpEInst;

        if (einst == NULL)
        {
            einst = &tmpEInst;
            memset(&tmpEInst, 0, sizeof(tmpEInst));
            dmMsg(1,
            "Extended instrument #%d is NULL!\n",
            index);
        }

        // Misc data
        BOOL ok =
            dmf_write_byte(outFile, einst->nsamples) &&
            dmf_write_byte(outFile, einst->vibratoType) &&
            dmf_write_le16(outFile, einst->vibratoSweep) &&
            dmf_write_le16(outFile, einst->vibratoDepth) &&
            dmf_write_le16(outFile, einst->vibratoRate) &&
            dmf_write_le16(outFile, einst->fadeOut);

        // Sample number for note(s)
        for (int i = 0; ok && i < jsetNNotes; i++)
        {
            int snum = einst->sNumForNotes[i];
            Uint32 tmp = (snum != jsetNotSet) ? snum + 1 : 0;
            ok = dmf_write_le32(outFile, tmp);
        }

        // Envelopes
        if (!ok ||
            !jssMODWriteEnvelope(outFile, &einst->volumeEnv, "volume", index) ||
            !jssMODWriteEnvelope(outFile, &einst->panningEnv, "panning", index))
        {
            JSSERROR(DMERR_FWRITE, DMERR_FWRITE,
            "Error writing JSSMOD extended instrument #%d.\n",
            index);
        }

        totalSize += sizeof(JSSMODExtInstrument);
    }

    dmMsg(1," * %d Extended Instruments, %" DM_PRIu_SIZE_T " bytes.\n",
        module->nextInstruments, totalSize);

    // Write sample instrument headers
    for (totalSize = index = 0; index < module->ninstruments; index++)
    if (module->instruments[index] != NULL)
    {
        JSSInstrument *inst = module->instruments[index];

        // Determine conversion flags to use
        inst->convFlags = (inst->flags & jsf16bit) ? flags16 : flags8;
        if (inst->data != NULL)
            inst->convFlags |= jsampHasData;

        // Write instrument header
        if (!dmf_write_le32(outFile, inst->size) ||
            !dmf_write_le32(outFile, inst->loopS) ||
            !dmf_write_le32(outFile, inst->loopE) ||
            !dmf_write_le16(outFile, inst->flags) ||
            !dmf_write_le16(outFile, inst->C4BaseSpeed) ||
            !dmf_write_le16(outFile, inst->ERelNote) ||
            !dmf_write_le16(outFile, inst->EFineTune) ||
            !dmf_write_le16(outFile, inst->EPanning) ||
            !dmf_write_byte(outFile, inst->volume) ||
            !dmf_write_byte(outFile, inst->convFlags))
        {
            JSSERROR(DMERR_FWRITE, DMERR_FWRITE,
            "Error writing JSSMOD instrument #%d.\n",
            index);
        }

        totalSize += sizeof(JSSMODInstrument);
    }
    else
    {
        JSSWARNING(DMERR_NULLPTR, DMERR_NULLPTR,
        "Instrument #%d NULL!\n",
        index);
    }

    dmMsg(1," * %d Instrument headers, %" DM_PRIu_SIZE_T " bytes.\n",
        module->ninstruments, totalSize);

    // Write sample data
    for (totalSize = index = 0; index < module->ninstruments; index++)
    {
        JSSInstrument *inst = module->instruments[index];
        if (inst != NULL && inst->data != NULL)
        {
            size_t bsize = inst->size;
            if (inst->flags & jsf16bit)
            {
                bsize *= sizeof(Uint16);
                res = jssEncodeSample16(inst->data, inst->size, flags16);
            }
            else
            {
                bsize *= sizeof(Uint8);
                res = jssEncodeSample8(inst->data, inst->size, flags8);
            }

            if (res != DMERR_OK)
            {
                JSSERROR(res, res,
                "Error encoding sample for instrument #%d: %s\n",
                index, dmErrorStr(res));
            }

            if (!dmf_write_str(outFile, inst->data, bsize))
            {
                JSSERROR(DMERR_FWRITE, DMERR_FWRITE,
                "Error writing JSSMOD sample data for instrument #%d.\n",
                index);
            }

            totalSize += bsize;
        }
    }

    dmMsg(1," * %d samples, %" DM_PRIu_SIZE_T " bytes.\n",
        module->ninstruments, totalSize);

    return DMERR_OK;
}


/* Scan given pattern for used instruments and channels.
 * Also checks if the pattern is empty.
 */
BOOL jssScanPattern(const JSSModule *module, const JSSPattern *pattern,
    const int npattern, BOOL *usedExtInstruments, BOOL *usedChannels)
{
    JSSNote *n = pattern->data;
    BOOL empty = TRUE;

    // Check all notes in this pattern
    for (int row = 0; row < pattern->nrows; row++)
    for (int channel = 0; channel < pattern->nchannels; channel++, n++)
    {
        // Is the instrument set?
        if (usedExtInstruments != NULL &&
            n->instrument != jsetNotSet)
        {
            // Is it valid?
            if (n->instrument >= 0 && n->instrument < module->nextInstruments)
                usedExtInstruments[n->instrument] = TRUE;
            else
            {
                dmMsg(2, "Pattern 0x%x, row=0x%x, chn=%d has invalid instrument 0x%x\n",
                npattern + 1, row, channel, n->instrument + 1);
            }
        }

        // Check if this channel is used
        if (n->note != jsetNotSet ||
            n->instrument != jsetNotSet ||
            n->volume != jsetNotSet ||
            n->effect != jsetNotSet ||
            n->param != jsetNotSet)
        {
            if (usedChannels != NULL)
                usedChannels[channel] = TRUE;

            empty = FALSE;
        }
    }

    return empty;
}


/* Check if two given patterns are dupes
 */
BOOL jssComparePattern(const JSSPattern *pat1, const JSSPattern *pat2)
{
    return
        pat1->nrows     == pat2->nrows &&
        pat1->nchannels == pat2->nchannels &&
        memcmp(pat1->data, pat2->data, sizeof(JSSNote) * pat1->nrows * pat1->nchannels) == 0;
}


/* Optimize a given module.
 * NOTE! While this function creates and returns a new JSSModule,
 * it mostly copies references to data in the source module,
 * AND also modifies the source module - which is kinda "bad".
 * If/when freeing module data, you MUST only jssFreeModule()
 * the source one, and dmFree() the optimized result.
 *
 * This should be re-written to copy the data instead.
 */
JSSModule *jssOptimizeModule(JSSModule *src)
{
    BOOL usedPatterns[jsetMaxPatterns + 1],
         usedInstruments[jsetMaxInstruments + 1],
         usedExtInstruments[jsetMaxInstruments + 1];
    int  mapExtInstruments[jsetMaxInstruments + 1],
         mapInstruments[jsetMaxInstruments + 1],
         mapPatterns[jsetMaxPatterns + 1],
         dupPatterns[jsetMaxPatterns + 1];

    JSSModule *dst = NULL;
    int n8, n16, nunused, ndupes;

    // Allocate a new module
    if ((dst = jssAllocateModule()) == NULL)
        return NULL;

    // Copy things
    dst->moduleType       = src->moduleType;
    dst->moduleName       = src->moduleName;
    dst->trackerName      = src->trackerName;
    dst->defSpeed         = src->defSpeed;
    dst->defTempo         = src->defTempo;
    dst->defFlags         = src->defFlags;
    dst->defRestartPos    = src->defRestartPos;
    dst->intVersion       = src->intVersion;
    dst->nchannels        = src->nchannels;
    dst->norders          = src->norders;

    for (int i = 0; i < jsetNChannels; i++)
    {
        dst->defPanning[i] = src->defPanning[i];
    }

    // Initialize values
    for (int i = 0; i <= jsetMaxInstruments; i++)
    {
        usedExtInstruments[i] = FALSE;
        usedInstruments[i]    = FALSE;
        mapExtInstruments[i]  = jsetNotSet;
        mapInstruments[i]     = jsetNotSet;
    }

    for (int i = 0; i <= jsetMaxPatterns; i++)
    {
        usedPatterns[i] = FALSE;
        mapPatterns[i]  = jsetNotSet;
        dupPatterns[i]  = jsetNotSet;
    }

    //
    // Find out all actually used patterns and ext.instruments
    // by going through all patterns specified in the order list
    //
    dmMsg(1, "Scanning patterns for used instruments and channels...\n");
    for (int norder = 0; norder < src->norders; norder++)
    {
        int npat = src->orderList[norder];
        if (npat >= 0 && npat < src->npatterns)
        {
            JSSPattern *pattern = src->patterns[npat];
            if (pattern != NULL)
            {
                // Scan for used instruments etc
                BOOL empty = jssScanPattern(src, pattern, npat, usedExtInstruments, NULL);

                // Empty patterns with known number of rows are "removed"
                if (empty && pattern->nrows == jsetDefaultRows)
                {
                    src->orderList[norder] = jsetNotSet;
                    usedPatterns[npat] = FALSE;
                }
                else
                    usedPatterns[npat] = TRUE;
            }
            else
            {
                dmErrorMsg("Pattern 0x%x is used on order 0x%x, but has no data!\n",
                    npat, norder);

                // Fix it.
                src->orderList[norder] = jsetNotSet;
            }
        }
        else
        if (npat != jsetNotSet)
        {
            dmErrorMsg("Order 0x%x has invalid pattern number 0x%x, changing to empty!\n",
                norder, npat);

            // Fix it.
            src->orderList[norder] = jsetNotSet;
        }
    }

    //
    // Find used sample instruments
    //
    dmMsg(1, "Checking ext.instruments for used sample instruments...\n");
    for (int neinst = 0; neinst < jsetMaxInstruments; neinst++)
    if (usedExtInstruments[neinst] && src->extInstruments[neinst] != NULL)
    {
        JSSExtInstrument *eip = src->extInstruments[neinst];

        for (int note = 0; note < jsetNNotes; note++)
        if (eip->sNumForNotes[note] != jsetNotSet)
        {
            int q = eip->sNumForNotes[note];
            if (q >= 0 && q < src->ninstruments)
            {
                usedInstruments[q] = TRUE;
            }
            else
            {
                dmErrorMsg("Ext.instrument #%d sNumForNotes[%d] value out range (%d < %d).\n",
                    neinst + 1, note, src->ninstruments, q + 1);
            }
        }
    }

    //
    // Create pattern mappings
    //
    dmMsg(1, "Creating pattern remaps...\n");
    nunused = ndupes = 0;
    for (int pat1 = 0; pat1 <= jsetMaxPatterns; pat1++)
    if (usedPatterns[pat1])
    {
        // Sanity check patterns
        if (pat1 >= src->npatterns)
        {
            dmErrorMsg("Pattern 0x%x >= 0x%x, but used!\n", pat1, src->npatterns);
            continue;
        }

        if (src->patterns[pat1] == NULL)
        {
            dmErrorMsg("Pattern 0x%x used but is NULL.\n", pat1);
            continue;
        }

        // Check for previously marked dupes
        if (dupPatterns[pat1] != jsetNotSet)
        {
            mapPatterns[pat1] = dupPatterns[pat1];
            continue;
        }

        // Check for duplicate patterns of "pat1" and mark them as such
        for (int pat2 = 0; pat2 < src->npatterns; pat2++)
        if (pat1 != pat2 && src->patterns[pat2] != NULL &&
            dupPatterns[pat2] == jsetNotSet &&
            jssComparePattern(src->patterns[pat1], src->patterns[pat2]))
        {
            dmPrint(1, " * %d and %d are dupes.\n", pat1, pat2);
            dupPatterns[pat2] = pat1;
            ndupes++;
        }

        mapPatterns[pat1] = dst->npatterns;
        dst->patterns[dst->npatterns] = src->patterns[pat1];
        (dst->npatterns)++;
    }
    else
    if (src->patterns[pat1] != NULL)
        nunused++;

    dmMsg(1, "%d used patterns (%d unused, %d duplicates).\n",
        dst->npatterns, nunused, ndupes);

    //
    // Re-map instruments
    //
    dmMsg(1, "Creating sample instrument remaps...\n");
    nunused = n8 = n16 = 0;
    for (int ninst = 0; ninst < jsetMaxInstruments; ninst++)
    if (usedInstruments[ninst])
    {
        JSSInstrument *ip;

        // XXX TODO instrument stripping should be done here
        // if this ever gets rewritten to copy source data instead.
        if (optStripInstr)
            continue;

        if (ninst >= src->ninstruments)
        {
            dmErrorMsg("Instrument 0x%x >= 0x%x, but used!\n",
                ninst + 1, src->ninstruments);
            continue;
        }

        if ((ip = src->instruments[ninst]) == NULL)
        {
            dmErrorMsg("Instrument 0x%x used but is NULL.\n", ninst + 1);
            continue;
        }

        dmPrint(2, "%02x -> %02x : ", ninst + 1, dst->ninstruments + 1);

        mapInstruments[ninst] = dst->ninstruments;
        dst->instruments[dst->ninstruments] = ip;
        (dst->ninstruments)++;

        if (ip->flags & jsf16bit)
            n16++;
        else
            n8++;
    }
    else
    if (src->instruments[ninst] != NULL)
        nunused++;

    dmPrint(2, "\n");
    dmMsg(1, "Total of %d [16-bit] + %d [8-bit] samples = %d instruments (%d unused).\n",
        n16, n8, dst->ninstruments, nunused);

    //
    // Re-map ext.instruments
    //
    dmMsg(1, "Creating ext.instrument remaps...\n");
    nunused = 0;
    for (int neinst = 0; neinst < jsetMaxInstruments; neinst++)
    if (usedExtInstruments[neinst])
    {
        JSSExtInstrument *eip;

        // XXX TODO instrument stripping should be done here
        // if this ever gets rewritten to copy source data instead.
        if (optStripExtInstr)
            continue;

        if (neinst >= src->nextInstruments)
        {
            dmErrorMsg("Ext.instrument 0x%x >= 0x%x, but used!\n",
                neinst + 1, src->nextInstruments);
            continue;
        }

        if ((eip = src->extInstruments[neinst]) == NULL)
        {
            dmErrorMsg("Extended instrument 0x%x used but is NULL.\n", neinst + 1);
            continue;
        }

        dmPrint(2, "%02x -> %02x : ", neinst + 1, dst->nextInstruments + 1);

        mapExtInstruments[neinst] = dst->nextInstruments;
        dst->extInstruments[dst->nextInstruments] = eip;
        (dst->nextInstruments)++;

        // Re-map sNumForNotes table for this ext.instrument
        for (int note = 0; note < jsetNNotes; note++)
        {
            int q = eip->sNumForNotes[note];
            if (q != jsetNotSet)
            {
                int map;
                if (q >= 0 && q < jsetMaxInstruments)
                {
                    map = mapInstruments[q];
                }
                else
                {
                    map = jsetNotSet;
                    dmErrorMsg("Einst=%d, note=%d, sNumForNote=%d (%d max)\n",
                        neinst + 1, note, q + 1, dst->ninstruments);
                }

                dmPrint(3, "%02x.%02x ", q + 1, map + 1);
                eip->sNumForNotes[note] = map;
            }
        }
    }
    else
    if (src->extInstruments[neinst] != NULL)
        nunused++;

    dmPrint(2, "\n");
    dmMsg(1, "%d extended instruments (%d unused).\n",
        dst->nextInstruments, nunused);

    //
    // Remap pattern data with remapped instrument data
    //
    for (int npat = 0; npat < dst->npatterns; npat++)
    {
        JSSPattern *pat = dst->patterns[npat];
        JSSNote *note = pat->data;

        for (int row = 0; row < pat->nrows; row++)
        for (int channel = 0; channel < pat->nchannels; channel++, note++)
        {
            // If not stripping extended instruments, check for
            // the validity of the used instrument and remap
            if (!optStripExtInstr)
            {
                if (note->instrument >= 0 && note->instrument < jsetMaxInstruments)
                    note->instrument = mapExtInstruments[note->instrument];

                if (note->instrument != jsetNotSet &&
                    dst->extInstruments[note->instrument] == NULL)
                {
                    dmErrorMsg("Non-existing instrument used #%d, INTERNAL ERROR.\n",
                        note->instrument + 1);
                }
            }

            // Convert certain effects
            char effect;
            JMPGETEFFECT(effect, note->effect);
            switch (effect)
            {
                case 'C': // Cxx = Set volume
                    if (note->volume == jsetNotSet)
                    {
                        note->volume = note->param;
                        note->effect = jsetNotSet;
                        note->param = jsetNotSet;
                    }
                    break;
            }
        }
    }

    //
    // Remap orders list
    //
    dmMsg(1, "Remapping orders list.\n");
    nunused = 0;
    for (int nord = 0; nord < src->norders; nord++)
    {
        int map = mapPatterns[src->orderList[nord]];
        if (map != src->orderList[nord])
        {
            dmPrint(2, "%02x -> %02x : ", src->orderList[nord], map);
            nunused++;
        }
        dst->orderList[nord] = map;
    }
    if (nunused)
        dmPrint(2, "\n");

    //
    // Do final pass on patterns to remove unused channels
    //
    for (int npat = 0; npat < dst->npatterns; npat++)
    {
        JSSPattern *pat = dst->patterns[npat];

        jssScanPattern(dst, pat, npat, NULL, pat->used);

        pat->nmap = 0;
        for (int nchn = 0; nchn < dst->nchannels; nchn++)
        {
            if (pat->used[nchn])
                pat->map[pat->nmap++] = nchn;
        }

        if (pat->nmap != pat->nchannels)
        {
            dmMsg(2, "Pattern %d: %d/%d used channels (%d unused).\n",
                npat, pat->nchannels - pat->nmap, pat->nchannels, pat->nmap);
        }
    }

    return dst;
}


int main(int argc, char *argv[])
{
    DMResource *inFile = NULL, *outFile = NULL;
    JSSModule *src = NULL, *dst = NULL;
    int res = DMERR_OK;

    dmInitProg("xm2jss", "XM to JSSMOD converter", "0.8", NULL, NULL);
    dmVerbosity = 0;

    // Parse arguments
    if (!dmArgsProcess(argc, argv, optList, optListN,
        argHandleOpt, argHandleFile, OPTH_BAILOUT))
        exit(1);

    // Check arguments
    if (optInFilename == NULL || optOutFilename == NULL)
    {
        res = dmError(DMERR_INVALID_ARGS,
            "Input or output file not specified. Try --help.\n");
        goto out;
    }

    // Read the source file
    if ((res = dmf_open_stdio(optInFilename, "rb", &inFile)) != DMERR_OK)
    {
        dmErrorMsg("Error opening input file '%s', %d: %s\n",
            optInFilename, res, dmErrorStr(res));
        goto out;
    }

    // Initialize miniJSS
    jssInit();

    // Read file
    dmMsg(1, "Reading XM-format file ...\n");
    res = jssLoadXM(inFile, &src, FALSE);
    dmf_close(inFile);
    if (res != 0)
    {
        dmErrorMsg("Error while loading XM file (%d), ", res);
        if (optIgnoreErrors)
            fprintf(stderr, "ignoring. This may cause problems.\n");
        else
        {
            fprintf(stderr, "giving up. Use --ignore if you want to try to convert anyway.\n");
            goto out;
        }
    }

    // Check stripping settings
    if (optStripExtInstr) optStripInstr = TRUE;
    if (optStripInstr) optStripSamples = TRUE;

    // Remove samples
    if (optStripSamples)
    {
        dmMsg(1, "Stripping samples...\n");
        for (int i = 0; i < src->ninstruments; i++)
        {
            dmFree(src->instruments[i]->data);
            src->instruments[i]->data = NULL;
        }
    }

    // Remove instruments
    if (optStripInstr)
    {
        dmMsg(1, "Stripping instruments...\n");
        for (int i = 0; i < src->ninstruments; i++)
        {
            dmFree(src->instruments[i]);
            src->instruments[i] = NULL;
        }
        src->ninstruments = 0;
    }

    // Remove ext.instruments
    if (optStripExtInstr)
    {
        dmMsg(1, "Stripping ext.instruments...\n");
        for (int i = 0; i < src->nextInstruments; i++)
        {
            dmFree(src->extInstruments[i]);
            src->extInstruments[i] = NULL;
        }
        src->nextInstruments = 0;
    }

    // Run the optimization procedure
    if (optOptimize)
    {
        dmMsg(1, "Optimizing module data...\n");
        dst = jssOptimizeModule(src);
    }
    else
        dst = src;

    // Write output file
    if ((res = dmf_open_stdio(optOutFilename, "wb", &outFile)) != DMERR_OK)
    {
        dmErrorMsg("Error creating output file '%s': %s\n",
            optOutFilename, dmErrorStr(res));
        goto out;
    }

    dmMsg(1, "Writing JSSMOD-format file [patMode=0x%04x, samp8=0x%02x, samp16=0x%02x]\n",
        optPatternMode, optSampMode8, optSampMode16);

    res = jssSaveJSSMOD(outFile, dst, optPatternMode, optSampMode8, optSampMode16);

    dmf_close(outFile);

    if (res != 0)
    {
        dmErrorMsg(
            "Error while saving JSSMOD file: %s\n"
            "WARNING: The resulting file may be broken!\n",
            dmErrorStr(res));
    }
    else
    {
        dmMsg(1, "Conversion complete.\n");
    }

out:
    jssFreeModule(src);
    dmFree(dst);

    return res;
}