view jssplr.c @ 96:6bf5220fa47e

Urgh .. use memset to silence some bogus GCC warnings about using potentially uninitialized values, while that will not actually be possible. In any case, it is annoying.
author Matti Hamalainen <ccr@tnsp.org>
date Tue, 02 Oct 2012 18:52:28 +0300
parents a33e47232161
children ff0fe1d1ab3d
line wrap: on
line source

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

// FIXME!! FIX ME!
#include <math.h>

/* Miscellaneous tables
 */
#define jmpNSineTable    (256)
static int *jmpSineTable = NULL;


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
 */
int jmpNoteToAmigaPeriod(int note, int finetune)
{
    int tmp = (note + finetune + 8);
    if (tmp < 0) tmp = 0; else
    if (tmp > 103) tmp = 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, int channel, int value)
{
    assert(mp != NULL);
    assert(mp->device != NULL);

    if (value > 0)
    {
        if (JMPGETMODFLAGS(mp, jmdfAmigaPeriods))
        {
            // Frequency = 8363*1712 / Period
//fprintf(stderr, "jmpCSetPitch::AMIGA(%d, %d)\n", channel, value);
            jvmSetFreq(mp->device, channel, 14317456.0f / (double) value);
        }
        else
        {
            // Frequency = Frequency = 8363*2^((6*12*16*4 - Period) / (12*16*4))
//fprintf(stderr, "jmpCSetPitch::Linear(%d, %d)\n", channel, value);
            jvmSetFreq(mp->device, channel,
                8363.0f * pow(2.0f, (4608.0f - (double) value) / 768.0f));
        }
    }
}


static void jmpCSetVolume(JSSPlayer * mp, JSSPlayerChannel *chn, int channel, int volume)
{
    assert(mp != NULL);
    assert(mp->device != NULL);

    if (volume < mpMinVol) volume = mpMinVol; else
    if (volume > mpMaxVol) volume = mpMaxVol;

//fprintf(stderr, "chn %d: vol=%d, fad=%d, env=%d\n", channel, volume, chn->iCFadeOutVol, chn->iCVolEnv);

    jvmSetVolume(mp->device, channel,
        (chn->iCFadeOutVol * chn->iCVolEnv * volume) / (16 * 65536));
}


static void jmpCSetPanning(JSSPlayer * mp, JSSPlayerChannel *chn, int channel, int panning)
{
    assert(mp != NULL);
    assert(mp->device != NULL);

    jvmSetPan(mp->device, channel,
        panning + (((chn->iCPanEnv - 32) * (128 - abs(panning - 128))) / 32));
}


static void jmpCSetPosition(JSSPlayer * mp, int channel, int value)
{
    assert(mp != NULL);
    assert(mp->device != NULL);

    jvmSetPos(mp->device, channel, value);
}


static int jmpFindEnvPoint(JSSEnvelope * env, const int pos)
{
    int i;
    
    for (i = 0; i < env->npoints - 1; i++)
    {
        if (env->points[i].frame <= pos &&
            env->points[i + 1].frame >= pos)
            return i;
    }

    return -1;
}


static void jmpExecEnvelope(JSSEnvelope * env, BOOL keyOff, int * frames, BOOL * doExec, int * result)
{
    int currPoint, delta;
    JSSEnvelopePoint *ipf1, *ipf2;

    // OK, find the current point based on frame
    currPoint = jmpFindEnvPoint(env, *frames);

    // Check if the envelope has ended
    if (currPoint < 0 && (env->flags & jenvfLooped) == 0)
    {
        *doExec = FALSE;
        return;
    }

    // Do the envelope looping here, if needed
    if ((env->flags & jenvfLooped) && *frames >= env->points[env->loopE].frame)
    {
        currPoint = env->loopS;
        *frames = env->points[currPoint].frame;
    }

    // If the current point is OK, then process the envelope
    if (currPoint >= 0)
    {
        // Linearly interpolate the value for given frame
        ipf1 = &env->points[currPoint];
        ipf2 = &env->points[currPoint + 1];

        delta = (ipf2->frame - ipf1->frame);
        if (delta > 0)
            *result = ipf1->value + ((ipf2->value - ipf1->value) * (*frames - ipf1->frame)) / delta;
        else
            *result = ipf1->value;
    }

    // The frame counter IS processed even if the envelope is not!
    if ((env->flags & jenvfSustain) && currPoint == env->sustain &&
        env->points[currPoint].frame == env->points[env->sustain].frame) {
        if (keyOff) (*frames)++;
    } else
        (*frames)++;
}


static void jmpProcessExtInstrument(JSSPlayerChannel *chn, int channel)
{
    JSSExtInstrument *inst = chn->iCExtInstrument;
    (void) channel;

    // Get the instrument for envelope data
    if (!inst) return;

    // Process the autovibrato
    /*
       FIXME fix me FIX me!!! todo.
     */

    // Process the volume envelope
    if (inst->volumeEnv.flags & jenvfUsed)
    {
        // Process the instrument volume fadeout
        if (chn->iCKeyOff && chn->iCFadeOutVol > 0 && inst->fadeOut > 0)
        {
            int tmp = chn->iCFadeOutVol - inst->fadeOut;
            if (tmp < 0) tmp = 0;
            chn->iCFadeOutVol = tmp;

            JMPSETNDFLAGS(cdfNewVolume);
        }

        if (chn->iCVolEnv_Exec)
        {
            // Execute the volume envelope
            jmpExecEnvelope(&(inst->volumeEnv),
                chn->iCKeyOff,
                &(chn->iCVolEnv_Frames), &(chn->iCVolEnv_Exec),
                &(chn->iCVolEnv));

            JMPSETNDFLAGS(cdfNewVolume);
        }
    }
    else
    {
        // If the envelope is not used, set max volume
        if (chn->iCVolEnv != mpMaxVol)
        {
            chn->iCVolEnv = mpMaxVol;
            chn->iCFadeOutVol = mpMaxFadeoutVol;
            JMPSETNDFLAGS(cdfNewVolume);
        }
    }

    // Process the panning envelope
    if (inst->panningEnv.flags & jenvfUsed)
    {
        if (chn->iCPanEnv_Exec)
        {
            // Execute the panning envelope
            jmpExecEnvelope(&(inst->panningEnv), chn->iCKeyOff,
                    &(chn->iCPanEnv_Frames), &(chn->iCPanEnv_Exec),
                    &(chn->iCPanEnv));

            JMPSETNDFLAGS(cdfNewPanPos);
        }
    }
    else
    {
        // If the envelope is not used, set center panning
        if (chn->iCPanEnv != mpPanCenter)
        {
            chn->iCPanEnv = mpPanCenter;
            JMPSETNDFLAGS(cdfNewPanPos);
        }
    }
}


/*
 * The player
 */
JSSPlayer *jmpInit(JSSMixer *pDevice)
{
    JSSPlayer *mp;
    
    // Initialize global tables
    if (jmpSineTable == NULL)
    {
        int i;
        if ((jmpSineTable = dmMalloc(jmpNSineTable * sizeof(int))) == NULL)
            JSSERROR(DMERR_MALLOC, NULL, "Could not allocate memory for sinus table.\n");
    
        for (i = 0; i < 256; i++)
        {
            float f = ((float) i * M_PI * 2.0f) / 256.0f;
            jmpSineTable[i] = (int) (sin(f) * 2048.0f);
        }
    }

    // Allocate a player structure
    mp = dmMalloc0(sizeof(JSSPlayer));
    if (!mp)
        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 jmpResetEnvelopes(JSSPlayerChannel *chn)
{
    chn->iCPanEnv_Frames = chn->iCVolEnv_Frames = 0;
    chn->iCPanEnv_Exec   = chn->iCVolEnv_Exec   = TRUE;
}


/* Clear module player structure
 */
void jmpClearChannel(JSSPlayerChannel *chn)
{
    memset(chn, 0, sizeof(JSSPlayerChannel));

    chn->iCNote            = jsetNotSet;
    chn->iCInstrumentN     = jsetNotSet;
    chn->iCExtInstrumentN  = jsetNotSet;
    chn->iCPanning         = mpPanCenter;
    chn->iCPanEnv          = 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.
 */
void jmpSetTempo(JSSPlayer * mp, int tempo)
{
    assert(mp != NULL);
    JSS_LOCK(mp);
    assert(mp->device != NULL);

    mp->tempo = tempo;
    jvmSetCallbackFreq(mp->device, (tempo * 2) / 5);
    JSS_UNLOCK(mp);
}


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);
        jvmClear(mp->device, i);
    }

    JSS_UNLOCK(mp);
}


/* Starts playing module from a given ORDER.
 */
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);

    // Check starting order
    if (order < 0 || order >= mp->module->norders)
    {
        JSS_UNLOCK(mp);
        JSSERROR(DMERR_INVALID_ARGS, DMERR_INVALID_ARGS, "Invalid playing startorder given.\n");
    }

    // Initialize playing
    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);
    }

    mp->speed = mp->module->defSpeed;
    jmpSetTempo(mp, mp->module->defTempo);

    // Set callback
    result = jvmSetCallback(mp->device, jmpExec, (void *) mp);
    if (result != DMERR_OK)
    {
        JSS_UNLOCK(mp);
        JSSERROR(result, result, "Could not initialize callback for player.\n");
    }

    mp->isPlaying = TRUE;

    JSS_UNLOCK(mp);
    return 0;
}


/* 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);
    jmpClearChannels(mp);

    // Initialize playing
    jmpClearPlayer(mp);

    mp->npattern = pattern;
    mp->speed = mp->module->defSpeed;
    jmpSetTempo(mp, mp->module->defTempo);

    // Set callback
    result = jvmSetCallback(mp->device, jmpExec, (void *) mp);
    if (result != DMERR_OK)
    {
        JSS_UNLOCK(mp);
        JSSERROR(result, result, "Could not initialize callback for player.\n");
    }

    mp->isPlaying = TRUE;

    JSS_UNLOCK(mp);
    return 0;
}


/* Set volume for given module channel.
 */
static void jmpSetVolume(JSSPlayerChannel * chn, int channel, int volume)
{
    (void) channel;

    // Check values
    if (volume < mpMinVol) volume = mpMinVol; else
    if (volume > mpMaxVol) volume = mpMaxVol;

    // Set the volume
    chn->iCVolume = volume;
    JMPSETNDFLAGS(cdfNewVolume);
}

#define jmpChangeVolume(Q, Z, X) jmpSetVolume(Q, Z, chn->iCVolume + (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->iCPitch + delta;
    if (value < 0)
        value = 0;

    // Set the new pitch
    chn->iCPitch = 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->iCPitch != chn->iLastPortaToNotePitch)
    {
        if (chn->iCPitch < chn->iLastPortaToNotePitch)
        {
            // Increase pitch UP
            jmpChangePitch(chn, channel, chn->iLastPortaToNoteParam * 4);
            if (chn->iCPitch > chn->iLastPortaToNotePitch)
                chn->iCPitch = chn->iLastPortaToNotePitch;
        }
        else
        {
            // Decrease pitch DOWN
            jmpChangePitch(chn, channel, -(chn->iLastPortaToNoteParam * 4));
            if (chn->iCPitch < chn->iLastPortaToNotePitch)
                chn->iCPitch = chn->iLastPortaToNotePitch;
        }
    }
}


/* Do a tremolo effect for given module channel.
 */
static void jmpDoTremolo(JSSPlayer * mp, JSSPlayerChannel *chn, int channel)
{
    int delta, pos, depth;
    
    // Check settings
    if (chn->iTremoloDepth == 0 || chn->iTremoloSpeed == 0)
        return;

    // Get position of tremolo waveform
    pos = chn->iTremoloPos & 255;
    depth = chn->iTremoloDepth;

    switch (chn->iTremoloWC & 3)
    {
        case 0:    // Sine-wave
            delta = (jmpSineTable[pos] * depth) / 2048;
            break;

        case 1:    // Ramp down
            delta = ((pos - 128) * depth) / 128;
            break;

        case 2:    // Square
            delta = (((pos & 128) - 64) * depth) / 64;
            break;

        default:
            return;
    }

    // Set the new volume
    jmpCSetVolume(mp, chn, channel, chn->iCVolume + delta);

    // Advance tremolo waveform position
    chn->iTremoloPos += chn->iTremoloSpeed;
    if (chn->iTremoloPos > 255)
        chn->iTremoloPos = 0;
}


/* Do a vibrato effect for given module channel.
 */
static void jmpDoVibrato(JSSPlayer * mp, JSSPlayerChannel *chn, int channel)
{
    int delta, pos, depth;

    // Check settings
    if (chn->iVibratoDepth == 0 || chn->iVibratoSpeed == 0)
        return;
    
    // Get position of vibrato waveform
    pos = chn->iVibratoPos & 255;
    depth = chn->iVibratoDepth;

    switch (chn->iVibratoWC & 3)
    {
        case 0:    // Sine-wave
            delta = (jmpSineTable[pos] * depth) / 2048;
            break;

        case 1:    // Ramp down
            delta = ((pos - 128) * depth) / 16;
            break;

        case 2:    // Square
            delta = (((pos & 128) - 64) * depth) / 8;
            break;

        default:
            return;
    }

    // Set the new frequency
    jmpCSetPitch(mp, channel, chn->iCPitch + delta);

    // Advance vibrato waveform position
    chn->iVibratoPos += chn->iVibratoSpeed;
    if (chn->iVibratoPos > 255)
        chn->iVibratoPos = 0;
}


/* 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);
}


/* 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 *tempInst = chn->iCInstrument;

    if (tempInst)
    {
        int tmp = chn->iCNote;
        if (tmp == jsetNotSet || tmp == jsetNoteOff) return;
        switch (mp->tick & 3)
        {
            case 1:
                tmp += paramX;
                break;
            case 2:
                tmp += paramY;
                break;
        }
        jmpCSetPitch(mp, channel, jmpGetPeriodFromNote(mp, tmp + tempInst->ERelNote, tempInst->EFineTune));
    }
}


/*
 * Process pattern effects
 */
static void jmpProcessRowEffect(JSSPlayer * mp, int channel, JSSNote * currNote)
{
    JSSPlayerChannel *chn = &(mp->channels[channel]);
    int param, paramX, paramY;
    char effect;

    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 (param)
                chn->iLastPortaToNoteParam = param;

            if (currNote->note != jsetNotSet && currNote->note != jsetNoteOff) {
                chn->iLastPortaToNotePitch = chn->iCPitch;
                chn->iCPitch = chn->iCOldPitch;
                JMPUNSETNDFLAGS(cdfNewPitch | cdfNewInstr | cdfNewPanPos);
            }
            break;

        case '4':        // 4xy = Vibrato : IMPL.VERIFIED
            if (paramX)
                chn->iVibratoSpeed = paramX;

            if (paramY)
                chn->iVibratoDepth = paramY;

            if ((chn->iVibratoWC & 4) == 0)
                chn->iVibratoPos = 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->iTremoloSpeed = paramX;

            if (paramY)
                chn->iTremoloDepth = paramY;

            if ((chn->iTremoloWC & 4) == 0)
                chn->iTremoloPos = 0;
            break;

        case '8':        // 8xx = Set Panning
            JMPDEBUG("Set Panning used, UNIMPLEMENTED");
            break;

        case '9':        // 9xx = Set Sample Offset : IMPL.VERIFIED
            if (chn->iCNewDataFlags & cdfNewPitch)
            {
                chn->iCPosition = param * 0x100;
                JMPSETNDFLAGS(cdfNewPos);
            }
            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->iVibratoWC = 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->iTremoloWC = paramY;
                break;

            case 0x08:    // E8x - Set Pan Position
                chn->iCPanning = (paramY * 16);
                JMPSETNDFLAGS(cdfNewPanPos);
                break;

            case 0x09:    // E9x - Retrig note
                JMPDEBUG("Retrig Note used, UNIMPLEMENTED");
                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->iCNewDataFlags;
                    chn->iCNewDataFlags = 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->iCKeyOff = TRUE;
            break;

        case 'L':        // Lxx = Set Envelope Position
            JMPDEBUG("Set Envelope Position used, NOT verified with FT2");
            chn->iCPanEnv_Frames = param;
            chn->iCVolEnv_Frames = param;
            chn->iCPanEnv_Exec = TRUE;
            chn->iCVolEnv_Exec = TRUE;
            break;

        case 'R':        // Rxy = Multi Retrig note
            JMPDEBUG("Multi Retrig Note used, UNIMPLEMENTED");
            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 jmpProcessNewRow(JSSPlayer * mp, int channel)
{
    JSSNote *currNote;
    JSSExtInstrument *extInst = NULL;
    JSSInstrument *inst = NULL;
    BOOL newNote = FALSE;
    int tmp, paramX, paramY;
    JSSPlayerChannel *chn = &(mp->channels[channel]);

    JMPGETNOTE(currNote, mp->row, channel);

    // Check for a new note/keyoff here
    if (currNote->note == jsetNoteOff)
        chn->iCKeyOff = TRUE;
    else
    if (currNote->note >= 0 && currNote->note <= 96)
    {
        // New note was set
        newNote = TRUE;
        chn->iCNote = currNote->note;
    }

    // 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.
         */
        jmpResetEnvelopes(chn);
        chn->iCKeyOff = FALSE;
        chn->iCFadeOutVol = mpMaxFadeoutVol;

        // We save the instrument number here for later use
        if (currNote->instrument >= 0 && currNote->instrument < mp->module->nextInstruments)
            chn->iCExtInstrumentN = currNote->instrument;
    }

    /* ONLY if newNote was SET NOW and ExtInstrument HAS BEEN set, we can
     * set new pitches, and other things...
     */
    if (newNote)
    {
        if (chn->iCExtInstrumentN != jsetNotSet)
            extInst = mp->module->extInstruments[chn->iCExtInstrumentN];
        else
            extInst = NULL;

        if (extInst)
        {
            // Set instrument
            int note = chn->iCNote;
            chn->iCExtInstrument = extInst;

            // We set new Instrument ONLY if NEW NOTE has been set
            if (note != jsetNotSet)
            {
                // Get instrument number
                tmp = extInst->sNumForNotes[note];

                if (tmp >= 0 && tmp < mp->module->ninstruments) {
                    // Set the new instrument
                    inst = mp->module->instruments[tmp];
                    chn->iCInstrumentN = tmp;
                    chn->iCInstrument = inst;
                    chn->iCVolume = inst->volume;
                    chn->iCPanning = inst->EPanning;
                    chn->iCPosition = 0;

                    // Set NDFlags
                    JMPSETNDFLAGS(cdfNewInstr | cdfNewPos | cdfNewPanPos | cdfNewVolume);
                }
            }
        }
    }

    if (inst)
    {
        // Save old pitch for later use
        chn->iCOldPitch = chn->iCPitch;

        // Compute new pitch
        tmp = (chn->iCNote + inst->ERelNote);
//fprintf(stderr, "HEH: %d + %d = %d\n", chn->iCNote, inst->ERelNote, tmp);
        if (tmp < 0)
            tmp = 0;
        else if (tmp > 119)
            tmp = 119;

        chn->iCPitch = jmpGetPeriodFromNote(mp, tmp, inst->EFineTune);
        JMPSETNDFLAGS(cdfNewPitch);
    }
    
    // Process the volume column
    JMPMAKEPARAM(currNote->volume, paramX, paramY);

    switch (paramX)
    {
        case 0x00:
        case 0x01:
        case 0x02:
        case 0x03:
        case 0x04:
            jmpSetVolume(chn, channel, currNote->volume);
            break;

        case 0x07:        // Dx = Fine Volumeslide Down : IMPL.VERIFIED
            jmpChangeVolume(chn, channel, -paramY);
            break;

        case 0x08:        // Ux = Fine Volumeslide Up : IMPL.VERIFIED
            jmpChangeVolume(chn, channel, paramY);
            break;

        case 0x09:        // Sx = Set vibrato speed : IMPL.VERIFIED
            chn->iVibratoSpeed = paramY;
            break;

        case 0x0a:        // Vx = Vibrato : IMPL.VERIFIED
            if (paramY)
                chn->iVibratoDepth = paramY;
            break;

        case 0x0e:        // Mx = Porta To Note : IMPL.VERIFIED
            if (paramY)
                chn->iLastPortaToNoteParam = paramY;

            if (currNote->note != jsetNotSet && currNote->note != jsetNoteOff) {
                chn->iLastPortaToNotePitch = chn->iCPitch;
                chn->iCPitch = chn->iCOldPitch;
                JMPUNSETNDFLAGS(cdfNewPitch | cdfNewInstr | cdfNewPanPos);
            }
            break;
    }

    // ...And finally process the Normal effects
    if (currNote->effect != jsetNotSet)
        jmpProcessRowEffect(mp, channel, currNote);
}


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(mp, 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(mp, 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(mp, chn, channel);
            jmpDoVolumeSlide(chn, channel, chn->iLastVolSlideParam);
            break;

        case '7': // 7xy = Tremolo
            jmpDoTremolo(mp, chn, channel);
            break;

        case 'A': // Axy = Volume slide
            jmpDoVolumeSlide(chn, channel, chn->iLastVolSlideParam);
            break;

        case 'E': // Exy = Special Effects
            switch (paramX)
            {
                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->iCNewDataFlags = chn->iSaveNDFlags;
                    break;
            }
            break;

        case 'T': // Txy = Tremor
            JMPMAKEPARAM(chn->iLastTremorParam, paramX, paramY)
            paramX++;
            paramY++;
            tmp = (chn->iTremorCount % (paramX + paramY));
            if (tmp < paramX)
                jmpCSetVolume(mp, chn, channel, chn->iCVolume);
            else
                jmpCSetVolume(mp, 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, flags;

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

    dev = (JSSMixer *) pDEV;
    assert(mp->device == dev);
    assert(mp->module != NULL);

    // 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].iCNewDataFlags = 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 < 0)
    {
        // Initialize pattern
        if (mp->order != jsetNotSet)
            jmpSetNewOrder(mp, mp->order);

        mp->newRow = 0;
        mp->newRowSet = TRUE;
        mp->tick = mp->speed;
    }

//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
        flags = chn->iCNewDataFlags;
        if (flags)
        {
            // Check if we stop?
            if (flags & cdfStop)
            {
                jvmStop(mp->device, channel);
            }
            else
            {
                // No, handle other flags
                if (flags & cdfNewInstr)
                {
                    JSSInstrument *instr = chn->iCInstrument;
                    if (instr != NULL)
                    {
                        jvmSetSample(mp->device, channel,
                            instr->data, instr->size,
                            instr->loopS, instr->loopE,
                            instr->flags);
                    }
                    jvmPlay(mp->device, channel);
                }

                if (flags & cdfNewPitch)
                    jmpCSetPitch(mp, channel, chn->iCPitch);

                if (flags & cdfNewPos)
                    jmpCSetPosition(mp, channel, chn->iCPosition);

                if (flags & cdfNewVolume)
                    jmpCSetVolume(mp, chn, channel, chn->iCVolume);

                if (flags & cdfNewPanPos)
                    jmpCSetPanning(mp, chn, channel, chn->iCPanning);

                if (flags & cdfNewGlobalVol)
                    jvmSetGlobalVol(mp->device, mp->globalVol);
            }
        }
    }

out:
    JSS_UNLOCK(mp);
}