changeset 2285:25398f2eba64

Merge.
author Matti Hamalainen <ccr@tnsp.org>
date Tue, 18 Jun 2019 14:23:02 +0300
parents 519c8726b235 (current diff) 2c90e455f006 (diff)
children e771185db600
files
diffstat 6 files changed, 406 insertions(+), 275 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile.gen	Tue Jun 18 07:38:05 2019 +0300
+++ b/Makefile.gen	Tue Jun 18 14:23:02 2019 +0300
@@ -367,15 +367,16 @@
 	$(COMPILE_OBJ)
 
 
-$(OBJPATH)jssmod.o: $(MINIJSS)jssmod.c $(MINIJSS)jssmod.h $(MINIJSS)jss.h
+$(OBJPATH)jssmod.o: $(MINIJSS)jssmod.c $(MINIJSS)jssmod.h $(OBJPATH)jss.o
 	$(COMPILE_OBJ)
 
-$(OBJPATH)jssplr.o: $(MINIJSS)jssplr.c $(MINIJSS)jssplr.h $(MINIJSS)jss.h $(MINIJSS)jssmod.h $(MINIJSS)jssmix.h
+$(OBJPATH)jssmix.o: $(MINIJSS)jssmix.c $(MINIJSS)jssmix.h $(OBJPATH)jssmod.o
 	$(COMPILE_OBJ)
 
-$(OBJPATH)jssmix.o: $(MINIJSS)jssmix.c $(MINIJSS)jssmix.h $(MINIJSS)jss.h
+$(OBJPATH)jssplr.o: $(MINIJSS)jssplr.c $(MINIJSS)jssplr.h $(OBJPATH)jssmix.o
 	$(COMPILE_OBJ)
 
+
 $(OBJPATH)dmblit.o: $(DMLIB_SRC)dmblit.c $(DMLIB_SRC)dmscaledblit.h $(DMLIB_SRC)dmunscaledblit.h $(DMLIB_SRC)dmblitfunc.h $(DMLIB_SRC)dmlib.h
 	$(COMPILE_OBJ)
 
--- a/minijss/jloadjss.c	Tue Jun 18 07:38:05 2019 +0300
+++ b/minijss/jloadjss.c	Tue Jun 18 14:23:02 2019 +0300
@@ -1,7 +1,7 @@
 /*
  * miniJSS - JSSMOD module loader
  * Programmed and designed by Matti 'ccr' Hamalainen
- * (C) Copyright 2007-2015 Tecnic Software productions (TNSP)
+ * (C) Copyright 2007-2019 Tecnic Software productions (TNSP)
  */
 #include "jssmod.h"
 
@@ -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 07:38:05 2019 +0300
+++ b/minijss/jssmod.c	Tue Jun 18 14:23:02 2019 +0300
@@ -1,7 +1,7 @@
 /*
  * miniJSS - Module structure and handling routines
  * Programmed and designed by Matti 'ccr' Hamalainen
- * (C) Copyright 2006-2015 Tecnic Software productions (TNSP)
+ * (C) Copyright 2006-2019 Tecnic Software productions (TNSP)
  */
 #include "jssmod.h"
 
@@ -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,22 @@
         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;
+        pattern->used[chn] = TRUE;
+    }
+
     return pattern;
 }
 
@@ -380,6 +397,8 @@
 {
     if (pattern != NULL)
     {
+        dmFree(pattern->used);
+        dmFree(pattern->map);
         dmFree(pattern->data);
         dmFree(pattern);
     }
--- a/minijss/jssmod.h	Tue Jun 18 07:38:05 2019 +0300
+++ b/minijss/jssmod.h	Tue Jun 18 14:23:02 2019 +0300
@@ -1,7 +1,7 @@
 /*
  * miniJSS - Module structure and handling routines
  * Programmed and designed by Matti 'ccr' Hamalainen
- * (C) Copyright 2006-2015 Tecnic Software productions (TNSP)
+ * (C) Copyright 2006-2019 Tecnic Software productions (TNSP)
  */
 #ifndef JSSMOD_H
 #define JSSMOD_H
@@ -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/ppl.c	Tue Jun 18 07:38:05 2019 +0300
+++ b/tools/ppl.c	Tue Jun 18 14:23:02 2019 +0300
@@ -1,7 +1,7 @@
 /*
  * Cyrbe Pasci Player - A simple SDL-based UI for XM module playing
  * Programmed and designed by Matti 'ccr' Hamalainen
- * (C) Copyright 2012-2018 Tecnic Software productions (TNSP)
+ * (C) Copyright 2012-2019 Tecnic Software productions (TNSP)
  *
  * Please read file 'COPYING' for information on license and distribution.
  */
@@ -61,7 +61,8 @@
         optOutFreq = 48000,
         optMuteOChannels = -1,
         optStartOrder = 0;
-BOOL    optUsePlayTime = FALSE;
+BOOL    optUsePlayTime = FALSE,
+        optUseGUI = TRUE;
 size_t  optPlayTime;
 
 
@@ -70,6 +71,7 @@
     {  0, '?', "help",     "Show this help", OPT_NONE },
     {  1, 'v', "verbose",  "Be more verbose", OPT_NONE },
     {  2,   0, "fs",       "Fullscreen", OPT_NONE },
+    { 12, 'C', "cli",      "Do not open GUI window", OPT_NONE },
     {  3, 'w', "window",   "Initial window size/resolution -w 640x480", OPT_ARGREQ },
 
     {  4, '1', "16bit",    "16-bit output", OPT_NONE },
@@ -163,6 +165,10 @@
             optUsePlayTime = TRUE;
             break;
 
+        case 12:
+            optUseGUI = FALSE;
+            break;
+
         default:
             dmErrorMsg("Unimplemented option argument '%s'.\n", currArg);
             return FALSE;
@@ -597,7 +603,7 @@
     {
         dmErrorMsg("Error loading module file, %d: %s\n",
             result, dmErrorStr(result));
-        goto error_exit;
+        goto exit;
     }
 
     // Try to convert it
@@ -605,39 +611,15 @@
     {
         dmErrorMsg("Could not convert module for playing, %d: %s\n",
             result, dmErrorStr(result));
-        goto error_exit;
+        goto exit;
     }
 
-    // Get font
-    result = dmf_open_memio(NULL, "pplfont.fnt", engineSetupFont, sizeof(engineSetupFont), &file);
-    if (result != DMERR_OK)
-    {
-        dmErrorMsg("Error opening font file 'pplfont.fnt', #%d: %s\n",
-            result, dmErrorStr(result));
-        goto error_exit;
-    }
-    result = dmLoadBitmapFont(file, &font);
-    dmf_close(file);
-    if (result != DMERR_OK)
-    {
-        dmErrorMsg("Could not load font from file, %d: %s\n",
-            result, dmErrorStr(result));
-        goto error_exit;
-    }
-
-    SDL_Color pal[DMFONT_NPALETTE];
-    for (int n = 0; n < DMFONT_NPALETTE; n++)
-    {
-        pal[n].r = pal[n].g = pal[n].b = 0;
-        pal[n].a = n > 0 ? 255 : 0;
-    }
-    dmSetBitmapFontPalette(font, pal, 0, DMFONT_NPALETTE);
 
     // Initialize SDL components
     if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER) != 0)
     {
         dmErrorMsg("Could not initialize SDL: %s\n", SDL_GetError());
-        goto error_exit;
+        goto exit;
     }
     initSDL = TRUE;
 
@@ -650,7 +632,7 @@
     if (eng.dev == NULL)
     {
         dmErrorMsg("jvmInit() returned NULL\n");
-        goto error_exit;
+        goto exit;
     }
 
     switch (optOutFormat)
@@ -662,7 +644,7 @@
         default:
             dmErrorMsg("Unsupported audio format %d (could not set matching SDL format)\n",
                 optOutFormat);
-            goto error_exit;
+            goto exit;
     }
 
     eng.afmt.freq     = optOutFreq;
@@ -676,7 +658,7 @@
     {
         dmErrorMsg("Couldn't open SDL audio: %s\n",
             SDL_GetError());
-        goto error_exit;
+        goto exit;
     }
     audioInit = TRUE;
 
@@ -684,7 +666,7 @@
     if ((eng.plr = jmpInit(eng.dev)) == NULL)
     {
         dmErrorMsg("jmpInit() returned NULL\n");
-        goto error_exit;
+        goto exit;
     }
 
     jvmSetCallback(eng.dev, jmpExec, eng.plr);
@@ -700,32 +682,60 @@
         muteState = TRUE;
     }
 
-    // Open window
-    if ((eng.window = SDL_CreateWindow(dmProgName,
-        SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
-        eng.optScrWidth, eng.optScrHeight,
-        eng.optVFlags | SDL_WINDOW_RESIZABLE
-        //| SDL_WINDOW_HIDDEN
-        )) == NULL)
+    if (optUseGUI)
     {
-        dmErrorMsg("Can't create an SDL window: %s\n", SDL_GetError());
-        goto error_exit;
-    }
+        // Get font
+        result = dmf_open_memio(NULL, "pplfont.fnt", engineSetupFont, sizeof(engineSetupFont), &file);
+        if (result != DMERR_OK)
+        {
+            dmErrorMsg("Error opening font file 'pplfont.fnt', #%d: %s\n",
+                result, dmErrorStr(result));
+            goto exit;
+        }
+        result = dmLoadBitmapFont(file, &font);
+        dmf_close(file);
+        if (result != DMERR_OK)
+        {
+            dmErrorMsg("Could not load font from file, %d: %s\n",
+                result, dmErrorStr(result));
+            goto exit;
+        }
+
+        SDL_Color pal[DMFONT_NPALETTE];
+        for (int n = 0; n < DMFONT_NPALETTE; n++)
+        {
+            pal[n].r = pal[n].g = pal[n].b = 0;
+            pal[n].a = n > 0 ? 255 : 0;
+        }
+        dmSetBitmapFontPalette(font, pal, 0, DMFONT_NPALETTE);
 
-    SDL_SetWindowTitle(eng.window, dmProgDesc);
+        // Open window
+        if ((eng.window = SDL_CreateWindow(dmProgName,
+            SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
+            eng.optScrWidth, eng.optScrHeight,
+            eng.optVFlags | SDL_WINDOW_RESIZABLE
+            //| SDL_WINDOW_HIDDEN
+            )) == NULL)
+        {
+            dmErrorMsg("Can't create an SDL window: %s\n", SDL_GetError());
+            goto exit;
+        }
 
-    if ((eng.renderer = SDL_CreateRenderer(eng.window, -1, SDL_RENDERER_PRESENTVSYNC)) == NULL)
-    {
-        dmErrorMsg("Can't create an SDL renderer: %s\n", SDL_GetError());
-        goto error_exit;
+        SDL_SetWindowTitle(eng.window, dmProgDesc);
+
+        if ((eng.renderer = SDL_CreateRenderer(eng.window, -1, SDL_RENDERER_PRESENTVSYNC)) == NULL)
+        {
+            dmErrorMsg("Can't create an SDL renderer: %s\n", SDL_GetError());
+            goto exit;
+        }
+
+        if (!dmInitializeVideo())
+            goto exit;
+
+        //SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "best");
+        //SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY, SDL_DEFAULT_REPEAT_INTERVAL);
     }
 
-    if (!dmInitializeVideo())
-        goto error_exit;
-
-//    SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "best");
-//    SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY, SDL_DEFAULT_REPEAT_INTERVAL);
-
     // okay, main loop here ... "play" module and print out info
     SDL_LockAudio();
     SDL_PauseAudio(0);
@@ -809,7 +819,7 @@
                     case SDLK_f:
                         eng.optVFlags ^= SDL_WINDOW_FULLSCREEN_DESKTOP;
                         if (SDL_SetWindowFullscreen(eng.window, eng.optVFlags) != 0)
-                            goto error_exit;
+                            goto exit;
                         needUpdate = TRUE;
                         break;
 
@@ -830,7 +840,7 @@
                         eng.optScrWidth  = eng.event.window.data1;
                         eng.optScrHeight = eng.event.window.data2;
                         if (!dmInitializeVideo())
-                            goto error_exit;
+                            goto exit;
 
                         needUpdate = TRUE;
                         break;
@@ -842,98 +852,110 @@
                 break;
         }
 
-
-#if 1
+        // Check for end of song
         JSS_LOCK(eng.plr);
-        JSSPattern *currPattern = eng.plr->pattern;
-        int currRow = eng.plr->row;
         if (!eng.plr->isPlaying)
             eng.exitFlag = TRUE;
         JSS_UNLOCK(eng.plr);
 
-        if (currRow != prevRow || needUpdate)
-        {
-            prevRow = currRow;
-            needUpdate = TRUE;
-        }
-
-        // Draw frame
-        if (needUpdate)
+        if (optUseGUI)
         {
-            dmClearSurface(eng.screen, col.boxBg);
-
-            dmDrawBMTextQ(eng.screen, font, DMD_TRANSPARENT, 5, 5,
-                "%s v%s by ccr/TNSP - (c) Copyright 2012-2018 TNSP",
-                dmProgDesc, dmProgVersion);
-
-            dmDrawBMTextQ(eng.screen, font, DMD_TRANSPARENT, 5, 5 + (font->height + 2),
-                "Song: '%s'",
-                eng.mod->moduleName);
-
-            dmDisplayPattern(eng.screen, 5, 5 + (font->height + 2) * 3 + 4,
-                eng.screen->w - 6,
-                eng.screen->h * 0.8,
-                currPattern, currRow);
-
+            // Check if we need to update screen
             JSS_LOCK(eng.plr);
-            dmDrawBMTextQ(eng.screen, font, DMD_TRANSPARENT, 5, 5 + (font->height + 2) * 2,
-            "Tempo: %3d | Speed: %3d | Row: %3d/%-3d | Order: %3d/%-3d | Pattern: %3d/%-3d",
-            eng.plr->tempo, eng.plr->speed,
-            eng.plr->row, (eng.plr->pattern != NULL) ? eng.plr->pattern->nrows : 0,
-            eng.plr->order + 1, eng.mod->norders,
-            eng.plr->npattern, eng.mod->npatterns);
+            JSSPattern *currPattern = eng.plr->pattern;
+            int currRow = eng.plr->row;
             JSS_UNLOCK(eng.plr);
-            needRender = TRUE;
-        }
+
+            if (currRow != prevRow || needUpdate)
+            {
+                prevRow = currRow;
+                needUpdate = TRUE;
+            }
+
+            // Draw frame
+            if (needUpdate)
+            {
+                dmClearSurface(eng.screen, col.boxBg);
 
-        if (needUpdate || currTick - prevTick >= (eng.pauseFlag ? 100 : 20))
-        {
-            JSS_LOCK(eng.dev);
-            dmDisplayChannels(eng.screen, 5, eng.screen->h * 0.8 + 5,
-                eng.screen->w - 5, eng.screen->h - 5, eng.dev);
-            JSS_UNLOCK(eng.dev);
-            needRender = TRUE;
-        }
+                dmDrawBMTextQ(eng.screen, font, DMD_TRANSPARENT, 5, 5,
+                    "%s v%s by ccr/TNSP - (c) Copyright 2012-2019 TNSP",
+                    dmProgDesc, dmProgVersion);
 
-        if (needUpdate)
-            prevTick = currTick;
+                dmDrawBMTextQ(eng.screen, font, DMD_TRANSPARENT, 5, 5 + (font->height + 2),
+                    "Song: '%s'",
+                    eng.mod->moduleName);
+
+                dmDisplayPattern(eng.screen, 5, 5 + (font->height + 2) * 3 + 4,
+                    eng.screen->w - 6,
+                    eng.screen->h * 0.8,
+                    currPattern, currRow);
 
-#endif
-        // Flip screen
-        if (needRender)
-        {
-            SDL_Surface dst;
-            SDL_LockTexture(eng.texture, NULL, &dst.pixels, &dst.pitch);
+                JSS_LOCK(eng.plr);
+                dmDrawBMTextQ(eng.screen, font, DMD_TRANSPARENT, 5, 5 + (font->height + 2) * 2,
+                "Tempo: %3d | Speed: %3d | Row: %3d/%-3d | Order: %3d/%-3d | Pattern: %3d/%-3d",
+                eng.plr->tempo, eng.plr->speed,
+                eng.plr->row, (eng.plr->pattern != NULL) ? eng.plr->pattern->nrows : 0,
+                eng.plr->order + 1, eng.mod->norders,
+                eng.plr->npattern, eng.mod->npatterns);
+                JSS_UNLOCK(eng.plr);
+                needRender = TRUE;
+            }
+
+            if (needUpdate || currTick - prevTick >= (eng.pauseFlag ? 100 : 20))
+            {
+                JSS_LOCK(eng.dev);
+                dmDisplayChannels(eng.screen, 5, eng.screen->h * 0.8 + 5,
+                    eng.screen->w - 5, eng.screen->h - 5, eng.dev);
+                JSS_UNLOCK(eng.dev);
+                needRender = TRUE;
+            }
 
-            if (dst.pitch != eng.screen->pitch)
-                eng.exitFlag = TRUE;
-            else
-                memcpy(dst.pixels, eng.screen->pixels, eng.screen->h * dst.pitch);
+            if (needUpdate)
+                prevTick = currTick;
+
+            // Flip screen
+            if (needRender)
+            {
+                SDL_Surface dst;
+                SDL_LockTexture(eng.texture, NULL, &dst.pixels, &dst.pitch);
 
-            SDL_UnlockTexture(eng.texture);
+                if (dst.pitch != eng.screen->pitch)
+                    eng.exitFlag = TRUE;
+                else
+                    memcpy(dst.pixels, eng.screen->pixels, eng.screen->h * dst.pitch);
 
-            //SDL_RenderClear(eng.renderer);
-            SDL_RenderCopy(eng.renderer, eng.texture, NULL, NULL);
-            SDL_RenderPresent(eng.renderer);
-        }
+                SDL_UnlockTexture(eng.texture);
+
+                //SDL_RenderClear(eng.renderer);
+                SDL_RenderCopy(eng.renderer, eng.texture, NULL, NULL);
+                SDL_RenderPresent(eng.renderer);
+            }
+        } // optUseGUI
 
         SDL_Delay(eng.pauseFlag ? 100 : 30);
     }
 
-error_exit:
-    if (eng.texture != NULL)
-        SDL_DestroyTexture(eng.texture);
+exit:
+    // Cleanup
+    if (optUseGUI)
+    {
+        dmMsg(1, "GUI shutdown.\n");
+        if (eng.texture != NULL)
+            SDL_DestroyTexture(eng.texture);
 
-    if (eng.renderer != NULL)
-        SDL_DestroyRenderer(eng.renderer);
+        if (eng.renderer != NULL)
+            SDL_DestroyRenderer(eng.renderer);
 
-    if (eng.window != NULL)
-        SDL_DestroyWindow(eng.window);
+        if (eng.window != NULL)
+            SDL_DestroyWindow(eng.window);
 
-    if (eng.screen != NULL)
-        SDL_FreeSurface(eng.screen);
+        if (eng.screen != NULL)
+            SDL_FreeSurface(eng.screen);
 
-    dmMsg(0, "Audio shutdown.\n");
+        dmFreeBitmapFont(font);
+    }
+
+    dmMsg(1, "Audio shutdown.\n");
     if (audioInit)
     {
         SDL_LockAudio();
@@ -946,8 +968,7 @@
     jvmClose(eng.dev);
     jssFreeModule(eng.mod);
 
-    dmFreeBitmapFont(font);
-
+    dmMsg(1, "Shut down SDL.\n");
     if (initSDL)
         SDL_Quit();
 
--- a/tools/xm2jss.c	Tue Jun 18 07:38:05 2019 +0300
+++ b/tools/xm2jss.c	Tue Jun 18 14:23:02 2019 +0300
@@ -1,7 +1,7 @@
 /*
  * xm2jss - Convert XM module to JSSMOD
  * Programmed and designed by Matti 'ccr' Hamalainen
- * (C) Copyright 2006-2017 Tecnic Software productions (TNSP)
+ * (C) Copyright 2006-2019 Tecnic Software productions (TNSP)
  *
  * Please read file 'COPYING' for information on license and distribution.
  */
@@ -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,
@@ -546,12 +577,18 @@
 
     // Convert and write patterns
     for (totalSize = index = 0; index < module->npatterns; index++)
-    if (module->patterns[index] != NULL)
     {
         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,
@@ -593,17 +630,30 @@
 
         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);
         }
-    }
-    else
-    {
-        JSSERROR(DMERR_NULLPTR, DMERR_NULLPTR,
-        "Pattern #%d was NULL.\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);
@@ -620,8 +670,8 @@
         {
             einst = &tmpEInst;
             memset(&tmpEInst, 0, sizeof(tmpEInst));
-            JSSWARNING(DMERR_NULLPTR, DMERR_NULLPTR,
-            "Extended instrument #%d NULL!\n",
+            dmMsg(1,
+            "Extended instrument #%d is NULL!\n",
             index);
         }
 
@@ -744,18 +794,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,16 +825,20 @@
             n->effect != jsetNotSet ||
             n->param != jsetNotSet)
         {
-            usedChannels[channel] = TRUE;
-            *empty = FALSE;
+            if (usedChannels != NULL)
+                usedChannels[channel] = TRUE;
+
+            empty = FALSE;
         }
     }
+
+    return empty;
 }
 
 
 /* Check if two given patterns are dupes
  */
-BOOL comparePattern(const JSSPattern *pat1, const JSSPattern *pat2)
+BOOL jssComparePattern(const JSSPattern *pat1, const JSSPattern *pat2)
 {
     return
         pat1->nrows     == pat2->nrows &&
@@ -794,12 +849,11 @@
 
 /* Optimize a given module
  */
-JSSModule *optimizeModule(JSSModule *m)
+JSSModule *jssOptimizeModule(JSSModule *m)
 {
     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 +881,6 @@
     for (int i = 0; i < jsetNChannels; i++)
     {
         r->defPanning[i] = m->defPanning[i];
-        usedChannels[i]  = FALSE;
     }
 
     // Initialize values
@@ -859,13 +912,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 +921,8 @@
                     m->orderList[norder] = jsetNotSet;
                     usedPatterns[npat] = FALSE;
                 }
+                else
+                    usedPatterns[npat] = TRUE;
             }
             else
             {
@@ -951,7 +1001,7 @@
         for (int pat2 = 0; pat2 < m->npatterns; pat2++)
         if (pat1 != pat2 && m->patterns[pat2] != NULL &&
             dupPatterns[pat2] == jsetNotSet &&
-            comparePattern(m->patterns[pat1], m->patterns[pat2]))
+            jssComparePattern(m->patterns[pat1], m->patterns[pat2]))
         {
             dmPrint(1, " * %d and %d are dupes.\n", pat1, pat2);
             dupPatterns[pat2] = pat1;
@@ -1077,19 +1127,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 +1187,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;
 }
 
@@ -1161,7 +1221,7 @@
     JSSModule *sm, *dm;
     int result;
 
-    dmInitProg("xm2jss", "XM to JSSMOD converter", "0.7", NULL, NULL);
+    dmInitProg("xm2jss", "XM to JSSMOD converter", "0.8", NULL, NULL);
     dmVerbosity = 0;
 
     // Parse arguments
@@ -1252,7 +1312,7 @@
     if (optOptimize)
     {
         dmMsg(1, "Optimizing module data...\n");
-        dm = optimizeModule(sm);
+        dm = jssOptimizeModule(sm);
     } else
         dm = sm;