view minijss/jssplr.c @ 2208:90ec1ec89c56

Revamp the palette handling in lib64gfx somewhat, add helper functions to lib64util for handling external palette file options and add support for specifying one of the "internal" palettes or external (.act) palette file to gfxconv and 64vw.
author Matti Hamalainen <ccr@tnsp.org>
date Fri, 14 Jun 2019 05:01:12 +0300
parents 3b71aa1ef915
children 69a5af2eb1ea
line wrap: on
line source

/*
 * miniJSS - Module playing routines
 * Programmed and designed by Matti 'ccr' Hamalainen
 * (C) Copyright 2006-2015 Tecnic Software productions (TNSP)
 */
#include "jssplr.h"

#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);
            //chn->cfreq = chn->freq = 8363 * (1 << ((4608 - value) / 768));
        }

        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
    dmMemset(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)
{
    dmMemset(chn, 0, sizeof(JSSPlayerChannel));

    chn->note            = jsetNotSet;
    chn->ninstrument     = jsetNotSet;
    chn->nextInstrument  = jsetNotSet;
    chn->panning         = mpPanCenter;
    chn->panningEnv.value          = mpPanCenter;
}


void jmpClearPlayer(JSSPlayer * mp)
{
    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 (int 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;
    mp->order = jsetNotSet;
    orderOK = FALSE;

    while (!orderOK)
    {
        if (order < 0 || order >= mp->module->norders)
        {
            jmpStop(mp);
            orderOK = TRUE;
        }
        else
        {
            int 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)
{
    assert(mp != NULL);
    JSS_LOCK(mp);
    assert(mp->device != NULL);
    assert(mp->module != NULL);

    for (int i = 0; i < mp->module->nchannels; i++)
        jvmStop(mp->device, i);

    // Initialize channel data
    for (int 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:
            default:
                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:
            default:
                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;

    // Check some things via assert()
    mp = (JSSPlayer *) pMP;
    JSS_LOCK(mp);

    (void) pDEV;
//    JSSMixer *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 (int 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 (int 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 (int 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 (int 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);
}