Mercurial > hg > dmlib
diff jssplr.c @ 0:32250b436bca
Initial re-import.
author | Matti Hamalainen <ccr@tnsp.org> |
---|---|
date | Fri, 28 Sep 2012 01:54:23 +0300 |
parents | |
children | ca0e00facb7b |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jssplr.c Fri Sep 28 01:54:23 2012 +0300 @@ -0,0 +1,1593 @@ +/* + * 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->pDevice != NULL); + + if (value > 0) + { + if (JMPGETMODFLAGS(mp, jmdfAmigaPeriods)) + { + // Frequency = 8363*1712 / Period +//fprintf(stderr, "jmpCSetPitch::AMIGA(%d, %d)\n", channel, value); + jvmSetFreq(mp->pDevice, 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->pDevice, channel, + 8363.0f * pow(2.0f, (4608.0f - (double) value) / 768.0f)); + } + } +} + + +static void jmpCSetInstrument(JSSPlayer * mp, int channel) +{ + JSSInstrument *instr; + assert(mp != NULL); + assert(mp->pDevice != NULL); + + instr = mp->iCInstrument[channel]; + + if (instr) + { + jvmSetSample(mp->pDevice, channel, + instr->data, instr->size, + instr->loopS, instr->loopE, + instr->flags); + } +} + + +static void jmpCSetVolume(JSSPlayer * mp, int channel, int volume) +{ + assert(mp != NULL); + assert(mp->pDevice != NULL); + if (volume < 0) volume = 0; else + if (volume > 64) volume = 64; + +//fprintf(stderr, "chn %d: vol=%d, fad=%d, env=%d\n", channel, volume, mp->iCFadeOutVol[channel], mp->iCVolEnv[channel]); + + jvmSetVolume(mp->pDevice, channel, + (mp->iCFadeOutVol[channel] * mp->iCVolEnv[channel] * volume) / (16 * 65536)); +} + + +static void jmpCSetPanning(JSSPlayer * mp, int channel, int panning) +{ + assert(mp != NULL); + assert(mp->pDevice != NULL); + + jvmSetPan(mp->pDevice, channel, + panning + (((mp->iCPanEnv[channel] - 32) * (128 - abs(panning - 128))) / 32)); +} + + +static void jmpCSetPosition(JSSPlayer * mp, int channel, int value) +{ + assert(mp != NULL); + assert(mp->pDevice != NULL); + + jvmSetPos(mp->pDevice, channel, value); +} + + +static void jmpCPlay(JSSPlayer * mp, int channel) +{ + assert(mp != NULL); + assert(mp->pDevice != NULL); + + jvmPlay(mp->pDevice, channel); +} + + +static void jmpCStop(JSSPlayer * mp, int channel) +{ + assert(mp != NULL); + assert(mp->pDevice != NULL); + + jvmStop(mp->pDevice, channel); +} + + + +static int jmpFindEnvPoint(JSSEnvelope * env, 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, int channel) +{ + JSSExtInstrument *inst = mp->iCExtInstrument[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 (mp->iCKeyOff[channel] && (mp->iCFadeOutVol[channel] > 0) && (inst->fadeOut > 0)) + { + int tmp = (mp->iCFadeOutVol[channel] - inst->fadeOut); + if (tmp < 0) tmp = 0; + mp->iCFadeOutVol[channel] = tmp; + + JMPSETNDFLAGS(cdfNewVolume); + } + + if (mp->iCVolEnv_Exec[channel]) + { + // Execute the volume envelope + jmpExecEnvelope(&(inst->volumeEnv), mp->iCKeyOff[channel], + &(mp->iCVolEnv_Frames[channel]), &(mp->iCVolEnv_Exec[channel]), + &(mp->iCVolEnv[channel])); + + JMPSETNDFLAGS(cdfNewVolume); + } + } + else + { + // If the envelope is not used, set max volume + if (mp->iCVolEnv[channel] != mpMaxVol) + { + mp->iCVolEnv[channel] = mpMaxVol; + mp->iCFadeOutVol[channel] = mpMaxFadeoutVol; + JMPSETNDFLAGS(cdfNewVolume); + } + } + + // Process the panning envelope + if (inst->panningEnv.flags & jenvfUsed) + { + if (mp->iCPanEnv_Exec[channel]) + { + // Execute the panning envelope + jmpExecEnvelope(&(inst->panningEnv), mp->iCKeyOff[channel], + &(mp->iCPanEnv_Frames[channel]), &(mp->iCPanEnv_Exec[channel]), + &(mp->iCPanEnv[channel])); + + JMPSETNDFLAGS(cdfNewPanPos); + } + } + else + { + // If the envelope is not used, set center panning + if (mp->iCPanEnv[channel] != mpPanCenter) + { + mp->iCPanEnv[channel] = 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->pDevice = 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(JSSPlayer * mp, int channel) +{ + assert(mp != NULL); + + mp->iCPanEnv_Frames[channel] = mp->iCVolEnv_Frames[channel] = 0; + mp->iCPanEnv_Exec[channel] = mp->iCVolEnv_Exec[channel] = TRUE; +} + + +/* Clear module player structure + */ +void jmpClearPlayer(JSSPlayer * mp) +{ + int i; + assert(mp != NULL); + JSS_LOCK(mp); + + // Initialize general variables + mp->iPatternDelay = 0; + mp->newRowSet = FALSE; + mp->newOrderSet = FALSE; + mp->iTick = jsetNotSet; + mp->iRow = 0; + mp->iLastPatLoopRow = 0; + + // Initialize channel data + memset(&(mp->iPatLoopRow), 0, sizeof(mp->iPatLoopRow)); + memset(&(mp->iPatLoopCount), 0, sizeof(mp->iPatLoopCount)); + + memset(&mp->iLastPortaParam, 0, sizeof(mp->iLastPortaParam)); + memset(&mp->iLastPortaToNoteParam, 0, sizeof(mp->iLastPortaToNoteParam)); + memset(&mp->iLastPortaToNotePitch, 0, sizeof(mp->iLastPortaToNotePitch)); + + memset(&mp->iVibratoPos, 0, sizeof(mp->iVibratoPos)); + memset(&mp->iVibratoSpeed, 0, sizeof(mp->iVibratoSpeed)); + memset(&mp->iVibratoDepth, 0, sizeof(mp->iVibratoDepth)); + memset(&mp->iVibratoWC, 0, sizeof(mp->iVibratoWC)); + + memset(&mp->iTremoloPos, 0, sizeof(mp->iTremoloPos)); + memset(&mp->iTremoloSpeed, 0, sizeof(mp->iTremoloSpeed)); + memset(&mp->iTremoloDepth, 0, sizeof(mp->iTremoloDepth)); + memset(&mp->iTremoloWC, 0, sizeof(mp->iTremoloWC)); + + memset(&mp->iLastTremorParam, 0, sizeof(mp->iLastTremorParam)); + memset(&mp->iTremorCount, 0, sizeof(mp->iTremorCount)); + memset(&mp->iLastSampleOffset, 0, sizeof(mp->iLastSampleOffset)); + memset(&mp->iLastRetrigParam, 0, sizeof(mp->iLastRetrigParam)); + memset(&mp->iLastVolSlideParam, 0, sizeof(mp->iLastVolSlideParam)); + + memset(&mp->iRetrigNDFlags, 0, sizeof(mp->iRetrigNDFlags)); + memset(&mp->iSaveNDFlags, 0, sizeof(mp->iSaveNDFlags)); + + memset(&mp->iCNewDataFlags, 0, sizeof(mp->iCNewDataFlags)); + memset(&mp->iCPitch, 0, sizeof(mp->iCPitch)); + memset(&mp->iCPosition, 0, sizeof(mp->iCPosition)); + memset(&mp->iCVolume, 0, sizeof(mp->iCVolume)); + memset(&mp->iCFadeOutVol, 0, sizeof(mp->iCFadeOutVol)); + memset(&mp->iCVolEnv, 0, sizeof(mp->iCVolEnv)); + + memset(&mp->iCAutoVib_Frames, 0, sizeof(mp->iCAutoVib_Frames)); + memset(&mp->iCPanEnv_Frames, 0, sizeof(mp->iCPanEnv_Frames)); + memset(&mp->iCVolEnv_Frames, 0, sizeof(mp->iCVolEnv_Frames)); + + memset(&mp->iCLastFineVolumeslideUpParam, 0, sizeof(mp->iCLastFineVolumeslideUpParam)); + memset(&mp->iCLastFineVolumeslideDownParam, 0, sizeof(mp->iCLastFineVolumeslideDownParam)); + memset(&mp->iCLastExtraFinePortamentoUpParam, 0, sizeof(mp->iCLastExtraFinePortamentoUpParam)); + memset(&mp->iCLastExtraFinePortamentoDownParam, 0, sizeof(mp->iCLastExtraFinePortamentoDownParam)); + memset(&mp->iCLastFinePortamentoUpParam, 0, sizeof(mp->iCLastFinePortamentoUpParam)); + memset(&mp->iCLastFinePortamentoDownParam, 0, sizeof(mp->iCLastFinePortamentoDownParam)); + + memset(&mp->iCPanEnv_Exec, 0, sizeof(mp->iCPanEnv_Exec)); + memset(&mp->iCVolEnv_Exec, 0, sizeof(mp->iCVolEnv_Exec)); + memset(&mp->iCKeyOff, 0, sizeof(mp->iCKeyOff)); + + for (i = 0; i < jsetNChannels; i++) + { + mp->iCNote[i] = jsetNotSet; + mp->iCInstrument[i] = NULL; + mp->iCInstrumentN[i] = jsetNotSet; + mp->iCExtInstrument[i] = NULL; + mp->iCExtInstrumentN[i] = jsetNotSet; + mp->iCPanning[i] = mpPanCenter; + mp->iCPanEnv[i] = mpPanCenter; + } + + JSS_UNLOCK(mp); +} + + +/* Set module + */ +void jmpSetModule(JSSPlayer * mp, JSSModule * pModule) +{ + assert(mp != NULL); + JSS_LOCK(mp); + + jmpStop(mp); + jmpClearPlayer(mp); + + mp->pModule = pModule; + + JSS_UNLOCK(mp); +} + + +/* Stop playing + */ +void jmpStop(JSSPlayer * mp) +{ + assert(mp != NULL); + JSS_LOCK(mp); + + if (mp->isPlaying) + { + // Remove callback + jvmRemoveCallback(mp->pDevice); + 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->pDevice, 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->iOrder = jsetNotSet; + orderOK = FALSE; + + while (!orderOK) + { + if (order < 0 || order >= mp->pModule->norders) + { + jmpStop(mp); + orderOK = TRUE; + } + else + { + pattern = mp->pModule->orderList[order]; + if (pattern == jsetOrderSkip) + { + order++; + } + else + if (pattern >= mp->pModule->npatterns || pattern == jsetOrderEnd) + { + jmpStop(mp); + orderOK = TRUE; + } + else + { + // All OK + orderOK = TRUE; + mp->pPattern = mp->pModule->patterns[pattern]; + mp->iPattern = pattern; + mp->iOrder = order; + } + } + } +} + + +/* Set new tempo-value of the player. + */ +void jmpSetTempo(JSSPlayer * mp, int tempo) +{ + assert(mp != NULL); + JSS_LOCK(mp); + assert(mp->pDevice != NULL); + + mp->iTempo = tempo; + jvmSetCallbackFreq(mp->pDevice, (tempo * 2) / 5); + JSS_UNLOCK(mp); +} + + +void jmpClearChannels(JSSPlayer * mp) +{ + int i; + assert(mp != NULL); + JSS_LOCK(mp); + assert(mp->pDevice != NULL); + assert(mp->pModule != NULL); + + for (i = 0; i < mp->pModule->nchannels; i++) + { + jvmStop(mp->pDevice, i); + jvmClear(mp->pDevice, i); + } + + JSS_UNLOCK(mp); +} + + +/* Starts playing module from a given ORDER. + */ +int jmpPlayOrder(JSSPlayer * mp, int iStartOrder) +{ + int result; + assert(mp != NULL); + + JSS_LOCK(mp); + assert(mp->pModule != NULL); + + // Stop if already playing + jmpStop(mp); + + jmpClearChannels(mp); + + // Check starting order + if (iStartOrder < 0 || iStartOrder >= mp->pModule->norders) + { + JSS_UNLOCK(mp); + JSSERROR(DMERR_INVALID_ARGS, DMERR_INVALID_ARGS, "Invalid playing startorder given.\n"); + } + + // Initialize playing + jmpClearPlayer(mp); + + jmpSetNewOrder(mp, iStartOrder); + + if (mp->iOrder == jsetNotSet) + { + JSS_UNLOCK(mp); + JSSERROR(DMERR_NOT_SUPPORTED, DMERR_NOT_SUPPORTED, + "Could not start playing from given order #%i\n", iStartOrder); + } + + mp->iSpeed = mp->pModule->defSpeed; + jmpSetTempo(mp, mp->pModule->defTempo); + + // Set callback + result = jvmSetCallback(mp->pDevice, 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->pModule != NULL); + + // Stop if already playing + jmpStop(mp); + jmpClearChannels(mp); + + // Initialize playing + jmpClearPlayer(mp); + + mp->iPattern = pattern; + mp->iSpeed = mp->pModule->defSpeed; + jmpSetTempo(mp, mp->pModule->defTempo); + + // Set callback + result = jvmSetCallback(mp->pDevice, 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(JSSPlayer * mp, int channel, int value) +{ + // Check values + if (value < mpMinVol) + value = mpMinVol; + else if (value > mpMaxVol) + value = mpMaxVol; + + // Set the volume + mp->iCVolume[channel] = value; + JMPSETNDFLAGS(cdfNewVolume); +} + +#define jmpChangeVolume(Q, Z, X) jmpSetVolume(Q, Z, mp->iCVolume[channel] + (X)) + + +/* Change the pitch of given channel by ADelta. + */ +static void jmpChangePitch(JSSPlayer * mp, int channel, int delta) +{ + int value; + + // Calculate new pitch and check it + value = (mp->iCPitch[channel] + delta); + if (value < 0) + value = 0; + + // Set the new pitch + mp->iCPitch[channel] = value; + JMPSETNDFLAGS(cdfNewPitch); +} + + +/* Do a note portamento (pitch slide) effect for given module channel. + */ +static void jmpDoPortamento(JSSPlayer * mp, int channel) +{ + // Check for zero parameter + if (mp->iLastPortaToNoteParam[channel] == 0) + { + JMPSETNDFLAGS(cdfNewPitch); + return; + } + + /* Slide the pitch of channel to the destination value + * with speed of iLastPortaToNoteParam[] * 4 and stop when it equals. + */ + if (mp->iCPitch[channel] != mp->iLastPortaToNotePitch[channel]) + { + if (mp->iCPitch[channel] < mp->iLastPortaToNotePitch[channel]) + { + // Increase pitch UP + jmpChangePitch(mp, channel, mp->iLastPortaToNoteParam[channel] * 4); + if (mp->iCPitch[channel] > mp->iLastPortaToNotePitch[channel]) + mp->iCPitch[channel] = mp->iLastPortaToNotePitch[channel]; + } + else + { + // Decrease pitch DOWN + jmpChangePitch(mp, channel, -(mp->iLastPortaToNoteParam[channel] * 4)); + if (mp->iCPitch[channel] < mp->iLastPortaToNotePitch[channel]) + mp->iCPitch[channel] = mp->iLastPortaToNotePitch[channel]; + } + } +} + + +/* Do a tremolo effect for given module channel. + */ +static void jmpDoTremolo(JSSPlayer * mp, int channel) +{ + int delta, pos, depth; + + // Check settings + if (mp->iTremoloDepth[channel] == 0 || mp->iTremoloSpeed[channel] == 0) + return; + + // Get position of tremolo waveform + pos = mp->iTremoloPos[channel] & 255; + depth = mp->iTremoloDepth[channel]; + + switch (mp->iTremoloWC[channel] & 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, channel, mp->iCVolume[channel] + delta); + + // Advance tremolo waveform position + mp->iTremoloPos[channel] += mp->iTremoloSpeed[channel]; + if (mp->iTremoloPos[channel] > 255) + mp->iTremoloPos[channel] = 0; +} + + +/* Do a vibrato effect for given module channel. + */ +static void jmpDoVibrato(JSSPlayer * mp, int channel) +{ + int delta, pos, depth; + + // Check settings + if (mp->iVibratoDepth[channel] == 0 || mp->iVibratoSpeed[channel] == 0) + return; + + // Get position of vibrato waveform + pos = mp->iVibratoPos[channel] & 255; + depth = mp->iVibratoDepth[channel]; + + switch (mp->iVibratoWC[channel] & 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, mp->iCPitch[channel] + delta); + + // Advance vibrato waveform position + mp->iVibratoPos[channel] += mp->iVibratoSpeed[channel]; + if (mp->iVibratoPos[channel] > 255) + mp->iVibratoPos[channel] = 0; +} + + +/* Do a volume slide effect for given module channel. + */ +static void jmpDoVolumeSlide(JSSPlayer * mp, int channel, int param) +{ + int paramX, paramY; + + JMPMAKEPARAM(param, paramX, paramY) + + if (paramY == 0) + jmpChangeVolume(mp, channel, paramX); + if (paramX == 0) + jmpChangeVolume(mp, 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, int channel, int paramY) +{ + // Check what we need to do + if (paramY > 0) + { + // SBx/E6x loops 'x' times + if (mp->iPatLoopCount[channel] == 1) + mp->iPatLoopCount[channel] = 0; + else + { + // Check if we need to set the count + if (mp->iPatLoopCount[channel] == 0) + mp->iPatLoopCount[channel] = (paramY + 1); + + // Loop to specified row + mp->iPatLoopCount[channel]--; + mp->iNewRow = mp->iPatLoopRow[channel]; + mp->newRowSet = TRUE; + } + } + else + { + // SB0/E60 sets the loop start point + mp->iPatLoopRow[channel] = mp->iRow; + + // This is here because of the infamous FT2 patloop bug + mp->iLastPatLoopRow = mp->iRow; + } +} + + +/* Do arpeggio effect + */ +static void jmpDoArpeggio(JSSPlayer * mp, int channel, int paramY, int paramX) +{ + JSSInstrument *tempInst = mp->iCInstrument[channel]; + if (tempInst) + { + int tmp = mp->iCNote[channel]; + if (tmp == jsetNotSet || tmp == jsetNoteOff) return; + switch (mp->iTick & 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) +{ + 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, 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) + mp->iLastPortaParam[channel] = param; + break; + + case '3': // 3xy = Porta To Note + if (param) + mp->iLastPortaToNoteParam[channel] = param; + + if (currNote->note != jsetNotSet && currNote->note != jsetNoteOff) { + mp->iLastPortaToNotePitch[channel] = mp->iCPitch[channel]; + mp->iCPitch[channel] = mp->iCOldPitch[channel]; + JMPUNSETNDFLAGS(cdfNewPitch | cdfNewInstr | cdfNewPanPos); + } + break; + + case '4': // 4xy = Vibrato : IMPL.VERIFIED + if (paramX) + mp->iVibratoSpeed[channel] = paramX; + + if (paramY) + mp->iVibratoDepth[channel] = paramY; + + if ((mp->iVibratoWC[channel] & 4) == 0) + mp->iVibratoPos[channel] = 0; + break; + + case '5': // 5xy = Portamento + Volume Slide + case '6': // 6xy = Vibrato + Volume slide + if (param) + mp->iLastVolSlideParam[channel] = param; + break; + + case '7': // 7xy = Tremolo + if (paramX) + mp->iTremoloSpeed[channel] = paramX; + + if (paramY) + mp->iTremoloDepth[channel] = paramY; + + if ((mp->iTremoloWC[channel] & 4) == 0) + mp->iTremoloPos[channel] = 0; + break; + + case '8': // 8xx = Set Panning + JMPDEBUG("Set Panning used, UNIMPLEMENTED"); + break; + + case '9': // 9xx = Set Sample Offset : IMPL.VERIFIED + if (mp->iCNewDataFlags[channel] & cdfNewPitch) { + mp->iCPosition[channel] = param * 0x100; + JMPSETNDFLAGS(cdfNewPos); + } + break; + + case 'A': // Axy = Volume Slide : IMPL.VERIFIED + if (param) + mp->iLastVolSlideParam[channel] = param; + break; + + case 'B': // Bxx = Pattern Jump : IMPL.VERIFIED + mp->iNewOrder = param; + mp->newOrderSet = TRUE; + mp->jumpFlag = TRUE; + mp->iLastPatLoopRow = 0; + break; + + case 'C': // Cxx = Set Volume : IMPL.VERIFIED + jmpSetVolume(mp, channel, param); + break; + + case 'D': // Dxx = Pattern Break : IMPL.VERIFIED + // Compute the new row + mp->iNewRow = (paramX * 10) + paramY; + if (mp->iNewRow >= mp->pPattern->nrows) + mp->iNewRow = 0; + + mp->newRowSet = TRUE; + + // Now we do some tricky tests + if (!mp->breakFlag && !mp->jumpFlag) { + mp->iNewOrder = mp->iOrder + 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) + mp->iCLastFinePortamentoUpParam[channel] = paramY; + + jmpChangePitch(mp, channel, -(mp->iCLastFinePortamentoUpParam[channel] * 4)); + break; + + case 0x02: // E2x - Fine Portamento Down + if (paramY) + mp->iCLastFinePortamentoDownParam[channel] = paramY; + + jmpChangePitch(mp, channel, (mp->iCLastFinePortamentoDownParam[channel] * 4)); + break; + + case 0x03: // E3x - Glissando Control (NOT SUPPORTED) + break; + + case 0x04: // E4x - Set Vibrato waveform + mp->iVibratoWC[channel] = paramY; + break; + + case 0x05: // E5x - Set Finetune + JMPDEBUG("Set Finetune used, UNIMPLEMENTED"); + break; + + case 0x06: // E6x - Set Pattern Loop + jmpDoPatternLoop(mp, channel, paramY); + break; + + case 0x07: // E7x - Set Tremolo waveform + mp->iTremoloWC[channel] = paramY; + break; + + case 0x08: // E8x - Set Pan Position + mp->iCPanning[channel] = (paramY * 16); + JMPSETNDFLAGS(cdfNewPanPos); + break; + + case 0x09: // E9x - Retrig note + JMPDEBUG("Retrig Note used, UNIMPLEMENTED"); + break; + + case 0x0a: // EAx - Fine Volumeslide Up + if (paramY) + mp->iCLastFineVolumeslideUpParam[channel] = paramY; + + jmpChangeVolume(mp, channel, mp->iCLastFineVolumeslideUpParam[channel]); + break; + + case 0x0b: // EBx - Fine Volumeslide Down + if (paramY) + mp->iCLastFineVolumeslideDownParam[channel] = paramY; + jmpChangeVolume(mp, channel, -(mp->iCLastFineVolumeslideDownParam[channel])); + 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 + mp->iSaveNDFlags[channel] = mp->iCNewDataFlags[channel]; + mp->iCNewDataFlags[channel] = 0; + // TODO .. does this only affect NOTE or also instrument? + } + break; + + case 0x0e: // EEx - Set Pattern Delay : IMPL.VERIFIED + mp->iPatternDelay = 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->iSpeed = param; + else + jmpSetTempo(mp, param); + } + break; + + case 'G': // Gxx = Global Volume + mp->iGlobalVol = 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) + mp->iCKeyOff[channel] = TRUE; + break; + + case 'L': // Lxx = Set Envelope Position + JMPDEBUG("Set Envelope Position used, NOT verified with FT2"); + mp->iCPanEnv_Frames[channel] = param; + mp->iCVolEnv_Frames[channel] = param; + mp->iCPanEnv_Exec[channel] = TRUE; + mp->iCVolEnv_Exec[channel] = TRUE; + break; + + case 'R': // Rxy = Multi Retrig note + JMPDEBUG("Multi Retrig Note used, UNIMPLEMENTED"); + break; + + case 'T': // Txy = Tremor + if (param) + mp->iLastTremorParam[channel] = param; + break; + + case 'X': // Xxy = Extra Fine Portamento + switch (paramX) + { + case 0x01: // X1y - Extra Fine Portamento Up + if (paramY) + mp->iCLastExtraFinePortamentoUpParam[param] = paramY; + + jmpChangePitch(mp, channel, -(mp->iCLastExtraFinePortamentoUpParam[param])); + break; + + case 0x02: // X2y - Extra Fine Portamento Down + if (paramY) + mp->iCLastExtraFinePortamentoDownParam[param] = paramY; + + jmpChangePitch(mp, channel, mp->iCLastExtraFinePortamentoUpParam[param]); + 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; + + JMPGETNOTE(currNote, mp->iRow, channel); + + // Check for a new note/keyoff here + if (currNote->note == jsetNoteOff) + mp->iCKeyOff[channel] = TRUE; + else + if (currNote->note >= 0 && currNote->note <= 96) + { + // New note was set + newNote = TRUE; + mp->iCNote[channel] = 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(mp, channel); + mp->iCKeyOff[channel] = FALSE; + mp->iCFadeOutVol[channel] = mpMaxFadeoutVol; + + // We save the instrument number here for later use + if (currNote->instrument >= 0 && currNote->instrument < mp->pModule->nextInstruments) + mp->iCExtInstrumentN[channel] = currNote->instrument; + } + + /* ONLY if newNote was SET NOW and ExtInstrument HAS BEEN set, we can + * set new pitches, and other things... + */ + if (newNote) + { + if (mp->iCExtInstrumentN[channel] != jsetNotSet) + extInst = mp->pModule->extInstruments[mp->iCExtInstrumentN[channel]]; + else + extInst = NULL; + + if (extInst) + { + // Set instrument + int note = mp->iCNote[channel]; + mp->iCExtInstrument[channel] = 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->pModule->ninstruments) { + // Set the new instrument + inst = mp->pModule->instruments[tmp]; + mp->iCInstrumentN[channel] = tmp; + mp->iCInstrument[channel] = inst; + mp->iCVolume[channel] = inst->volume; + mp->iCPanning[channel] = inst->EPanning; + mp->iCPosition[channel] = 0; + + // Set NDFlags + JMPSETNDFLAGS(cdfNewInstr | cdfNewPos | cdfNewPanPos | cdfNewVolume); + } + } + } + } + + if (inst) + { + // Save old pitch for later use + mp->iCOldPitch[channel] = mp->iCPitch[channel]; + + // Compute new pitch + tmp = (mp->iCNote[channel] + inst->ERelNote); +//fprintf(stderr, "HEH: %d + %d = %d\n", mp->iCNote[channel], inst->ERelNote, tmp); + if (tmp < 0) + tmp = 0; + else if (tmp > 119) + tmp = 119; + + mp->iCPitch[channel] = 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(mp, channel, currNote->volume); + break; + + case 0x07: // Dx = Fine Volumeslide Down : IMPL.VERIFIED + jmpChangeVolume(mp, channel, -paramY); + break; + + case 0x08: // Ux = Fine Volumeslide Up : IMPL.VERIFIED + jmpChangeVolume(mp, channel, paramY); + break; + + case 0x09: // Sx = Set vibrato speed : IMPL.VERIFIED + mp->iVibratoSpeed[channel] = paramY; + break; + + case 0x0a: // Vx = Vibrato : IMPL.VERIFIED + if (paramY) + mp->iVibratoDepth[channel] = paramY; + break; + + case 0x0e: // Mx = Porta To Note : IMPL.VERIFIED + if (paramY) + mp->iLastPortaToNoteParam[channel] = paramY; + + if (currNote->note != jsetNotSet && currNote->note != jsetNoteOff) { + mp->iLastPortaToNotePitch[channel] = mp->iCPitch[channel]; + mp->iCPitch[channel] = mp->iCOldPitch[channel]; + 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) +{ + JSSNote *currNote; + int param, paramX, paramY, tmp; + char effect; + + // Process the volume column effects + JMPGETNOTE(currNote, mp->iRow, channel); + JMPMAKEPARAM(currNote->volume, paramX, paramY); + + switch (paramX) + { + case 0x05: // -x = Volumeslide Down : IMPL.VERIFIED + jmpChangeVolume(mp, channel, -paramY); + break; + + case 0x06: // +x = Volumeslide Down : IMPL.VERIFIED + jmpChangeVolume(mp, channel, paramY); + break; + + case 0x0a: // Vx = Vibrato : IMPL.VERIFIED + jmpDoVibrato(mp, channel); + break; + + case 0x0e: // Mx = Porta To Note : IMPL.VERIFIED + jmpDoPortamento(mp, 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, channel, paramX, paramY); + break; + + case '1': // 1xy = Portamento Up + if (mp->iLastPortaParam[channel] > 0) + jmpChangePitch(mp, channel, -(mp->iLastPortaParam[channel] * 4)); + break; + + case '2': // 2xy = Portamento Down + if (mp->iLastPortaParam[channel] > 0) + jmpChangePitch(mp, channel, (mp->iLastPortaParam[channel] * 4)); + break; + + case '3': // 3xy = Porta To Note + jmpDoPortamento(mp, channel); + break; + + case '4': // 4xy = Vibrato + jmpDoVibrato(mp, channel); + break; + + case '5': // 5xy = Portamento + Volume Slide + jmpDoPortamento(mp, channel); + jmpDoVolumeSlide(mp, channel, mp->iLastVolSlideParam[channel]); + break; + + case '6': // 6xy = Vibrato + Volume Slide + jmpDoVibrato(mp, channel); + jmpDoVolumeSlide(mp, channel, mp->iLastVolSlideParam[channel]); + break; + + case '7': // 7xy = Tremolo + jmpDoTremolo(mp, channel); + break; + + case 'A': // Axy = Volume slide + jmpDoVolumeSlide(mp, channel, mp->iLastVolSlideParam[channel]); + break; + + case 'E': // Exy = Special Effects + switch (paramX) + { + case 0x0c: // ECx - Set Note Cut + if (mp->iTick == paramY) + jmpSetVolume(mp, channel, jsetMinVol); + break; + + case 0x0d: // EDx - Set Note Delay + if (mp->iTick == paramY) + mp->iCNewDataFlags[channel] = mp->iSaveNDFlags[channel]; + break; + } + break; + + case 'T': // Txy = Tremor + JMPMAKEPARAM(mp->iLastTremorParam[channel], paramX, paramY) + paramX++; + paramY++; + tmp = (mp->iTremorCount[channel] % (paramX + paramY)); + if (tmp < paramX) + jmpCSetVolume(mp, channel, mp->iCVolume[channel]); + else + jmpCSetVolume(mp, channel, jsetMinVol); + + mp->iTremorCount[channel] = (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->pDevice == dev); + assert(mp->pModule != NULL); + + // Check if we are playing + if (!mp->isPlaying) + goto out; + + // Clear channel new data flags + mp->jumpFlag = FALSE; + mp->breakFlag = FALSE; + memset(mp->iCNewDataFlags, 0, sizeof(mp->iCNewDataFlags)); + +//fprintf(stderr, "1: iTick=%d, iOrder=%d, iPattern=%d, iRow=%d\n", mp->iTick, mp->iOrder, mp->iPattern, mp->iRow); + + // Check for init-tick + if (mp->iTick < 0) + { + // Initialize pattern + if (mp->iOrder != jsetNotSet) + jmpSetNewOrder(mp, mp->iOrder); + + mp->iNewRow = 0; + mp->newRowSet = TRUE; + mp->iTick = mp->iSpeed; + } + +//fprintf(stderr, "2: iTick=%d, iOrder=%d, iPattern=%d, iRow=%d\n", mp->iTick, mp->iOrder, mp->iPattern, mp->iRow); + + // Check if we are playing + if (!mp->isPlaying) + goto out; + + assert(mp->pPattern); + + // Update the tick + mp->iTick++; + if (mp->iTick >= mp->iSpeed) + { + // Re-init tick counter + mp->iTick = 0; + + // Check pattern delay + if (mp->iPatternDelay > 0) + mp->iPatternDelay--; + else + { + // New pattern row + if (mp->newRowSet) + { + mp->iRow = mp->iNewRow; + mp->newRowSet = FALSE; + } else + mp->iRow++; + + // Check for end of pattern + if (mp->iRow >= mp->pPattern->nrows) + { + // Go to next order + if (mp->iOrder != jsetNotSet) + jmpSetNewOrder(mp, mp->iOrder + 1); + else + mp->isPlaying = FALSE; + + // Check for FT2 quirks + if (JMPGETMODFLAGS(mp, jmdfFT2Replay)) + mp->iRow = mp->iLastPatLoopRow; + else + mp->iRow = 0; + } + + if (!mp->isPlaying) + goto out; + + // Check current order + if (mp->newOrderSet) + { + jmpSetNewOrder(mp, mp->iNewOrder); + mp->newOrderSet = FALSE; + } + +//fprintf(stderr, "3: iTick=%d, iOrder=%d, iPattern=%d, iRow=%d\n", mp->iTick, mp->iOrder, mp->iPattern, mp->iRow); + + if (!mp->isPlaying) + goto out; + + // TICK #0: Process new row + for (channel = 0; channel < mp->pModule->nchannels; channel++) + jmpProcessNewRow(mp, channel); + } // iPatternDelay + } // iTick + else + { + // Implement FT2's pattern delay-effect: don't update effects while on patdelay + if (!JMPGETMODFLAGS(mp, jmdfFT2Replay) || + (JMPGETMODFLAGS(mp, jmdfFT2Replay) && mp->iPatternDelay <= 0)) + { + // TICK n: Process the effects + for (channel = 0; channel < mp->pModule->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->pModule->nchannels; channel++) + { + // Process extended instruments + jmpProcessExtInstrument(mp, channel); + + // Check NDFlags and update channel data + flags = mp->iCNewDataFlags[channel]; + if (flags) + { + // Check if we stop? + if (flags & cdfStop) + jmpCStop(mp, channel); + else + { + // No, handle other flags + if (flags & cdfNewInstr) + { + jmpCSetInstrument(mp, channel); + jmpCPlay(mp, channel); + } + + if (flags & cdfNewPitch) + jmpCSetPitch(mp, channel, mp->iCPitch[channel]); + + if (flags & cdfNewPos) + jmpCSetPosition(mp, channel, mp->iCPosition[channel]); + + if (flags & cdfNewVolume) + jmpCSetVolume(mp, channel, mp->iCVolume[channel]); + + if (flags & cdfNewPanPos) + jmpCSetPanning(mp, channel, mp->iCPanning[channel]); + + if (flags & cdfNewGlobalVol) + jvmSetGlobalVol(mp->pDevice, mp->iGlobalVol); + } + } + } + +out: + JSS_UNLOCK(mp); +}