Mercurial > hg > dmlib
view tools/xm2jss.c @ 2444:fd31b5d1ed5e
Add few comments.
author | Matti Hamalainen <ccr@tnsp.org> |
---|---|
date | Mon, 09 Mar 2020 22:29:29 +0200 |
parents | bc05bcfc4598 |
children | f5848606d5ad |
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(FILE *outFile, JSSEnvelope *env, const char *name, const int ninst) { BOOL ok = dm_fwrite_byte(outFile, env->flags) && dm_fwrite_byte(outFile, env->npoints) && dm_fwrite_byte(outFile, env->sustain) && dm_fwrite_byte(outFile, env->loopS) && dm_fwrite_byte(outFile, env->loopE); for (int i = 0; ok && i < env->npoints; i++) { ok = dm_fwrite_le16(outFile, env->points[i].frame) && dm_fwrite_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(FILE *outFile, JSSModule *module, int patMode, int flags8, 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 (!dm_fwrite_str(outFile, jssH.idMagic, sizeof(jssH.idMagic)) || !dm_fwrite_byte(outFile, jssH.idVersion) || !dm_fwrite_le16(outFile, jssH.defFlags) || !dm_fwrite_le16(outFile, jssH.intVersion) || !dm_fwrite_le16(outFile, jssH.norders) || !dm_fwrite_le16(outFile, jssH.npatterns) || !dm_fwrite_le16(outFile, jssH.nextInstruments) || !dm_fwrite_le16(outFile, jssH.ninstruments) || !dm_fwrite_le16(outFile, jssH.defRestartPos) || !dm_fwrite_byte(outFile, jssH.nchannels) || !dm_fwrite_byte(outFile, jssH.defSpeed) || !dm_fwrite_byte(outFile, jssH.defTempo) || !dm_fwrite_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 (!dm_fwrite_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 (!dm_fwrite_le32(outFile, dataSize) || !dm_fwrite_le16(outFile, pattern->nrows) || !dm_fwrite_le16(outFile, pattern->nmap)) { JSSERROR(DMERR_FWRITE, DMERR_FWRITE, "Error writing JSSMOD pattern header #%d.\n", index); } if (pattern->nmap != pattern->nchannels) { if (!dm_fwrite_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 (!dm_fwrite_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 = dm_fwrite_byte(outFile, einst->nsamples) && dm_fwrite_byte(outFile, einst->vibratoType) && dm_fwrite_le16(outFile, einst->vibratoSweep) && dm_fwrite_le16(outFile, einst->vibratoDepth) && dm_fwrite_le16(outFile, einst->vibratoRate) && dm_fwrite_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 = dm_fwrite_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 (!dm_fwrite_le32(outFile, inst->size) || !dm_fwrite_le32(outFile, inst->loopS) || !dm_fwrite_le32(outFile, inst->loopE) || !dm_fwrite_le16(outFile, inst->flags) || !dm_fwrite_le16(outFile, inst->C4BaseSpeed) || !dm_fwrite_le16(outFile, inst->ERelNote) || !dm_fwrite_le16(outFile, inst->EFineTune) || !dm_fwrite_le16(outFile, inst->EPanning) || !dm_fwrite_byte(outFile, inst->volume) || !dm_fwrite_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 (!dm_fwrite_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 */ JSSModule *jssOptimizeModule(JSSModule *m) { BOOL usedPatterns[jsetMaxPatterns + 1], usedInstruments[jsetMaxInstruments + 1], usedExtInstruments[jsetMaxInstruments + 1]; int mapExtInstruments[jsetMaxInstruments + 1], mapInstruments[jsetMaxInstruments + 1], mapPatterns[jsetMaxPatterns + 1], dupPatterns[jsetMaxPatterns + 1]; JSSModule *r = NULL; int n8, n16, nunused, ndupes; // Allocate a new module if ((r = jssAllocateModule()) == NULL) return NULL; // Copy things r->moduleType = m->moduleType; r->moduleName = dm_strdup(m->moduleName); r->trackerName = dm_strdup(m->trackerName); r->defSpeed = m->defSpeed; r->defTempo = m->defTempo; r->defFlags = m->defFlags; r->defRestartPos = m->defRestartPos; r->intVersion = m->intVersion; r->nchannels = m->nchannels; r->norders = m->norders; for (int i = 0; i < jsetNChannels; i++) { r->defPanning[i] = m->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 < m->norders; norder++) { int npat = m->orderList[norder]; if (npat >= 0 && npat < m->npatterns) { JSSPattern *pattern = m->patterns[npat]; if (pattern != NULL) { // Scan for used instruments etc BOOL empty = jssScanPattern(m, pattern, npat, usedExtInstruments, NULL); // Empty patterns with known number of rows are "removed" if (empty && pattern->nrows == jsetDefaultRows) { m->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. m->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. m->orderList[norder] = jsetNotSet; } } // // Find used sample instruments // dmMsg(1, "Checking ext.instruments for used sample instruments...\n"); for (int i = 0; i < jsetMaxInstruments; i++) if (usedExtInstruments[i] && m->extInstruments[i] != NULL) { JSSExtInstrument *e = m->extInstruments[i]; for (int note = 0; note < jsetNNotes; note++) if (e->sNumForNotes[note] != jsetNotSet) { int q = e->sNumForNotes[note]; if (q >= 0 && q < m->ninstruments) { usedInstruments[q] = TRUE; } else { dmErrorMsg("Ext.instrument #%d sNumForNotes[%d] value out range (%d < %d).\n", i + 1, note, m->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 >= m->npatterns) { dmErrorMsg("Pattern 0x%x >= 0x%x, but used!\n", pat1, m->npatterns); continue; } if (m->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 < m->npatterns; pat2++) if (pat1 != pat2 && m->patterns[pat2] != NULL && dupPatterns[pat2] == jsetNotSet && jssComparePattern(m->patterns[pat1], m->patterns[pat2])) { dmPrint(1, " * %d and %d are dupes.\n", pat1, pat2); dupPatterns[pat2] = pat1; ndupes++; } mapPatterns[pat1] = r->npatterns; r->patterns[r->npatterns] = m->patterns[pat1]; (r->npatterns)++; } else if (m->patterns[pat1] != NULL) nunused++; dmMsg(1, "%d used patterns (%d unused, %d duplicates).\n", r->npatterns, nunused, ndupes); // // Re-map instruments // dmMsg(1, "Creating sample instrument remaps...\n"); nunused = n8 = n16 = 0; for (int i = 0; i < jsetMaxInstruments; i++) if (usedInstruments[i]) { JSSInstrument *ip; if (optStripInstr) continue; if (i >= m->ninstruments) { dmErrorMsg("Instrument 0x%x >= 0x%x, but used!\n", i + 1, m->ninstruments); continue; } if ((ip = m->instruments[i]) == NULL) { dmErrorMsg("Instrument 0x%x used but is NULL.\n", i + 1); continue; } dmPrint(2, "%02x -> %02x : ", i + 1, r->ninstruments + 1); mapInstruments[i] = r->ninstruments; r->instruments[r->ninstruments] = ip; (r->ninstruments)++; if (ip->flags & jsf16bit) n16++; else n8++; } else if (m->instruments[i] != NULL) nunused++; dmPrint(2, "\n"); dmMsg(1, "Total of %d [16-bit] + %d [8-bit] samples = %d instruments (%d unused).\n", n16, n8, r->ninstruments, nunused); // // Re-map ext.instruments // dmMsg(1, "Creating ext.instrument remaps...\n"); nunused = 0; for (int i = 0; i < jsetMaxInstruments; i++) if (usedExtInstruments[i]) { JSSExtInstrument *eip; if (optStripExtInstr) continue; if (i >= m->nextInstruments) { dmErrorMsg("Ext.instrument 0x%x >= 0x%x, but used!\n", i + 1, m->nextInstruments); continue; } if ((eip = m->extInstruments[i]) == NULL) { dmErrorMsg("Extended instrument 0x%x used but is NULL.\n", i + 1); continue; } dmPrint(2, "%02x -> %02x : ", i + 1, r->nextInstruments + 1); mapExtInstruments[i] = r->nextInstruments; r->extInstruments[r->nextInstruments] = eip; (r->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", i + 1, note, q + 1, r->ninstruments); } dmPrint(3, "%02x.%02x ", q + 1, map + 1); eip->sNumForNotes[note] = map; } } } else if (m->extInstruments[i] != NULL) nunused++; dmPrint(2, "\n"); dmMsg(1, "%d extended instruments (%d unused).\n", r->nextInstruments, nunused); // // Remap pattern data with remapped instrument data // for (int i = 0; i < r->npatterns; i++) { JSSPattern *p = r->patterns[i]; JSSNote *n = p->data; for (int row = 0; row < p->nrows; row++) for (int channel = 0; channel < p->nchannels; channel++, n++) { // If not stripping extended instruments, check for // the validity of the used instrument and remap if (!optStripExtInstr) { if (n->instrument >= 0 && n->instrument < jsetMaxInstruments) n->instrument = mapExtInstruments[n->instrument]; if (n->instrument != jsetNotSet && r->extInstruments[n->instrument] == NULL) { dmErrorMsg("Non-existing instrument used #%d, INTERNAL ERROR.\n", n->instrument + 1); } } // Convert certain effects char effect; JMPGETEFFECT(effect, n->effect); switch (effect) { case 'C': // Cxx = Set volume if (n->volume == jsetNotSet) { n->volume = n->param; n->effect = jsetNotSet; n->param = jsetNotSet; } break; } } } // // Remap orders list // dmMsg(1, "Remapping orders list.\n"); nunused = 0; for (int i = 0; i < m->norders; i++) { int map = mapPatterns[m->orderList[i]]; if (map != m->orderList[i]) { dmPrint(2, "%02x -> %02x : ", m->orderList[i], map); nunused++; } r->orderList[i] = map; } if (nunused) dmPrint(2, "\n"); // // Do final pass on patterns to remove unused channels // for (int i = 0; i < r->npatterns; i++) { JSSPattern *p = r->patterns[i]; jssScanPattern(r, p, i, NULL, p->used); p->nmap = 0; for (int i = 0; i < r->nchannels; i++) { if (p->used[i]) p->map[p->nmap++] = i; } if (p->nmap != p->nchannels) { dmMsg(2, "Pattern %d: %d/%d used channels (%d unused).\n", i, p->nchannels - p->nmap, p->nchannels, p->nmap); } } return r; } int main(int argc, char *argv[]) { DMResource *inFile = NULL; FILE *outFile = NULL; JSSModule *sm, *dm; int result; 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) { dmErrorMsg("Input or output file not specified. Try --help.\n"); return 1; } // Read the source file if ((result = dmf_open_stdio(optInFilename, "rb", &inFile)) != DMERR_OK) { dmErrorMsg("Error opening input file '%s', %d: %s\n", optInFilename, result, dmErrorStr(result)); return 1; } // Initialize miniJSS jssInit(); // Read file dmMsg(1, "Reading XM-format file ...\n"); result = jssLoadXM(inFile, &sm, FALSE); dmf_close(inFile); if (result != 0) { dmErrorMsg("Error while loading XM file (%d), ", result); 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"); return 2; } } // Check stripping settings if (optStripExtInstr) optStripInstr = TRUE; if (optStripInstr) optStripSamples = TRUE; // Remove samples if (optStripSamples) { int i; dmMsg(1, "Stripping samples...\n"); for (i = 0; i < sm->ninstruments; i++) { dmFree(sm->instruments[i]->data); sm->instruments[i]->data = NULL; } } // Remove instruments if (optStripInstr) { int i; dmMsg(1, "Stripping instruments...\n"); for (i = 0; i < sm->ninstruments; i++) { dmFree(sm->instruments[i]); sm->instruments[i] = NULL; } sm->ninstruments = 0; } // Remove ext.instruments if (optStripExtInstr) { int i; dmMsg(1, "Stripping ext.instruments...\n"); for (i = 0; i < sm->nextInstruments; i++) { dmFree(sm->extInstruments[i]); sm->extInstruments[i] = NULL; } sm->nextInstruments = 0; } // Run the optimization procedure if (optOptimize) { dmMsg(1, "Optimizing module data...\n"); dm = jssOptimizeModule(sm); } else dm = sm; // Write output file if ((outFile = fopen(optOutFilename, "wb")) == NULL) { int err = dmGetErrno(); dmErrorMsg("Error creating output file '%s', %d: %s\n", optOutFilename, err, dmErrorStr(err)); return 1; } dmMsg(1, "Writing JSSMOD-format file [patMode=0x%04x, samp8=0x%02x, samp16=0x%02x]\n", optPatternMode, optSampMode8, optSampMode16); result = jssSaveJSSMOD(outFile, dm, optPatternMode, optSampMode8, optSampMode16); dmFree(sm); fclose(outFile); if (result != 0) { dmErrorMsg( "Error while saving JSSMOD file, %d: %s\n" "WARNING: The resulting file may be broken!\n", result, dmErrorStr(result)); } else { dmMsg(1, "Conversion complete.\n"); } return 0; }