view minijss/jssplr.c @ 2298:b5abfff07ca9

Add new DMGrowBuf helper functions dmGrowBufCopyOffsSize() and dmGrowBufConstCopyOffsSize().
author Matti Hamalainen <ccr@tnsp.org>
date Thu, 04 Jul 2019 10:54:16 +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);
}