view jssplr.c @ 60:f28cd66356f6

Initial work for bitmapped fonts and text drawing. Also moved TTF header stuff to dmtext.h.
author Matti Hamalainen <ccr@tnsp.org>
date Mon, 01 Oct 2012 09:46:56 +0300
parents e0e470c3fc8e
children a33e47232161
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(JSSPlayer * mp, JSSPlayerChannel *chn, int channel)
{
    JSSExtInstrument *inst = chn->iCExtInstrument;

    // 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)
{
    // 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;

    // 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)
{
    // 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)
{
    // 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(mp, 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);
}