changeset 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 026c3aa0e48f
children fc58f62f100c
files minijss/jloadjss.c minijss/jssmod.c minijss/jssmod.h tools/xm2jss.c
diffstat 4 files changed, 232 insertions(+), 127 deletions(-) [+]
line wrap: on
line diff
--- a/minijss/jloadjss.c	Tue Jun 18 12:11:16 2019 +0300
+++ b/minijss/jloadjss.c	Tue Jun 18 12:12:51 2019 +0300
@@ -16,13 +16,19 @@
 #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, Uint8 note)
+static int jssDoGetConvertedNote(DMResource *inFile, JSSNote *pnote, const Uint8 note)
 {
     Uint8 tmp;
 
@@ -50,18 +56,29 @@
 }
 
 
-static inline int jssGetConvertedNote(DMResource *inFile, JSSNote *pnote)
+static inline int jssGetConvertedNote(DMResource *inFile,
+    JSSPattern *pattern, const int channel, const int row)
 {
     Uint8 tmp;
+    int res;
+
     JSGETBYTE(&tmp);
-    return jssDoGetConvertedNote(inFile, pnote, 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 jssGetCompressedNote(DMResource *inFile, JSSNote *pnote)
+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)
@@ -102,12 +119,23 @@
     }
     else
     {
-        int ret;
-        if ((ret = jssDoGetConvertedNote(inFile, pnote, packb)) != DMERR_OK)
-            return ret;
+        res = jssDoGetConvertedNote(inFile, pnote, packb);
     }
 
-    return DMERR_OK;
+    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
 
@@ -115,19 +143,12 @@
 #ifdef JM_SUP_PATMODE_2
 static int jssGetPatternCompHoriz(DMResource *inFile, JSSPattern *pattern)
 {
-    int row, channel;
-
-    assert(buf != NULL);
-    assert(pattern != NULL);
-
-    for (row = 0; row < pattern->nrows; row++)
-    for (channel = 0; channel < pattern->nchannels; channel++)
+    for (int row = 0; row < pattern->nrows; row++)
+    for (int channel = 0; channel < pattern->nmap; channel++)
     {
-        JSSNote *pnote = &pattern->data[(pattern->nchannels * row) + channel];
-        int res = jssGetCompressedNote(inFile, pnote);
+        int res = jssGetCompressedNote(inFile, pattern, channel, row);
         if (res != DMERR_OK)
-            JSSERROR(res, res, "Error uncompressing note on row=%d, chn=%d\n",
-            row, channel);
+            return res;
     }
 
     return DMERR_OK;
@@ -138,19 +159,12 @@
 #ifdef JM_SUP_PATMODE_4
 static int jssGetPatternCompVert(DMResource *inFile, JSSPattern *pattern)
 {
-    int row, channel;
-
-    assert(buf != NULL);
-    assert(pattern != NULL);
-
-    for (channel = 0; channel < pattern->nchannels; channel++)
-    for (row = 0; row < pattern->nrows; row++)
+    for (int channel = 0; channel < pattern->nmap; channel++)
+    for (int row = 0; row < pattern->nrows; row++)
     {
-        JSSNote *pnote = &pattern->data[(pattern->nchannels * row) + channel];
-        int res = jssGetCompressedNote(inFile, pnote);
+        int res = jssGetCompressedNote(inFile, pattern, channel, row);
         if (res != DMERR_OK)
-            JSSERROR(res, res, "Error uncompressing note on row=%d, chn=%d\n",
-            row, channel);
+            return res;
     }
 
     return DMERR_OK;
@@ -161,19 +175,12 @@
 #ifdef JM_SUP_PATMODE_1
 static int jssGetPatternRawHoriz(DMResource *inFile, JSSPattern *pattern)
 {
-    int row, channel;
-
-    assert(buf != NULL);
-    assert(pattern != NULL);
-
-    for (row = 0; row < pattern->nrows; row++)
-    for (channel = 0; channel < pattern->nchannels; channel++)
+    for (int row = 0; row < pattern->nrows; row++)
+    for (int channel = 0; channel < pattern->nmap; channel++)
     {
-        JSSNote *pnote = &pattern->data[(pattern->nchannels * row) + channel];
-        int res = jssGetConvertedNote(inFile, pnote);
+        int res = jssGetConvertedNote(inFile, pattern, channel, row);
         if (res != DMERR_OK)
-            JSSERROR(res, res, "Error converting note on row=%d, chn=%d\n",
-            row, channel);
+            return res;
     }
 
     return DMERR_OK;
@@ -184,19 +191,12 @@
 #ifdef JM_SUP_PATMODE_3
 static int jssGetPatternRawVert(DMResource *inFile, JSSPattern *pattern)
 {
-    int row, channel;
-
-    assert(buf != NULL);
-    assert(pattern != NULL);
-
-    for (channel = 0; channel < pattern->nchannels; channel++)
-    for (row = 0; row < pattern->nrows; row++)
+    for (int channel = 0; channel < pattern->nmap; channel++)
+    for (int row = 0; row < pattern->nrows; row++)
     {
-        JSSNote *pnote = &pattern->data[(pattern->nchannels * row) + channel];
-        int res = jssGetConvertedNote(inFile, pnote);
+        int res = jssGetConvertedNote(inFile, pattern, channel, row);
         if (res != DMERR_OK)
-            JSSERROR(res, res, "Error converting note on row=%d, chn=%d\n",
-            row, channel);
+            return res;
     }
 
     return DMERR_OK;
@@ -209,21 +209,18 @@
 #undef JSGETBYTE
 #define JSGETBYTE(XV) if (!dmf_read_byte(inFile, XV)) return DMERR_OUT_OF_DATA
 
-#define JSFOREACHNOTE1                                                              \
-  for (channel = 0; channel < pattern->nchannels; channel++)                        \
-  for (row = 0; row < pattern->nrows; row++) {                                      \
-      JSSNote *pnote = pattern->data + (pattern->nchannels * row) + channel;
+#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)
 {
-    int row, channel;
     Uint8 tmp;
 
-    assert(buf != NULL);
-    assert(pattern != NULL);
-
     JSFOREACHNOTE1
     JSGETBYTE(&tmp);
     if (tmp == 0)
@@ -433,7 +430,8 @@
 
         // Read pattern header
         if (!dmf_read_le32(inFile, &jssP.size) ||
-            !dmf_read_le16(inFile, &jssP.nrows))
+            !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);
@@ -444,6 +442,11 @@
             "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)
@@ -453,6 +456,28 @@
             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)
         {
--- a/minijss/jssmod.c	Tue Jun 18 12:11:16 2019 +0300
+++ b/minijss/jssmod.c	Tue Jun 18 12:12:51 2019 +0300
@@ -343,7 +343,7 @@
 
     // Check arguments
     if (nrows <= 0 || nchannels <= 0)
-        JSSERROR(DMERR_INVALID_ARGS, NULL, "Invalid nrows=%i or nchannels=%i.\n", nrows, nchannels);
+        JSSERROR(DMERR_INVALID_ARGS, NULL, "Invalid nrows=%d or nchannels=%d.\n", nrows, nchannels);
 
     // Allocate a pattern structure
     if ((pattern = dmMalloc0(sizeof(JSSPattern))) == NULL)
@@ -353,14 +353,15 @@
     pattern->data = dmMalloc(nrows * nchannels * sizeof(JSSNote));
     if (pattern->data == NULL)
     {
-        dmFree(pattern);
-        JSSERROR(DMERR_MALLOC, NULL, "Could not allocate pattern data (nrows=%i, nchannels=%i).\n", nrows,
-             nchannels);
+        jssFreePattern(pattern);
+        JSSERROR(DMERR_MALLOC, NULL, "Could not allocate pattern data (nrows=%d, nchannels=%d).\n",
+            nrows, nchannels);
     }
 
     // Initialize structure
     pattern->nrows     = nrows;
     pattern->nchannels = nchannels;
+    pattern->nmap      = nchannels;
 
     pnote = pattern->data;
     for (int row = 0; row < nrows; row++)
@@ -372,6 +373,19 @@
         pnote++;
     }
 
+    // Initialize pattern channel map
+    pattern->map = dmMalloc(nchannels * sizeof(pattern->map[0]));
+    pattern->used = dmMalloc(nchannels * sizeof(pattern->used[0]));
+    if (pattern->map == NULL || pattern->used == NULL)
+    {
+        jssFreePattern(pattern);
+        JSSERROR(DMERR_MALLOC, NULL, "Could not allocate pattern map (nchannels=%d).\n",
+             nchannels);
+    }
+
+    for (int chn = 0; chn < nchannels; chn++)
+        pattern->map[chn] = chn;
+
     return pattern;
 }
 
@@ -380,6 +394,8 @@
 {
     if (pattern != NULL)
     {
+        dmFree(pattern->used);
+        dmFree(pattern->map);
         dmFree(pattern->data);
         dmFree(pattern);
     }
--- a/minijss/jssmod.h	Tue Jun 18 12:11:16 2019 +0300
+++ b/minijss/jssmod.h	Tue Jun 18 12:12:51 2019 +0300
@@ -143,7 +143,9 @@
 
 typedef struct
 {
-    int nrows, nchannels;
+    int nrows, nchannels, nmap;
+    BOOL *used;
+    Uint8 *map;
     JSSNote *data;
 } JSSPattern;
 
@@ -180,7 +182,7 @@
 
 #ifdef JSS_SUP_JSSMOD
 
-#define JSSMOD_VERSION    (0x20)
+#define JSSMOD_VERSION    (0x30)
 
 enum
 {
@@ -290,6 +292,9 @@
 {
     Uint32 size;
     Uint16 nrows;
+    Uint16 nchannels; // may differ from JSSMODHeader::nchannels
+    // IF nchannels != JSSMODHeader::nchannels, then:
+    // Uint8 map[JSSMODPattern::nchannels];
 } JSSMODPattern;
 
 #endif
--- a/tools/xm2jss.c	Tue Jun 18 12:11:16 2019 +0300
+++ b/tools/xm2jss.c	Tue Jun 18 12:12:51 2019 +0300
@@ -147,6 +147,15 @@
 }
 
 
+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.
  */
@@ -189,7 +198,7 @@
 
 /* Convert a note
  */
-static int jssConvertNote(
+static int jssDoConvertNote(
     Uint8 *patBuf, const size_t patBufSize,
     size_t *patSize, const JSSNote *pnote)
 {
@@ -220,7 +229,7 @@
 
 /* Compress a note
  */
-static int jssCompressNote(
+static int jssDoCompressNote(
     Uint8 *patBuf, const size_t patBufSize,
     size_t *patSize, const JSSNote *pnote)
 {
@@ -256,13 +265,33 @@
         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 jssConvertNote(patBuf, patBufSize, patSize, pnote);
+        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;
 }
 
@@ -278,14 +307,9 @@
     for (int row = 0; row < pattern->nrows; row++)
     for (int channel = 0; channel < pattern->nchannels; channel++)
     {
-        const JSSNote *pnote = &pattern->data[(pattern->nchannels * row) + channel];
-        const int res = jssCompressNote(patBuf, patBufSize, patSize, pnote);
+        int res = jssCompressNote(patBuf, patBufSize, patSize, pattern, channel, row);
         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;
@@ -301,14 +325,9 @@
     for (int channel = 0; channel < pattern->nchannels; channel++)
     for (int row = 0; row < pattern->nrows; row++)
     {
-        const JSSNote *pnote = &pattern->data[(pattern->nchannels * row) + channel];
-        const int res = jssCompressNote(patBuf, patBufSize, patSize, pnote);
+        int res = jssCompressNote(patBuf, patBufSize, patSize, pattern, channel, row);
         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;
@@ -317,6 +336,26 @@
 
 /* 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)
@@ -326,14 +365,9 @@
     for (int row = 0; row < pattern->nrows; row++)
     for (int channel = 0; channel < pattern->nchannels; channel++)
     {
-        const JSSNote *pnote = &pattern->data[(pattern->nchannels * row) + channel];
-        const int res = jssConvertNote(patBuf, patBufSize, patSize, pnote);
+        int res = jssConvertNote(patBuf, patBufSize, patSize, pattern, channel, row);
         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;
@@ -349,14 +383,9 @@
     for (int channel = 0; channel < pattern->nchannels; channel++)
     for (int row = 0; row < pattern->nrows; row++)
     {
-        const JSSNote *pnote = &pattern->data[(pattern->nchannels * row) + channel];
-        const int res = jssConvertNote(patBuf, patBufSize, patSize, pnote);
+        int res = jssConvertNote(patBuf, patBufSize, patSize, pattern, channel, row);
         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;
@@ -366,9 +395,11 @@
 #define JSFOREACHNOTE1                                                              \
   for (channel = 0; channel < pattern->nchannels; channel++)                        \
   for (row = 0; row < pattern->nrows; row++) {                                      \
-  const JSSNote *pnote = &pattern->data[(pattern->nchannels * row) + channel];
+  const JSSNote *pnote = jssGetNotePtr(pattern, channel, row); \
+  if (pnote != NULL) {
 
-#define JSFOREACHNOTE2 }
+#define JSFOREACHNOTE2 } }
+
 
 static int jssConvertPatternRawElem(
     Uint8 *patBuf, const size_t patBufSize,
@@ -593,10 +624,28 @@
 
         if (!dm_fwrite_le32(outFile, dataSize) ||
             !dm_fwrite_le16(outFile, pattern->nrows) ||
-            !dm_fwrite_str(outFile, patBuf, dataSize))
+            !dm_fwrite_le16(outFile, pattern->nmap))
         {
             JSSERROR(DMERR_FWRITE, DMERR_FWRITE,
-            "Error writing JSSMOD pattern #%d.\n",
+            "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);
         }
     }
@@ -744,18 +793,19 @@
 /* Scan given pattern for used instruments and channels.
  * Also checks if the pattern is empty.
  */
-void scanPattern(const JSSModule *module, const JSSPattern *pattern,
-    const int npattern, BOOL *usedExtInstruments, BOOL *usedChannels, BOOL *empty)
+BOOL jssScanPattern(const JSSModule *module, const JSSPattern *pattern,
+    const int npattern, BOOL *usedExtInstruments, BOOL *usedChannels)
 {
     JSSNote *n = pattern->data;
-    *empty = FALSE;
+    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 (n->instrument != jsetNotSet)
+        if (usedExtInstruments != NULL &&
+            n->instrument != jsetNotSet)
         {
             // Is it valid?
             if (n->instrument >= 0 && n->instrument < module->nextInstruments)
@@ -774,10 +824,14 @@
             n->effect != jsetNotSet ||
             n->param != jsetNotSet)
         {
-            usedChannels[channel] = TRUE;
-            *empty = FALSE;
+            if (usedChannels != NULL)
+                usedChannels[channel] = TRUE;
+
+            empty = FALSE;
         }
     }
+
+    return empty;
 }
 
 
@@ -798,8 +852,7 @@
 {
     BOOL usedPatterns[jsetMaxPatterns + 1],
          usedInstruments[jsetMaxInstruments + 1],
-         usedExtInstruments[jsetMaxInstruments + 1],
-         usedChannels[jsetMaxChannels];
+         usedExtInstruments[jsetMaxInstruments + 1];
     int  mapExtInstruments[jsetMaxInstruments + 1],
          mapInstruments[jsetMaxInstruments + 1],
          mapPatterns[jsetMaxPatterns + 1],
@@ -827,7 +880,6 @@
     for (int i = 0; i < jsetNChannels; i++)
     {
         r->defPanning[i] = m->defPanning[i];
-        usedChannels[i]  = FALSE;
     }
 
     // Initialize values
@@ -859,13 +911,8 @@
             JSSPattern *pattern = m->patterns[npat];
             if (pattern != NULL)
             {
-                BOOL empty;
-
-                // Mark this pattern as used
-                usedPatterns[npat] = TRUE;
-
-                // Scan for used instruments and channels
-                scanPattern(m, pattern, npat, usedExtInstruments, usedChannels, &empty);
+                // 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)
@@ -873,6 +920,8 @@
                     m->orderList[norder] = jsetNotSet;
                     usedPatterns[npat] = FALSE;
                 }
+                else
+                    usedPatterns[npat] = TRUE;
             }
             else
             {
@@ -1077,19 +1126,6 @@
         r->nextInstruments, nunused);
 
     //
-    // Check for actually used channels
-    // XXX TODO: Actually remove the unused channels.
-    //
-    nunused = 0;
-    for (int i = 0; i < r->nchannels; i++)
-    {
-        if(!usedChannels[i])
-            nunused++;
-    }
-    dmMsg(1, "%d channels (%d unused).\n",
-        r->nchannels - nunused, nunused);
-
-    //
     // Remap pattern data with remapped instrument data
     //
     for (int i = 0; i < r->npatterns; i++)
@@ -1150,6 +1186,29 @@
     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;
 }