diff minijss/jssplr.c @ 658:c430112449a7

Move miniJSS into a subdirectory.
author Matti Hamalainen <ccr@tnsp.org>
date Tue, 16 Apr 2013 07:32:29 +0300
parents jssplr.c@67d9e319246f
children 4ff7d7f6f4d1
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/minijss/jssplr.c	Tue Apr 16 07:32:29 2013 +0300
@@ -0,0 +1,1579 @@
+/*
+ * miniJSS - Module playing routines
+ * Programmed and designed by Matti 'ccr' Hamalainen
+ * (C) Copyright 2006-2012 Tecnic Software productions (TNSP)
+ */
+#include "jssplr.h"
+
+// FIXME!! FIX ME!
+#include <math.h>
+
+/* Miscellaneous tables
+ */
+static const Uint8 jmpSineTab[32] =
+{
+      0,  24,  49,  74,  97, 120, 141, 161,
+    180, 197, 212, 224, 235, 244, 250, 253,
+    255, 253, 250, 244, 235, 224, 212, 197,
+    180, 161, 141, 120,  97,  74,  49,  24
+};
+
+
+static const Sint16 jmpXMAmigaPeriodTab[13 * 8] = {
+    907, 900, 894, 887, 881, 875, 868, 862, 856, 850, 844, 838,
+    832, 826, 820, 814, 808, 802, 796, 791, 785, 779, 774, 768,
+    762, 757, 752, 746, 741, 736, 730, 725, 720, 715, 709, 704,
+    699, 694, 689, 684, 678, 675, 670, 665, 660, 655, 651, 646,
+    640, 636, 632, 628, 623, 619, 614, 610, 604, 601, 597, 592,
+    588, 584, 580, 575, 570, 567, 563, 559, 555, 551, 547, 543,
+    538, 535, 532, 528, 524, 520, 516, 513, 508, 505, 502, 498,
+    494, 491, 487, 484, 480, 477, 474, 470, 467, 463, 460, 457,
+
+    453, 450, 447, 443, 440, 437, 434, 431
+};
+
+
+#define jmpNMODEffectTable (36)
+static const char jmpMODEffectTable[jmpNMODEffectTable] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+
+/* Helper functions
+ */
+static int jmpNoteToAmigaPeriod(int note, int finetune)
+{
+    int tmp = dmClamp(note + finetune + 8, 0, 103);
+    return jmpXMAmigaPeriodTab[tmp];
+}
+
+
+static int jmpGetPeriodFromNote(JSSPlayer *mp, int note, int finetune)
+{
+    int res;
+
+    if (JMPGETMODFLAGS(mp, jmdfAmigaPeriods))
+    {
+        int mfinetune = finetune / 16,
+            mnote = (note % 12) * 8,
+            moctave = note / 12,
+            period1, period2;
+        
+        period1 = jmpNoteToAmigaPeriod(mnote, mfinetune);
+        
+        if (finetune < 0)
+        {
+            mfinetune--;
+            finetune = -finetune;
+        } else
+            mfinetune++;
+        
+        period2 = jmpNoteToAmigaPeriod(mnote, mfinetune);
+        
+        mfinetune = finetune & 15;
+        period1 *= (16 - mfinetune);
+        period2 *= mfinetune;
+        
+        res = ((period1 + period2) * 2) >> moctave;
+        
+//fprintf(stderr, "jmpGetAmigaPeriod(%d, %d) = %d\n", note, finetune, res);
+    }
+    else
+    {
+//fprintf(stderr, "jmpGetLinearPeriod(%d, %d) = %d\n", note, finetune, res);
+        //res = ((120 - note) << 6) - (finetune / 2);
+        res = 7680 - (note * 64) - (finetune / 2);
+        if (res < 1) res = 1;
+    }
+
+    return res;
+}
+
+
+static void jmpCSetPitch(JSSPlayer *mp, JSSPlayerChannel *chn, int value)
+{
+    if (value > 0)
+    {
+        if (JMPGETMODFLAGS(mp, jmdfAmigaPeriods))
+        {
+            // Frequency = 8363*1712 / Period
+            chn->cfreq = chn->freq = 14317456 / value;
+        }
+        else
+        {
+            // Frequency = Frequency = 8363*2^((6*12*16*4 - Period) / (12*16*4))
+            chn->cfreq = chn->freq = 8363.0f * pow(2.0f, (4608.0f - (double) value) / 768.0f);
+        }
+
+        JMPSETNDFLAGS(cdfNewFreq);
+    }
+}
+
+
+static void jmpCSetVolume(JSSPlayerChannel *chn, int channel, int volume)
+{
+    (void) channel;
+
+    chn->volume = dmClamp(volume, mpMinVol, mpMaxVol);
+    JMPSETNDFLAGS(cdfNewVolume);
+}
+
+
+static BOOL jmpExecEnvelope(JSSEnvelope *env, JSSPlayerEnvelope *pe, BOOL keyOff)
+{
+    int point;
+
+    if (!pe->exec)
+        return FALSE;
+    
+    // Find current point, if not last point
+    for (point = 0; point < env->npoints - 1; point++)
+    {
+        if (pe->frame < env->points[point + 1].frame)
+            break;
+    }
+
+    if (env->flags & jenvfLooped && pe->frame >= env->points[env->loopE].frame)
+    {
+        point = env->loopS;
+        pe->frame = env->points[env->loopS].frame;
+        pe->value = env->points[point].value;
+    }
+
+    // Check for last point
+    if (pe->frame >= env->points[env->npoints - 1].frame)
+    {
+        point = env->npoints - 1;
+        pe->exec = FALSE;
+        pe->value = env->points[point].value;
+    }
+    else
+    {
+        // Linearly interpolate the value between current and next point
+        JSSEnvelopePoint
+            *ep1 = &env->points[point],
+            *ep2 = &env->points[point + 1];
+
+        int delta = ep2->frame - ep1->frame;
+        if (delta > 0)
+            pe->value = ep1->value + ((ep2->value - ep1->value) * (pe->frame - ep1->frame)) / delta;
+        else
+            pe->value = ep1->value;
+    }
+
+    if (pe->exec)
+    {
+        // The frame counter IS processed even if the envelope is not!
+        if ((env->flags & jenvfSustain) && point == env->sustain &&
+            env->points[point].frame == env->points[env->sustain].frame)
+        {
+            if (keyOff)
+                pe->frame++;
+        } else
+            pe->frame++;
+    }
+        
+    return TRUE;
+}
+
+
+static void jmpProcessExtInstrument(JSSPlayerChannel *chn, int channel)
+{
+    JSSExtInstrument *inst = chn->extInstrument;
+    (void) channel;
+
+    // Get the instrument for envelope data
+    if (!inst) return;
+
+    // Process the autovibrato
+    /*
+       FIXME fix me FIX me!!! todo.
+     */
+
+    if (inst->volumeEnv.flags & jenvfUsed)
+    {
+        // Process the instrument volume fadeout
+        if (chn->keyOff && chn->fadeOutVol > 0 && inst->fadeOut > 0)
+        {
+            int tmp = chn->fadeOutVol - inst->fadeOut;
+            if (tmp < 0) tmp = 0;
+            chn->fadeOutVol = tmp;
+
+            JMPSETNDFLAGS(cdfNewVolume);
+        }
+
+        // Execute the volume envelope
+        if (jmpExecEnvelope(&inst->volumeEnv, &chn->volumeEnv, chn->keyOff))
+            JMPSETNDFLAGS(cdfNewVolume);
+    }
+    else
+    {
+        // If the envelope is not used, set max volume
+        chn->volumeEnv.value = mpMaxVol;
+        chn->fadeOutVol = chn->keyOff ? 0 : mpMaxFadeoutVol;
+        JMPSETNDFLAGS(cdfNewVolume);
+    }
+
+    if (inst->panningEnv.flags & jenvfUsed)
+    {
+        // Process the panning envelope
+        if (jmpExecEnvelope(&inst->panningEnv, &chn->panningEnv, chn->keyOff))
+            JMPSETNDFLAGS(cdfNewPanPos);
+    }
+    else
+    {
+        // If the envelope is not used, set center panning
+        if (chn->panningEnv.value != mpPanCenter)
+        {
+            chn->panningEnv.value = mpPanCenter;
+            JMPSETNDFLAGS(cdfNewPanPos);
+        }
+    }
+}
+
+
+/*
+ * The player
+ */
+JSSPlayer *jmpInit(JSSMixer *pDevice)
+{
+    JSSPlayer *mp;
+    
+    // Allocate a player structure
+    mp = dmMalloc0(sizeof(JSSPlayer));
+    if (mp == NULL)
+        JSSERROR(DMERR_MALLOC, NULL, "Could not allocate memory for player structure.\n");
+
+    // Set variables
+    mp->device = pDevice;
+
+#ifdef JSS_SUP_THREADS
+    mp->mutex = dmCreateMutex();
+#endif
+    
+    return mp;
+}
+
+
+int jmpClose(JSSPlayer * mp)
+{
+    if (mp == NULL)
+        return DMERR_NULLPTR;
+
+    // Stop player
+    jmpStop(mp);
+
+    // Deallocate resources
+#ifdef JSS_SUP_THREADS
+    dmDestroyMutex(mp->mutex);
+#endif
+
+    // Clear structure
+    memset(mp, 0, sizeof(JSSPlayer));
+    dmFree(mp);
+
+    return DMERR_OK;
+}
+
+
+/* Reset the envelopes for given channel.
+ */
+static void jmpResetEnvelope(JSSPlayerEnvelope *env)
+{
+    env->frame = env->value = 0;
+    env->exec = TRUE;
+}
+
+
+/* Clear module player structure
+ */
+void jmpClearChannel(JSSPlayerChannel *chn)
+{
+    memset(chn, 0, sizeof(JSSPlayerChannel));
+
+    chn->note            = jsetNotSet;
+    chn->ninstrument     = jsetNotSet;
+    chn->nextInstrument  = jsetNotSet;
+    chn->panning         = mpPanCenter;
+    chn->panningEnv.value          = mpPanCenter;
+}
+
+
+void jmpClearPlayer(JSSPlayer * mp)
+{
+    int i;
+    assert(mp != NULL);
+    JSS_LOCK(mp);
+
+    // Initialize general variables
+    mp->patternDelay      = 0;
+    mp->newRowSet         = FALSE;
+    mp->newOrderSet       = FALSE;
+    mp->tick              = jsetNotSet;
+    mp->row               = 0;
+    mp->lastPatLoopRow    = 0;
+
+    // Initialize channel data
+    for (i = 0; i < jsetNChannels; i++)
+        jmpClearChannel(&mp->channels[i]);
+    
+    JSS_UNLOCK(mp);
+}
+
+
+/* Set module
+ */
+void jmpSetModule(JSSPlayer * mp, JSSModule * module)
+{
+    assert(mp != NULL);
+    JSS_LOCK(mp);
+
+    jmpStop(mp);
+    jmpClearPlayer(mp);
+
+    mp->module = module;
+
+    JSS_UNLOCK(mp);
+}
+
+
+/* Stop playing
+ */
+void jmpStop(JSSPlayer * mp)
+{
+    assert(mp != NULL);
+    JSS_LOCK(mp);
+
+    if (mp->isPlaying)
+    {
+        jvmRemoveCallback(mp->device);
+        mp->isPlaying = FALSE;
+    }
+    
+    JSS_UNLOCK(mp);
+}
+
+
+/* Resume playing
+ */
+void jmpResume(JSSPlayer * mp)
+{
+    assert(mp != NULL);
+    JSS_LOCK(mp);
+
+    if (!mp->isPlaying)
+    {
+        int result = jvmSetCallback(mp->device, jmpExec, (void *) mp);
+        if (result != DMERR_OK)
+            JSSERROR(result,, "Could not initialize callback for player.\n");
+
+        mp->isPlaying = TRUE;
+    }
+
+    JSS_UNLOCK(mp);
+}
+
+
+/* Sets new order using given value as reference.
+ * Jumps over skip-points and invalid values, loops
+ * to first order if enabled.
+ */
+static void jmpSetNewOrder(JSSPlayer * mp, int order)
+{
+    BOOL orderOK;
+    int pattern;
+    
+    pattern = jsetOrderEnd;
+    mp->order = jsetNotSet;
+    orderOK = FALSE;
+
+    while (!orderOK)
+    {
+        if (order < 0 || order >= mp->module->norders)
+        {
+            jmpStop(mp);
+            orderOK = TRUE;
+        }
+        else
+        {
+            pattern = mp->module->orderList[order];
+            if (pattern == jsetOrderSkip)
+            {
+                order++;
+            }
+            else
+            if (pattern >= mp->module->npatterns || pattern == jsetOrderEnd)
+            {
+                jmpStop(mp);
+                orderOK = TRUE;
+            }
+            else
+            {
+                // All OK
+                orderOK = TRUE;
+                mp->pattern = mp->module->patterns[pattern];
+                mp->npattern = pattern;
+                mp->order = order;
+            }
+        }
+    }
+}
+
+
+/* Set new tempo-value of the player.
+ */
+static void jmpSetTempo(JSSPlayer * mp, int tempo)
+{
+    assert(mp != NULL);
+    JSS_LOCK(mp);
+    assert(mp->device != NULL);
+
+    mp->tempo = tempo;
+    jvmSetCallbackFreq(mp->device, (mp->device->outFreq * 5) / (tempo * 2));
+    JSS_UNLOCK(mp);
+}
+
+
+static void jmpClearChannels(JSSPlayer * mp)
+{
+    int i;
+    assert(mp != NULL);
+    JSS_LOCK(mp);
+    assert(mp->device != NULL);
+    assert(mp->module != NULL);
+
+    for (i = 0; i < mp->module->nchannels; i++)
+        jvmStop(mp->device, i);
+
+    // Initialize channel data
+    for (i = 0; i < jsetNChannels; i++)
+        jmpClearChannel(&mp->channels[i]);
+
+    JSS_UNLOCK(mp);
+}
+
+
+/* Starts playing module from a given ORDER.
+ */
+static int jmpPlayStart(JSSPlayer *mp)
+{
+    int result;
+
+    mp->speed = mp->module->defSpeed;
+    jmpSetTempo(mp, mp->module->defTempo);
+
+    result = jvmSetCallback(mp->device, jmpExec, (void *) mp);
+    if (result != DMERR_OK)
+    {
+        JSSERROR(result, result, "Could not initialize callback for player.\n");
+    }
+
+    mp->isPlaying = TRUE;
+    return DMERR_OK;
+}
+
+
+int jmpChangeOrder(JSSPlayer *mp, int order)
+{
+    assert(mp != NULL);
+
+    JSS_LOCK(mp);
+    assert(mp->module != NULL);
+
+    jmpClearChannels(mp);
+    jmpClearPlayer(mp);
+
+    jmpSetNewOrder(mp, order);
+    if (mp->order == jsetNotSet)
+    {
+        JSS_UNLOCK(mp);
+        JSSERROR(DMERR_NOT_SUPPORTED, DMERR_NOT_SUPPORTED,
+             "Could not start playing from given order #%i\n", order);
+    }
+
+    JSS_UNLOCK(mp);
+    return DMERR_OK;
+}
+
+
+int jmpPlayOrder(JSSPlayer * mp, int order)
+{
+    int result;
+    assert(mp != NULL);
+
+    JSS_LOCK(mp);
+    assert(mp->module != NULL);
+
+    // Stop if already playing
+    jmpStop(mp);
+    jmpClearChannels(mp);
+    jmpClearPlayer(mp);
+
+    // Check starting order
+    jmpSetNewOrder(mp, order);
+    if (mp->order == jsetNotSet)
+    {
+        JSS_UNLOCK(mp);
+        JSSERROR(DMERR_NOT_SUPPORTED, DMERR_NOT_SUPPORTED,
+             "Could not start playing from given order #%i\n", order);
+    }
+
+    if ((result = jmpPlayStart(mp)) != DMERR_OK)
+    {
+        JSS_UNLOCK(mp);
+        return result;
+    }
+
+    JSS_UNLOCK(mp);
+    return DMERR_OK;
+}
+
+
+/* Play given pattern
+ */
+int jmpPlayPattern(JSSPlayer * mp, int pattern)
+{
+    int result;
+    assert(mp != NULL);
+    JSS_LOCK(mp);
+    assert(mp->module != NULL);
+
+    // Stop if already playing
+    jmpStop(mp);
+    jmpClearPlayer(mp);
+
+    mp->npattern = pattern;
+
+    if ((result = jmpPlayStart(mp)) != DMERR_OK)
+    {
+        JSS_UNLOCK(mp);
+        return result;
+    }
+
+    JSS_UNLOCK(mp);
+    return DMERR_OK;
+}
+
+
+/* Set volume for given module channel.
+ */
+static void jmpSetVolume(JSSPlayerChannel * chn, int channel, int volume)
+{
+    (void) channel;
+
+    chn->volume = chn->cvolume = dmClamp(volume, mpMinVol, mpMaxVol);
+    JMPSETNDFLAGS(cdfNewVolume);
+}
+
+#define jmpChangeVolume(Q, Z, X) jmpSetVolume(Q, Z, chn->volume + (X))
+
+
+/* Change the pitch of given channel by ADelta.
+ */
+static void jmpChangePitch(JSSPlayerChannel *chn, int channel, int delta)
+{
+    int value;
+    (void) channel;
+
+    // Calculate new pitch and check it
+    value = chn->pitch + delta;
+    if (value < 0)
+        value = 0;
+
+    chn->pitch = value;
+    JMPSETNDFLAGS(cdfNewPitch);
+}
+
+
+/* Do a note portamento (pitch slide) effect for given module channel.
+ */
+static void jmpDoPortamento(JSSPlayerChannel * chn, int channel)
+{
+    (void) channel;
+
+    // Check for zero parameter
+    if (chn->iLastPortaToNoteParam == 0)
+    {
+        JMPSETNDFLAGS(cdfNewPitch);
+        return;
+    }
+
+    /* Slide the pitch of channel to the destination value
+     * with speed of iLastPortaToNoteParam[] * 4 and stop when it equals.
+     */
+    if (chn->pitch < chn->iLastPortaToNotePitch)
+    {
+        // Increase pitch UP
+        jmpChangePitch(chn, channel, chn->iLastPortaToNoteParam * 4);
+        if (chn->pitch > chn->iLastPortaToNotePitch)
+            chn->pitch = chn->iLastPortaToNotePitch;
+    }
+    else
+    if (chn->pitch > chn->iLastPortaToNotePitch)
+    {
+        // Decrease pitch DOWN
+        jmpChangePitch(chn, channel, -(chn->iLastPortaToNoteParam * 4));
+        if (chn->pitch < chn->iLastPortaToNotePitch)
+            chn->pitch = chn->iLastPortaToNotePitch;
+    }
+}
+
+
+/* Do a tremolo effect for given module channel.
+ */
+static void jmpDoTremolo(JSSPlayerChannel *chn, int channel)
+{
+    (void) channel;
+    
+    if (chn->tremolo.depth != 0 && chn->tremolo.speed != 0)
+    {
+        int delta, tmp = chn->tremolo.pos & 31;
+
+        switch (chn->tremolo.wc & 3)
+        {
+            case 0:
+                delta = jmpSineTab[tmp];
+                break;
+            case 1:
+                tmp <<= 3;
+                delta = (chn->tremolo.pos < 0) ? 255 - tmp : tmp;
+                break;
+            case 2:
+                delta = 255;
+                break;
+            case 3:
+                delta = jmpSineTab[tmp];
+                break;
+        }
+
+        delta = (delta * chn->tremolo.depth) >> 6;
+        jmpCSetVolume(chn, channel, chn->cvolume + (chn->tremolo.pos >= 0 ? delta : -delta));
+
+        chn->tremolo.pos += chn->tremolo.speed;
+        if (chn->tremolo.pos > 31)
+            chn->tremolo.pos -= 64;
+    }
+}
+
+
+/* Do a vibrato effect for given module channel.
+ */
+static void jmpDoVibrato(JSSPlayerChannel *chn, int channel)
+{
+    (void) channel;
+
+    if (chn->vibrato.depth != 0 && chn->vibrato.speed != 0)
+    {
+        int delta, tmp = chn->vibrato.pos & 31;
+
+        switch (chn->vibrato.wc & 3)
+        {
+            case 0:
+                delta = jmpSineTab[tmp];
+                break;
+            case 1:
+                tmp <<= 3;
+                delta = (chn->vibrato.pos < 0) ? 255 - tmp : tmp;
+                break;
+            case 2:
+                delta = 255;
+                break;
+            case 3:
+                delta = jmpSineTab[tmp];
+                break;
+        }
+
+        delta = ((delta * chn->vibrato.depth) >> 7) << 2;
+        chn->freq = chn->cfreq + (chn->vibrato.pos >= 0 ? delta : -delta);
+        JMPSETNDFLAGS(cdfNewFreq);
+
+        chn->vibrato.pos += chn->vibrato.speed;
+        if (chn->vibrato.pos > 31)
+            chn->vibrato.pos -= 64;
+    }
+}
+
+
+/* Do a volume slide effect for given module channel.
+ */
+static void jmpDoVolumeSlide(JSSPlayerChannel * chn, int channel, int param)
+{
+    int paramX, paramY;
+
+    JMPMAKEPARAM(param, paramX, paramY)
+
+    if (paramY == 0)
+        jmpChangeVolume(chn, channel, paramX);
+    if (paramX == 0)
+        jmpChangeVolume(chn, channel, -paramY);
+}
+
+static void jmpTriggerNote(JSSPlayer * mp, JSSPlayerChannel *chn, BOOL newExtInstrument);
+
+
+static void jmpDoMultiRetrigNote(JSSPlayer *mp, JSSPlayerChannel *chn, int channel)
+{
+    if (chn->lastMultiRetrigParamY != 0 &&
+       (mp->tick % chn->lastMultiRetrigParamY) == 0)
+    {
+        BOOL change = TRUE;
+        int volume = chn->volume;
+        switch (chn->lastMultiRetrigParamX)
+        {
+            case 0x1: volume -= 1; break;
+            case 0x2: volume -= 2; break;
+            case 0x3: volume -= 4; break;
+            case 0x4: volume -= 8; break;
+            case 0x5: volume -= 16; break;
+            case 0x6: volume  = (volume * 2) / 3; break;
+            case 0x7: volume /= 2; break;
+
+            case 0x9: volume += 1; break;
+            case 0xA: volume += 2; break;
+            case 0xB: volume += 4; break;
+            case 0xC: volume += 8; break;
+            case 0xD: volume += 16; break;
+            case 0xE: volume  = (volume * 3) / 2; break;
+            case 0xF: volume *= 2; break;
+            default: change = FALSE;
+        }
+        jmpTriggerNote(mp, chn, FALSE);
+        if (change)
+            jmpSetVolume(chn, channel, volume);
+    }
+}
+
+
+/* Execute a pattern loop effect/command for given module channel.
+ *
+ * This routine works for most of the supported formats, as they
+ * use the 'standard' implementation ascending from MOD. However,
+ * here is included also a slightly kludgy implementation of the
+ * FT2 patloop bug.
+ */
+static void jmpDoPatternLoop(JSSPlayer * mp, JSSPlayerChannel *chn, int channel, int paramY)
+{
+    (void) channel;
+
+    // Check what we need to do
+    if (paramY > 0)
+    {
+        // SBx/E6x loops 'x' times
+        if (chn->iPatLoopCount == 1)
+            chn->iPatLoopCount = 0;
+        else
+        {
+            // Check if we need to set the count
+            if (chn->iPatLoopCount == 0)
+                chn->iPatLoopCount = paramY + 1;
+
+            // Loop to specified row
+            chn->iPatLoopCount--;
+            mp->newRow = chn->iPatLoopRow;
+            mp->newRowSet = TRUE;
+        }
+    }
+    else
+    {
+        // SB0/E60 sets the loop start point
+        chn->iPatLoopRow = mp->row;
+
+        // This is here because of the infamous FT2 patloop bug
+        mp->lastPatLoopRow = mp->row;
+    }
+}
+
+
+/* Do arpeggio effect
+ */
+static void jmpDoArpeggio(JSSPlayer * mp, JSSPlayerChannel *chn, int channel, int paramY, int paramX)
+{
+    JSSInstrument *inst = chn->instrument;
+    (void) channel;
+
+    if (inst != NULL)
+    {
+        int tmp = chn->note;
+        if (tmp == jsetNotSet || tmp == jsetNoteOff)
+            return;
+
+        switch (mp->tick & 3)
+        {
+            case 1:
+                tmp += paramX;
+                break;
+            case 2:
+                tmp += paramY;
+                break;
+        }
+
+        tmp = dmClamp(tmp + inst->ERelNote, 0, 119);
+        jmpCSetPitch(mp, chn, jmpGetPeriodFromNote(mp, tmp, inst->EFineTune));
+    }
+}
+
+
+/* Trigger a new note on the given channel.
+ * Separate function used from various places where note
+ * triggering is needed (retrig, multi-retrig, etc.)
+ */
+static void jmpTriggerNote(JSSPlayer * mp, JSSPlayerChannel *chn, BOOL newExtInstrument)
+{
+    if (chn->nextInstrument >= 0 &&
+        chn->nextInstrument < mp->module->nextInstruments &&
+        mp->module->extInstruments[chn->nextInstrument] != NULL)
+    {
+        chn->extInstrument = mp->module->extInstruments[chn->nextInstrument];
+    }
+    else
+    {
+        chn->extInstrument = NULL;
+        chn->instrument    = NULL;
+        chn->ninstrument   = jsetNotSet;
+    }
+
+    if (chn->extInstrument != NULL)
+    {
+        int tmp = chn->extInstrument->sNumForNotes[chn->note];
+
+        if (tmp >= 0 && tmp < mp->module->ninstruments &&
+            mp->module->instruments[tmp] != NULL)
+        {
+            if (chn->ninstrument != tmp)
+                JMPSETNDFLAGS(cdfNewInstr);
+            else
+                JMPSETNDFLAGS(cdfPlay);
+
+            chn->ninstrument = tmp;
+            chn->instrument  = mp->module->instruments[tmp];
+
+            if (newExtInstrument)
+            {
+                chn->volume  = chn->instrument->volume;
+                chn->panning = chn->instrument->EPanning;
+                JMPSETNDFLAGS(cdfNewPanPos | cdfNewVolume);
+            }
+        }
+    }
+
+    if (chn->instrument != NULL)
+    {
+        int tmp;
+        JSSInstrument *inst = chn->instrument;
+
+        // Save old pitch for later use
+        chn->oldPitch = chn->pitch;
+
+        chn->position = 0;
+
+        // Compute new pitch
+        tmp = dmClamp(chn->note + inst->ERelNote, 0, 119);
+        chn->pitch = jmpGetPeriodFromNote(mp, tmp, inst->EFineTune);
+        JMPSETNDFLAGS(cdfNewPitch | cdfPlay | cdfNewPos);
+    }
+    else
+    {
+        chn->volume = 0;
+        chn->panning = jchPanMiddle;
+        JMPSETNDFLAGS(cdfStop | cdfNewPanPos | cdfNewVolume);
+    }
+}
+
+
+/*
+ * Process a new pattern row
+ */
+static void jmpProcessNewRow(JSSPlayer * mp, int channel)
+{
+    JSSNote *currNote;
+    BOOL newNote = FALSE, newExtInstrument = FALSE, volumePortaSet = FALSE;
+    char effect;
+    int param, paramX, paramY;
+    JSSPlayerChannel *chn = &(mp->channels[channel]);
+
+    JMPGETNOTE(currNote, mp->row, channel);
+
+    // Check for a new note/keyoff here
+    if (currNote->note == jsetNoteOff)
+        chn->keyOff = TRUE;
+    else
+    if (currNote->note >= 0 && currNote->note <= 96)
+    {
+        newNote = TRUE;
+        chn->oldNote = chn->note;
+        chn->note = currNote->note;
+        chn->keyOff = FALSE;
+    }
+
+    // Check for new instrument
+    if (currNote->instrument != jsetNotSet)
+    {
+        /* Envelopes and ext.instrument fadeout are initialized always if
+         * new instrument is set, even if the instrument does not exist.
+         */
+        jmpResetEnvelope(&chn->volumeEnv);
+        jmpResetEnvelope(&chn->panningEnv);
+        chn->keyOff = FALSE;
+        chn->fadeOutVol = mpMaxFadeoutVol;
+
+        JMPSETNDFLAGS(cdfNewPanPos | cdfPlay | cdfNewVolume);
+
+        // We save the instrument number here for later use
+        chn->nextInstrument = currNote->instrument;
+        newExtInstrument = TRUE;
+    }
+
+    if (newNote)
+    {
+        jmpTriggerNote(mp, chn, newExtInstrument);
+    }
+
+    // Process the volume column
+    JMPMAKEPARAM(currNote->volume, paramX, paramY);
+
+    switch (paramX)
+    {
+        case 0x0:
+        case 0x1:
+        case 0x2:
+        case 0x3:
+        case 0x4:
+            jmpSetVolume(chn, channel, currNote->volume);
+            break;
+
+        case 0x7:        // Dx = Fine Volumeslide Down : IMPL.VERIFIED
+            jmpChangeVolume(chn, channel, -paramY);
+            break;
+
+        case 0x8:        // Ux = Fine Volumeslide Up : IMPL.VERIFIED
+            jmpChangeVolume(chn, channel, paramY);
+            break;
+
+        case 0x9:        // Sx = Set vibrato speed : IMPL.VERIFIED
+            chn->vibrato.speed = paramY;
+            break;
+
+        case 0xa:        // Vx = Vibrato : IMPL.VERIFIED
+            if (paramY)
+                chn->vibrato.depth = paramY;
+            break;
+
+        case 0xe:        // Mx = Porta To Note : IMPL.VERIFIED
+            if (paramY)
+                chn->iLastPortaToNoteParam = paramY * 16;
+
+            if (currNote->note != jsetNotSet && currNote->note != jsetNoteOff)
+            {
+                chn->lastPortaToNoteNote = chn->note;
+                chn->iLastPortaToNotePitch = chn->pitch;
+                chn->pitch = chn->oldPitch;
+                chn->note = chn->oldNote;
+                JMPUNSETNDFLAGS(cdfNewPitch | cdfPlay);
+                volumePortaSet = TRUE;
+            }
+            break;
+    }
+
+    // ...And finally process the Normal effects
+    if (currNote->effect == jsetNotSet)
+        return;
+
+    param = currNote->param;
+    JMPMAKEPARAM(param, paramX, paramY);
+    JMPGETEFFECT(effect, currNote->effect);
+
+    switch (effect)
+    {
+        case '0':        // 0xy = Arpeggio
+            jmpDoArpeggio(mp, chn, channel, paramX, paramY);
+            break;
+
+        case 'W':        // Used widely in demo-music as MIDAS Sound System sync-command
+        case 'Q':        // SoundTracker/OpenCP: Qxx = Set LP filter resonance
+        case 'Z':        // SoundTracker/OpenCP: Zxx = Set LP filter cutoff freq
+            break;
+
+        case '1':
+        case '2':        // 1xy = Portamento Up, 2xy = Portamento Down : IMPL.VERIFIED
+            if (param)
+                chn->iLastPortaParam = param;
+            break;
+
+        case '3':        // 3xy = Porta To Note
+            if (volumePortaSet)
+                break;
+
+            if (param)
+                chn->iLastPortaToNoteParam = param;
+
+            if (currNote->note != jsetNotSet && currNote->note != jsetNoteOff)
+            {
+                chn->lastPortaToNoteNote = chn->note;
+                chn->iLastPortaToNotePitch = chn->pitch;
+                chn->pitch = chn->oldPitch;
+                chn->note = chn->oldNote;
+                JMPUNSETNDFLAGS(cdfNewPitch | cdfPlay);
+            }
+            break;
+
+        case '4':        // 4xy = Vibrato : IMPL.VERIFIED
+            if (paramX)
+                chn->vibrato.speed = paramX;
+
+            if (paramY)
+                chn->vibrato.depth = paramY;
+
+            if ((chn->vibrato.wc & 4) == 0)
+                chn->vibrato.pos = 0;
+            break;
+
+        case '5':        // 5xy = Portamento + Volume Slide
+        case '6':        // 6xy = Vibrato + Volume slide
+            if (param)
+                chn->iLastVolSlideParam = param;
+            break;
+
+        case '7':        // 7xy = Tremolo
+            if (paramX)
+                chn->tremolo.speed = paramX;
+
+            if (paramY)
+                chn->tremolo.depth = paramY;
+
+            if ((chn->tremolo.wc & 4) == 0)
+                chn->tremolo.pos = 0;
+            break;
+
+        case '8':        // 8xx = Set Panning
+            chn->panning = param;
+            JMPSETNDFLAGS(cdfNewPanPos);
+            break;
+
+        case '9':        // 9xx = Set Sample Offset : IMPL.VERIFIED
+            if (param != 0)
+                chn->lastSampleOffsetParam = param;
+            if (chn->newDataFlags & cdfNewPitch && chn->instrument != NULL)
+            {
+                int pos = chn->lastSampleOffsetParam * 0x100,
+                    end = (chn->instrument->flags & jsfLooped) ?
+                          chn->instrument->loopE : chn->instrument->size;
+                if (pos <= end)
+                {
+                    chn->position = pos;
+                    JMPSETNDFLAGS(cdfNewPos);
+                }
+                else
+                {
+                    JMPSETNDFLAGS(cdfStop);
+                }
+            }
+            break;
+
+        case 'A':        // Axy = Volume Slide : IMPL.VERIFIED
+            if (param)
+                chn->iLastVolSlideParam = param;
+            break;
+
+        case 'B':        // Bxx = Pattern Jump : IMPL.VERIFIED
+            mp->newOrder = param;
+            mp->newOrderSet = TRUE;
+            mp->jumpFlag = TRUE;
+            mp->lastPatLoopRow = 0;
+            break;
+
+        case 'C':        // Cxx = Set Volume : IMPL.VERIFIED
+            jmpSetVolume(chn, channel, param);
+            break;
+
+        case 'D':        // Dxx = Pattern Break : IMPL.VERIFIED
+            // Compute the new row
+            mp->newRow = (paramX * 10) + paramY;
+            if (mp->newRow >= mp->pattern->nrows)
+                mp->newRow = 0;
+
+            mp->newRowSet = TRUE;
+
+            // Now we do some tricky tests
+            if (!mp->breakFlag && !mp->jumpFlag)
+            {
+                mp->newOrder = mp->order + 1;
+                mp->newOrderSet = TRUE;
+            }
+
+            mp->breakFlag = TRUE;
+            break;
+
+        case 'E':         // Exy = Special Effects
+            switch (paramX)
+            {
+                case 0x00:    // E0x - Set filter (NOT SUPPORTED)
+                    JMPDEBUG("Set Filter used, UNSUPPORTED");
+                    break;
+
+                case 0x01:    // E1x - Fine Portamento Up
+                    if (paramY)
+                        chn->iCLastFinePortamentoUpParam = paramY;
+
+                    jmpChangePitch(chn, channel, -(chn->iCLastFinePortamentoUpParam * 4));
+                    break;
+
+                case 0x02:    // E2x - Fine Portamento Down
+                    if (paramY)
+                        chn->iCLastFinePortamentoDownParam = paramY;
+
+                    jmpChangePitch(chn, channel, (chn->iCLastFinePortamentoDownParam * 4));
+                    break;
+
+                case 0x03:    // E3x - Glissando Control (NOT SUPPORTED)
+                    break;
+
+                case 0x04:    // E4x - Set Vibrato waveform
+                    chn->vibrato.wc = paramY;
+                    break;
+
+                case 0x05:    // E5x - Set Finetune
+                    JMPDEBUG("Set Finetune used, UNIMPLEMENTED");
+                    break;
+
+                case 0x06:    // E6x - Set Pattern Loop
+                    jmpDoPatternLoop(mp, chn, channel, paramY);
+                    break;
+
+                case 0x07:    // E7x - Set Tremolo waveform
+                    chn->tremolo.wc = paramY;
+                    break;
+
+                case 0x08:    // E8x - Set Pan Position
+                    chn->panning = (paramY * 16);
+                    JMPSETNDFLAGS(cdfNewPanPos);
+                    break;
+
+                case 0x09:    // E9x - Retrig note
+                    if (mp->tick == paramY)
+                        jmpTriggerNote(mp, chn, FALSE);
+                    break;
+
+                case 0x0a:    // EAx - Fine Volumeslide Up
+                    if (paramY)
+                        chn->iCLastFineVolumeslideUpParam = paramY;
+
+                    jmpChangeVolume(chn, channel, chn->iCLastFineVolumeslideUpParam);
+                    break;
+
+                case 0x0b:    // EBx - Fine Volumeslide Down
+                    if (paramY)
+                        chn->iCLastFineVolumeslideDownParam = paramY;
+                    jmpChangeVolume(chn, channel, -(chn->iCLastFineVolumeslideDownParam));
+                    break;
+
+                case 0x0c:    // ECx - Set Note Cut (NOT PROCESSED IN TICK0)
+                    break;
+
+                case 0x0d:    // EDx - Set Note Delay : IMPL.VERIFIED
+                    if (paramY > 0)
+                    {
+                        // Save the ND-flags, then clear
+                        chn->iSaveNDFlags = chn->newDataFlags;
+                        chn->newDataFlags = 0;
+                        // TODO .. does this only affect NOTE or also instrument?
+                    }
+                    break;
+
+                case 0x0e:    // EEx - Set Pattern Delay : IMPL.VERIFIED
+                    mp->patternDelay = paramY;
+                    break;
+
+                case 0x0f:    // EFx - Invert Loop (NOT SUPPORTED)
+                    JMPDEBUG("Invert Loop used, UNSUPPORTED");
+                    break;
+
+                default:
+                    JMPDEBUG("Unsupported special command used");
+            }
+            break;
+
+        case 'F':        // Fxy = Set Speed / Tempo : IMPL.VERIFIED
+            if (param > 0)
+            {
+                if (param < 0x20)
+                    mp->speed = param;
+                else
+                    jmpSetTempo(mp, param);
+            }
+            break;
+
+        case 'G':        // Gxx = Global Volume
+            mp->globalVol = param;
+            JMPSETNDFLAGS(cdfNewGlobalVol);
+            break;
+
+
+        case 'H':        // Hxx = Global Volume Slide
+            JMPDEBUG("Global Volume Slide used, UNIMPLEMENTED");
+            break;
+
+        case 'K':        // Kxx = Key-off (Same as key-off note)
+            chn->keyOff = TRUE;
+            break;
+
+        case 'L':        // Lxx = Set Envelope Position
+            JMPDEBUG("Set Envelope Position used, NOT verified with FT2");
+            chn->panningEnv.frame = param;
+            chn->volumeEnv.frame = param;
+            chn->panningEnv.exec = TRUE;
+            chn->volumeEnv.exec = TRUE;
+            break;
+
+        case 'R':        // Rxy = Multi Retrig note
+            if (paramX != 0)
+                chn->lastMultiRetrigParamX = paramX;
+            if (paramY != 0)
+                chn->lastMultiRetrigParamY = paramY;
+            break;
+
+        case 'T':        // Txy = Tremor
+            if (param)
+                chn->iLastTremorParam = param;
+            break;
+
+        case 'X':        // Xxy = Extra Fine Portamento
+            switch (paramX)
+            {
+                case 0x01:    // X1y - Extra Fine Portamento Up
+                    if (paramY)
+                        chn->iCLastExtraFinePortamentoUpParam = paramY;
+
+                    jmpChangePitch(chn, channel, - chn->iCLastExtraFinePortamentoUpParam);
+                    break;
+
+                case 0x02:    // X2y - Extra Fine Portamento Down
+                    if (paramY)
+                        chn->iCLastExtraFinePortamentoDownParam = paramY;
+
+                    jmpChangePitch(chn, channel, chn->iCLastExtraFinePortamentoUpParam);
+                    break;
+
+                default:
+                    JMPDEBUG("Unsupported value in Extra Fine Portamento command!");
+                    break;
+            }
+            break;
+
+        default:
+            JMPDEBUG("Unsupported effect");
+            break;
+    }
+}
+
+
+static void jmpProcessEffects(JSSPlayer * mp, int channel)
+{
+    JSSPlayerChannel *chn = &(mp->channels[channel]);
+    JSSNote *currNote;
+    int param, paramX, paramY, tmp;
+    char effect;
+
+    // Process the volume column effects
+    JMPGETNOTE(currNote, mp->row, channel);
+    JMPMAKEPARAM(currNote->volume, paramX, paramY);
+
+    switch (paramX)
+    {
+        case 0x05: // -x = Volumeslide Down : IMPL.VERIFIED
+            jmpChangeVolume(chn, channel, -paramY);
+            break;
+
+        case 0x06: // +x = Volumeslide Down : IMPL.VERIFIED
+            jmpChangeVolume(chn, channel, paramY);
+            break;
+
+        case 0x0a: // Vx = Vibrato : IMPL.VERIFIED
+            jmpDoVibrato(chn, channel);
+            break;
+
+        case 0x0e: // Mx = Porta To Note : IMPL.VERIFIED
+            jmpDoPortamento(chn, channel);
+            break;
+    }
+
+    // ...And finally process the Normal effects
+    if (currNote->effect == jsetNotSet)
+        return;
+    
+    param = currNote->param;
+    JMPMAKEPARAM(param, paramX, paramY);
+    JMPGETEFFECT(effect, currNote->effect);
+
+    switch (effect)
+    {
+        case '0': // 0xy = Arpeggio
+            jmpDoArpeggio(mp, chn, channel, paramX, paramY);
+            break;
+
+        case '1': // 1xy = Portamento Up
+            if (chn->iLastPortaParam > 0)
+                jmpChangePitch(chn, channel, -(chn->iLastPortaParam * 4));
+            break;
+
+        case '2': // 2xy = Portamento Down
+            if (chn->iLastPortaParam > 0)
+                jmpChangePitch(chn, channel, (chn->iLastPortaParam * 4));
+            break;
+
+        case '3': // 3xy = Porta To Note
+            jmpDoPortamento(chn, channel);
+            break;
+
+        case '4': // 4xy = Vibrato
+            jmpDoVibrato(chn, channel);
+            break;
+
+        case '5': // 5xy = Portamento + Volume Slide
+            jmpDoPortamento(chn, channel);
+            jmpDoVolumeSlide(chn, channel, chn->iLastVolSlideParam);
+            break;
+
+        case '6': // 6xy = Vibrato + Volume Slide
+            jmpDoVibrato(chn, channel);
+            jmpDoVolumeSlide(chn, channel, chn->iLastVolSlideParam);
+            break;
+
+        case '7': // 7xy = Tremolo
+            jmpDoTremolo(chn, channel);
+            break;
+
+        case 'A': // Axy = Volume slide
+            jmpDoVolumeSlide(chn, channel, chn->iLastVolSlideParam);
+            break;
+
+        case 'E': // Exy = Special Effects
+            switch (paramX)
+            {
+                case 0x09:    // E9x - Retrig note
+                    if (mp->tick == paramY)
+                        jmpTriggerNote(mp, chn, FALSE);
+                    break;
+
+                case 0x0c: // ECx - Set Note Cut
+                    if (mp->tick == paramY)
+                        jmpSetVolume(chn, channel, jsetMinVol);
+                    break;
+
+                case 0x0d: // EDx - Set Note Delay
+                    if (mp->tick == paramY)
+                        chn->newDataFlags = chn->iSaveNDFlags;
+                    break;
+            }
+            break;
+
+        case 'R':        // Rxy = Multi Retrig note
+            jmpDoMultiRetrigNote(mp, chn, channel);
+            break;
+
+        case 'T': // Txy = Tremor
+            JMPMAKEPARAM(chn->iLastTremorParam, paramX, paramY)
+            paramX++;
+            paramY++;
+            tmp = chn->iTremorCount % (paramX + paramY);
+
+            if (tmp < paramX)
+                jmpCSetVolume(chn, channel, chn->cvolume);
+            else
+                jmpCSetVolume(chn, channel, jsetMinVol);
+
+            chn->iTremorCount = tmp + 1;
+            break;
+    }
+}
+
+
+/* This is the main processing callback-loop of a module player.
+ * It processes the ticks, calling the needed jmpProcessNewRow()
+ * and jmpProcessEffects() methods for processing the module playing.
+ */
+void jmpExec(void *pDEV, void *pMP)
+{
+    JSSPlayer *mp;
+    JSSMixer *dev;
+    int channel;
+
+    // Check some things via assert()
+    mp = (JSSPlayer *) pMP;
+    JSS_LOCK(mp);
+
+    dev = (JSSMixer *) pDEV;
+
+    // Check if we are playing
+    if (!mp->isPlaying)
+        goto out;
+
+    // Clear channel new data flags
+    mp->jumpFlag = FALSE;
+    mp->breakFlag = FALSE;
+
+    for (channel = 0; channel < jsetNChannels; channel++)
+        mp->channels[channel].newDataFlags = 0;
+
+//fprintf(stderr, "1: tick=%d, order=%d, iPattern=%d, row=%d\n", mp->tick, mp->order, mp->npattern, mp->row);
+
+    // Check for init-tick
+    if (mp->tick == jsetNotSet)
+    {
+        // Initialize pattern
+        mp->newRow = 0;
+        mp->newRowSet = TRUE;
+        mp->tick = mp->speed;
+        mp->patternDelay = 0;
+    }
+
+//fprintf(stderr, "2: tick=%d, order=%d, iPattern=%d, row=%d\n", mp->tick, mp->order, mp->npattern, mp->row);
+
+    // Check if we are playing
+    if (!mp->isPlaying)
+        goto out;
+
+    assert(mp->pattern);
+
+    // Update the tick
+    mp->tick++;
+    if (mp->tick >= mp->speed)
+    {
+        // Re-init tick counter
+        mp->tick = 0;
+
+        // Check pattern delay
+        if (mp->patternDelay > 0)
+            mp->patternDelay--;
+        else
+        {
+            // New pattern row
+            if (mp->newRowSet)
+            {
+                mp->row = mp->newRow;
+                mp->newRowSet = FALSE;
+            } else
+                mp->row++;
+
+            // Check for end of pattern
+            if (mp->row >= mp->pattern->nrows)
+            {
+                // Go to next order
+                if (mp->order != jsetNotSet)
+                    jmpSetNewOrder(mp, mp->order + 1);
+                else
+                    mp->isPlaying = FALSE;
+                
+                // Check for FT2 quirks
+                if (JMPGETMODFLAGS(mp, jmdfFT2Replay))
+                    mp->row = mp->lastPatLoopRow;
+                else
+                    mp->row = 0;
+            }
+
+            if (!mp->isPlaying)
+                goto out;
+
+            // Check current order
+            if (mp->newOrderSet)
+            {
+                jmpSetNewOrder(mp, mp->newOrder);
+                mp->newOrderSet = FALSE;
+            }
+
+//fprintf(stderr, "3: tick=%d, order=%d, iPattern=%d, row=%d\n", mp->tick, mp->order, mp->npattern, mp->row);
+
+            if (!mp->isPlaying)
+                goto out;
+
+            // TICK #0: Process new row
+            for (channel = 0; channel < mp->module->nchannels; channel++)
+                jmpProcessNewRow(mp, channel);
+        } // patternDelay
+    } // tick
+    else
+    {
+        // Implement FT2's pattern delay-effect: don't update effects while on patdelay
+        if (!JMPGETMODFLAGS(mp, jmdfFT2Replay) ||
+            (JMPGETMODFLAGS(mp, jmdfFT2Replay) && mp->patternDelay <= 0))
+        {
+            // TICK n: Process the effects
+            for (channel = 0; channel < mp->module->nchannels; channel++)
+                jmpProcessEffects(mp, channel);
+        }
+    }
+
+    // Check if playing has stopped
+    if (!mp->isPlaying)
+        goto out;
+
+    // Update player data to audio device/mixer
+    for (channel = 0; channel < mp->module->nchannels; channel++)
+    {
+        JSSPlayerChannel *chn = &mp->channels[channel];
+
+        // Process extended instruments
+        jmpProcessExtInstrument(chn, channel);
+        
+        // Check NDFlags and update channel data
+        int flags = chn->newDataFlags;
+        if (!flags)
+            continue;
+
+        // Check if we stop?
+        if (flags & cdfStop)
+        {
+            jvmStop(mp->device, channel);
+        }
+        else
+        {
+            // No, handle other flags
+            if (flags & cdfNewInstr)
+            {
+                JSSInstrument *instr = chn->instrument;
+                if (instr != NULL)
+                {
+                    jvmSetSample(mp->device, channel,
+                        instr->data, instr->size,
+                        instr->loopS, instr->loopE,
+                        instr->flags);
+                }
+            }
+
+            if (flags & cdfPlay)
+            {
+                jvmReset(mp->device, channel);
+                jvmPlay(mp->device, channel);
+            }
+
+            if (flags & cdfNewPitch)
+                jmpCSetPitch(mp, chn, chn->pitch);
+                
+            if (flags & (cdfNewFreq | cdfNewPitch))
+                jvmSetFreq(mp->device, channel, chn->freq);
+
+            if (flags & cdfNewPos)
+                jvmSetPos(mp->device, channel, chn->position);
+
+            if (flags & cdfNewVolume)
+            {
+                BOOL init = flags & (cdfNewInstr | cdfPlay);
+                jvmSetVolumeRamp(mp->device, channel,
+                    init ? 0 : jvmGetVolume(mp->device, channel),
+                    (chn->fadeOutVol * chn->volumeEnv.value * chn->volume) / (16 * 65536),
+                    init ? 5 : 0);
+            }
+
+            if (flags & cdfNewPanPos)
+            {
+                jvmSetPanRamp(mp->device, channel,
+                    jvmGetPan(mp->device, channel),
+                    chn->panning + (((chn->panningEnv.value - 32) * (128 - abs(chn->panning - 128))) / 32),
+                    0);
+            }
+
+            if (flags & cdfNewGlobalVol)
+                jvmSetGlobalVol(mp->device, mp->globalVol);
+        }
+    }
+
+out:
+    JSS_UNLOCK(mp);
+}