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