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