Mercurial > hg > dmlib
view jssplr.c @ 135:ff0fe1d1ab3d
Implement using of volume ramps in the player code. This MAY or MAY NOT be
correct, as we are setting the new value as the current value and target as
the new value. Needs to be tested / verified against FT2 .. did it this way
because it was easiest (no need to compute future values by peeking upcoming
data.)
author | Matti Hamalainen <ccr@tnsp.org> |
---|---|
date | Fri, 05 Oct 2012 02:49:13 +0300 |
parents | a33e47232161 |
children | 8ac24d753304 |
line wrap: on
line source
/* * miniJSS - Module playing routines * Programmed and designed by Matti 'ccr' Hamalainen * (C) Copyright 2006-2009 Tecnic Software productions (TNSP) */ #include "jssplr.h" #include <string.h> #include <stdlib.h> // FIXME!! FIX ME! #include <math.h> /* Miscellaneous tables */ #define jmpNSineTable (256) static int *jmpSineTable = NULL; static const Sint16 jmpXMAmigaPeriodTab[13 * 8] = { 907, 900, 894, 887, 881, 875, 868, 862, 856, 850, 844, 838, 832, 826, 820, 814, 808, 802, 796, 791, 785, 779, 774, 768, 762, 757, 752, 746, 741, 736, 730, 725, 720, 715, 709, 704, 699, 694, 689, 684, 678, 675, 670, 665, 660, 655, 651, 646, 640, 636, 632, 628, 623, 619, 614, 610, 604, 601, 597, 592, 588, 584, 580, 575, 570, 567, 563, 559, 555, 551, 547, 543, 538, 535, 532, 528, 524, 520, 516, 513, 508, 505, 502, 498, 494, 491, 487, 484, 480, 477, 474, 470, 467, 463, 460, 457, 453, 450, 447, 443, 440, 437, 434, 431 }; #define jmpNMODEffectTable (36) static const char jmpMODEffectTable[jmpNMODEffectTable] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; /* Helper functions */ int jmpNoteToAmigaPeriod(int note, int finetune) { int tmp = (note + finetune + 8); if (tmp < 0) tmp = 0; else if (tmp > 103) tmp = 103; return jmpXMAmigaPeriodTab[tmp]; } static int jmpGetPeriodFromNote(JSSPlayer *mp, int note, int finetune) { int res; if (JMPGETMODFLAGS(mp, jmdfAmigaPeriods)) { int mfinetune = finetune / 16, mnote = (note % 12) * 8, moctave = note / 12, period1, period2; period1 = jmpNoteToAmigaPeriod(mnote, mfinetune); if (finetune < 0) { mfinetune--; finetune = -finetune; } else mfinetune++; period2 = jmpNoteToAmigaPeriod(mnote, mfinetune); mfinetune = finetune & 15; period1 *= (16 - mfinetune); period2 *= mfinetune; res = ((period1 + period2) * 2) >> moctave; //fprintf(stderr, "jmpGetAmigaPeriod(%d, %d) = %d\n", note, finetune, res); } else { //fprintf(stderr, "jmpGetLinearPeriod(%d, %d) = %d\n", note, finetune, res); //res = ((120 - note) << 6) - (finetune / 2); res = 7680 - (note * 64) - (finetune / 2); if (res < 1) res = 1; } return res; } static void jmpCSetPitch(JSSPlayer *mp, int channel, int value) { assert(mp != NULL); assert(mp->device != NULL); if (value > 0) { if (JMPGETMODFLAGS(mp, jmdfAmigaPeriods)) { // Frequency = 8363*1712 / Period //fprintf(stderr, "jmpCSetPitch::AMIGA(%d, %d)\n", channel, value); jvmSetFreq(mp->device, channel, 14317456.0f / (double) value); } else { // Frequency = Frequency = 8363*2^((6*12*16*4 - Period) / (12*16*4)) //fprintf(stderr, "jmpCSetPitch::Linear(%d, %d)\n", channel, value); jvmSetFreq(mp->device, channel, 8363.0f * pow(2.0f, (4608.0f - (double) value) / 768.0f)); } } } static void jmpCSetVolume(JSSPlayer * mp, JSSPlayerChannel *chn, int channel, int volume) { assert(mp != NULL); assert(mp->device != NULL); if (volume < mpMinVol) volume = mpMinVol; else if (volume > mpMaxVol) volume = mpMaxVol; //fprintf(stderr, "chn %d: vol=%d, fad=%d, env=%d\n", channel, volume, chn->iCFadeOutVol, chn->iCVolEnv); /* jvmSetVolume(mp->device, channel, (chn->iCFadeOutVol * chn->iCVolEnv * volume) / (16 * 65536)); */ jvmSetVolumeRamp(mp->device, channel, jvmGetVolume(mp->device, channel), (chn->iCFadeOutVol * chn->iCVolEnv * volume) / (16 * 65536)); } static void jmpCSetPanning(JSSPlayer * mp, JSSPlayerChannel *chn, int channel, int panning) { assert(mp != NULL); assert(mp->device != NULL); jvmSetPan(mp->device, channel, panning + (((chn->iCPanEnv - 32) * (128 - abs(panning - 128))) / 32)); } static void jmpCSetPosition(JSSPlayer * mp, int channel, int value) { assert(mp != NULL); assert(mp->device != NULL); jvmSetPos(mp->device, channel, value); } static int jmpFindEnvPoint(JSSEnvelope * env, const int pos) { int i; for (i = 0; i < env->npoints - 1; i++) { if (env->points[i].frame <= pos && env->points[i + 1].frame >= pos) return i; } return -1; } static void jmpExecEnvelope(JSSEnvelope * env, BOOL keyOff, int * frames, BOOL * doExec, int * result) { int currPoint, delta; JSSEnvelopePoint *ipf1, *ipf2; // OK, find the current point based on frame currPoint = jmpFindEnvPoint(env, *frames); // Check if the envelope has ended if (currPoint < 0 && (env->flags & jenvfLooped) == 0) { *doExec = FALSE; return; } // Do the envelope looping here, if needed if ((env->flags & jenvfLooped) && *frames >= env->points[env->loopE].frame) { currPoint = env->loopS; *frames = env->points[currPoint].frame; } // If the current point is OK, then process the envelope if (currPoint >= 0) { // Linearly interpolate the value for given frame ipf1 = &env->points[currPoint]; ipf2 = &env->points[currPoint + 1]; delta = (ipf2->frame - ipf1->frame); if (delta > 0) *result = ipf1->value + ((ipf2->value - ipf1->value) * (*frames - ipf1->frame)) / delta; else *result = ipf1->value; } // The frame counter IS processed even if the envelope is not! if ((env->flags & jenvfSustain) && currPoint == env->sustain && env->points[currPoint].frame == env->points[env->sustain].frame) { if (keyOff) (*frames)++; } else (*frames)++; } static void jmpProcessExtInstrument(JSSPlayerChannel *chn, int channel) { JSSExtInstrument *inst = chn->iCExtInstrument; (void) channel; // Get the instrument for envelope data if (!inst) return; // Process the autovibrato /* FIXME fix me FIX me!!! todo. */ // Process the volume envelope if (inst->volumeEnv.flags & jenvfUsed) { // Process the instrument volume fadeout if (chn->iCKeyOff && chn->iCFadeOutVol > 0 && inst->fadeOut > 0) { int tmp = chn->iCFadeOutVol - inst->fadeOut; if (tmp < 0) tmp = 0; chn->iCFadeOutVol = tmp; JMPSETNDFLAGS(cdfNewVolume); } if (chn->iCVolEnv_Exec) { // Execute the volume envelope jmpExecEnvelope(&(inst->volumeEnv), chn->iCKeyOff, &(chn->iCVolEnv_Frames), &(chn->iCVolEnv_Exec), &(chn->iCVolEnv)); JMPSETNDFLAGS(cdfNewVolume); } } else { // If the envelope is not used, set max volume if (chn->iCVolEnv != mpMaxVol) { chn->iCVolEnv = mpMaxVol; chn->iCFadeOutVol = mpMaxFadeoutVol; JMPSETNDFLAGS(cdfNewVolume); } } // Process the panning envelope if (inst->panningEnv.flags & jenvfUsed) { if (chn->iCPanEnv_Exec) { // Execute the panning envelope jmpExecEnvelope(&(inst->panningEnv), chn->iCKeyOff, &(chn->iCPanEnv_Frames), &(chn->iCPanEnv_Exec), &(chn->iCPanEnv)); JMPSETNDFLAGS(cdfNewPanPos); } } else { // If the envelope is not used, set center panning if (chn->iCPanEnv != mpPanCenter) { chn->iCPanEnv = mpPanCenter; JMPSETNDFLAGS(cdfNewPanPos); } } } /* * The player */ JSSPlayer *jmpInit(JSSMixer *pDevice) { JSSPlayer *mp; // Initialize global tables if (jmpSineTable == NULL) { int i; if ((jmpSineTable = dmMalloc(jmpNSineTable * sizeof(int))) == NULL) JSSERROR(DMERR_MALLOC, NULL, "Could not allocate memory for sinus table.\n"); for (i = 0; i < 256; i++) { float f = ((float) i * M_PI * 2.0f) / 256.0f; jmpSineTable[i] = (int) (sin(f) * 2048.0f); } } // Allocate a player structure mp = dmMalloc0(sizeof(JSSPlayer)); if (!mp) JSSERROR(DMERR_MALLOC, NULL, "Could not allocate memory for player structure.\n"); // Set variables mp->device = pDevice; #ifdef JSS_SUP_THREADS mp->mutex = dmCreateMutex(); #endif return mp; } int jmpClose(JSSPlayer * mp) { if (mp == NULL) return DMERR_NULLPTR; // Stop player jmpStop(mp); // Deallocate resources #ifdef JSS_SUP_THREADS dmDestroyMutex(mp->mutex); #endif // Clear structure memset(mp, 0, sizeof(JSSPlayer)); dmFree(mp); return DMERR_OK; } /* Reset the envelopes for given channel. */ static void jmpResetEnvelopes(JSSPlayerChannel *chn) { chn->iCPanEnv_Frames = chn->iCVolEnv_Frames = 0; chn->iCPanEnv_Exec = chn->iCVolEnv_Exec = TRUE; } /* Clear module player structure */ void jmpClearChannel(JSSPlayerChannel *chn) { memset(chn, 0, sizeof(JSSPlayerChannel)); chn->iCNote = jsetNotSet; chn->iCInstrumentN = jsetNotSet; chn->iCExtInstrumentN = jsetNotSet; chn->iCPanning = mpPanCenter; chn->iCPanEnv = mpPanCenter; } void jmpClearPlayer(JSSPlayer * mp) { int i; assert(mp != NULL); JSS_LOCK(mp); // Initialize general variables mp->patternDelay = 0; mp->newRowSet = FALSE; mp->newOrderSet = FALSE; mp->tick = jsetNotSet; mp->row = 0; mp->lastPatLoopRow = 0; // Initialize channel data for (i = 0; i < jsetNChannels; i++) jmpClearChannel(&mp->channels[i]); JSS_UNLOCK(mp); } /* Set module */ void jmpSetModule(JSSPlayer * mp, JSSModule * module) { assert(mp != NULL); JSS_LOCK(mp); jmpStop(mp); jmpClearPlayer(mp); mp->module = module; JSS_UNLOCK(mp); } /* Stop playing */ void jmpStop(JSSPlayer * mp) { assert(mp != NULL); JSS_LOCK(mp); if (mp->isPlaying) { jvmRemoveCallback(mp->device); mp->isPlaying = FALSE; } JSS_UNLOCK(mp); } /* Resume playing */ void jmpResume(JSSPlayer * mp) { assert(mp != NULL); JSS_LOCK(mp); if (!mp->isPlaying) { int result = jvmSetCallback(mp->device, jmpExec, (void *) mp); if (result != DMERR_OK) JSSERROR(result,, "Could not initialize callback for player.\n"); mp->isPlaying = TRUE; } JSS_UNLOCK(mp); } /* Sets new order using given value as reference. * Jumps over skip-points and invalid values, loops * to first order if enabled. */ static void jmpSetNewOrder(JSSPlayer * mp, int order) { BOOL orderOK; int pattern; pattern = jsetOrderEnd; mp->order = jsetNotSet; orderOK = FALSE; while (!orderOK) { if (order < 0 || order >= mp->module->norders) { jmpStop(mp); orderOK = TRUE; } else { pattern = mp->module->orderList[order]; if (pattern == jsetOrderSkip) { order++; } else if (pattern >= mp->module->npatterns || pattern == jsetOrderEnd) { jmpStop(mp); orderOK = TRUE; } else { // All OK orderOK = TRUE; mp->pattern = mp->module->patterns[pattern]; mp->npattern = pattern; mp->order = order; } } } } /* Set new tempo-value of the player. */ void jmpSetTempo(JSSPlayer * mp, int tempo) { assert(mp != NULL); JSS_LOCK(mp); assert(mp->device != NULL); mp->tempo = tempo; jvmSetCallbackFreq(mp->device, (tempo * 2) / 5); JSS_UNLOCK(mp); } void jmpClearChannels(JSSPlayer * mp) { int i; assert(mp != NULL); JSS_LOCK(mp); assert(mp->device != NULL); assert(mp->module != NULL); for (i = 0; i < mp->module->nchannels; i++) { jvmStop(mp->device, i); jvmClear(mp->device, i); } JSS_UNLOCK(mp); } /* Starts playing module from a given ORDER. */ int jmpPlayOrder(JSSPlayer * mp, int order) { int result; assert(mp != NULL); JSS_LOCK(mp); assert(mp->module != NULL); // Stop if already playing jmpStop(mp); jmpClearChannels(mp); // Check starting order if (order < 0 || order >= mp->module->norders) { JSS_UNLOCK(mp); JSSERROR(DMERR_INVALID_ARGS, DMERR_INVALID_ARGS, "Invalid playing startorder given.\n"); } // Initialize playing jmpClearPlayer(mp); jmpSetNewOrder(mp, order); if (mp->order == jsetNotSet) { JSS_UNLOCK(mp); JSSERROR(DMERR_NOT_SUPPORTED, DMERR_NOT_SUPPORTED, "Could not start playing from given order #%i\n", order); } mp->speed = mp->module->defSpeed; jmpSetTempo(mp, mp->module->defTempo); // Set callback result = jvmSetCallback(mp->device, jmpExec, (void *) mp); if (result != DMERR_OK) { JSS_UNLOCK(mp); JSSERROR(result, result, "Could not initialize callback for player.\n"); } mp->isPlaying = TRUE; JSS_UNLOCK(mp); return 0; } /* Play given pattern */ int jmpPlayPattern(JSSPlayer * mp, int pattern) { int result; assert(mp != NULL); JSS_LOCK(mp); assert(mp->module != NULL); // Stop if already playing jmpStop(mp); jmpClearChannels(mp); // Initialize playing jmpClearPlayer(mp); mp->npattern = pattern; mp->speed = mp->module->defSpeed; jmpSetTempo(mp, mp->module->defTempo); // Set callback result = jvmSetCallback(mp->device, jmpExec, (void *) mp); if (result != DMERR_OK) { JSS_UNLOCK(mp); JSSERROR(result, result, "Could not initialize callback for player.\n"); } mp->isPlaying = TRUE; JSS_UNLOCK(mp); return 0; } /* Set volume for given module channel. */ static void jmpSetVolume(JSSPlayerChannel * chn, int channel, int volume) { (void) channel; // Check values if (volume < mpMinVol) volume = mpMinVol; else if (volume > mpMaxVol) volume = mpMaxVol; // Set the volume chn->iCVolume = volume; JMPSETNDFLAGS(cdfNewVolume); } #define jmpChangeVolume(Q, Z, X) jmpSetVolume(Q, Z, chn->iCVolume + (X)) /* Change the pitch of given channel by ADelta. */ static void jmpChangePitch(JSSPlayerChannel *chn, int channel, int delta) { int value; (void) channel; // Calculate new pitch and check it value = chn->iCPitch + delta; if (value < 0) value = 0; // Set the new pitch chn->iCPitch = value; JMPSETNDFLAGS(cdfNewPitch); } /* Do a note portamento (pitch slide) effect for given module channel. */ static void jmpDoPortamento(JSSPlayerChannel * chn, int channel) { (void) channel; // Check for zero parameter if (chn->iLastPortaToNoteParam == 0) { JMPSETNDFLAGS(cdfNewPitch); return; } /* Slide the pitch of channel to the destination value * with speed of iLastPortaToNoteParam[] * 4 and stop when it equals. */ if (chn->iCPitch != chn->iLastPortaToNotePitch) { if (chn->iCPitch < chn->iLastPortaToNotePitch) { // Increase pitch UP jmpChangePitch(chn, channel, chn->iLastPortaToNoteParam * 4); if (chn->iCPitch > chn->iLastPortaToNotePitch) chn->iCPitch = chn->iLastPortaToNotePitch; } else { // Decrease pitch DOWN jmpChangePitch(chn, channel, -(chn->iLastPortaToNoteParam * 4)); if (chn->iCPitch < chn->iLastPortaToNotePitch) chn->iCPitch = chn->iLastPortaToNotePitch; } } } /* Do a tremolo effect for given module channel. */ static void jmpDoTremolo(JSSPlayer * mp, JSSPlayerChannel *chn, int channel) { int delta, pos, depth; // Check settings if (chn->iTremoloDepth == 0 || chn->iTremoloSpeed == 0) return; // Get position of tremolo waveform pos = chn->iTremoloPos & 255; depth = chn->iTremoloDepth; switch (chn->iTremoloWC & 3) { case 0: // Sine-wave delta = (jmpSineTable[pos] * depth) / 2048; break; case 1: // Ramp down delta = ((pos - 128) * depth) / 128; break; case 2: // Square delta = (((pos & 128) - 64) * depth) / 64; break; default: return; } // Set the new volume jmpCSetVolume(mp, chn, channel, chn->iCVolume + delta); // Advance tremolo waveform position chn->iTremoloPos += chn->iTremoloSpeed; if (chn->iTremoloPos > 255) chn->iTremoloPos = 0; } /* Do a vibrato effect for given module channel. */ static void jmpDoVibrato(JSSPlayer * mp, JSSPlayerChannel *chn, int channel) { int delta, pos, depth; // Check settings if (chn->iVibratoDepth == 0 || chn->iVibratoSpeed == 0) return; // Get position of vibrato waveform pos = chn->iVibratoPos & 255; depth = chn->iVibratoDepth; switch (chn->iVibratoWC & 3) { case 0: // Sine-wave delta = (jmpSineTable[pos] * depth) / 2048; break; case 1: // Ramp down delta = ((pos - 128) * depth) / 16; break; case 2: // Square delta = (((pos & 128) - 64) * depth) / 8; break; default: return; } // Set the new frequency jmpCSetPitch(mp, channel, chn->iCPitch + delta); // Advance vibrato waveform position chn->iVibratoPos += chn->iVibratoSpeed; if (chn->iVibratoPos > 255) chn->iVibratoPos = 0; } /* Do a volume slide effect for given module channel. */ static void jmpDoVolumeSlide(JSSPlayerChannel * chn, int channel, int param) { int paramX, paramY; JMPMAKEPARAM(param, paramX, paramY) if (paramY == 0) jmpChangeVolume(chn, channel, paramX); if (paramX == 0) jmpChangeVolume(chn, channel, -paramY); } /* Execute a pattern loop effect/command for given module channel. * * This routine works for most of the supported formats, as they * use the 'standard' implementation ascending from MOD. However, * here is included also a slightly kludgy implementation of the * FT2 patloop bug. */ static void jmpDoPatternLoop(JSSPlayer * mp, JSSPlayerChannel *chn, int channel, int paramY) { (void) channel; // Check what we need to do if (paramY > 0) { // SBx/E6x loops 'x' times if (chn->iPatLoopCount == 1) chn->iPatLoopCount = 0; else { // Check if we need to set the count if (chn->iPatLoopCount == 0) chn->iPatLoopCount = (paramY + 1); // Loop to specified row chn->iPatLoopCount--; mp->newRow = chn->iPatLoopRow; mp->newRowSet = TRUE; } } else { // SB0/E60 sets the loop start point chn->iPatLoopRow = mp->row; // This is here because of the infamous FT2 patloop bug mp->lastPatLoopRow = mp->row; } } /* Do arpeggio effect */ static void jmpDoArpeggio(JSSPlayer * mp, JSSPlayerChannel *chn, int channel, int paramY, int paramX) { JSSInstrument *tempInst = chn->iCInstrument; if (tempInst) { int tmp = chn->iCNote; if (tmp == jsetNotSet || tmp == jsetNoteOff) return; switch (mp->tick & 3) { case 1: tmp += paramX; break; case 2: tmp += paramY; break; } jmpCSetPitch(mp, channel, jmpGetPeriodFromNote(mp, tmp + tempInst->ERelNote, tempInst->EFineTune)); } } /* * Process pattern effects */ static void jmpProcessRowEffect(JSSPlayer * mp, int channel, JSSNote * currNote) { JSSPlayerChannel *chn = &(mp->channels[channel]); int param, paramX, paramY; char effect; param = currNote->param; JMPMAKEPARAM(param, paramX, paramY); JMPGETEFFECT(effect, currNote->effect); switch (effect) { case '0': // 0xy = Arpeggio jmpDoArpeggio(mp, chn, channel, paramX, paramY); break; case 'W': // Used widely in demo-music as MIDAS Sound System sync-command case 'Q': // SoundTracker/OpenCP: Qxx = Set LP filter resonance case 'Z': // SoundTracker/OpenCP: Zxx = Set LP filter cutoff freq break; case '1': case '2': // 1xy = Portamento Up, 2xy = Portamento Down : IMPL.VERIFIED if (param) chn->iLastPortaParam = param; break; case '3': // 3xy = Porta To Note if (param) chn->iLastPortaToNoteParam = param; if (currNote->note != jsetNotSet && currNote->note != jsetNoteOff) { chn->iLastPortaToNotePitch = chn->iCPitch; chn->iCPitch = chn->iCOldPitch; JMPUNSETNDFLAGS(cdfNewPitch | cdfNewInstr | cdfNewPanPos); } break; case '4': // 4xy = Vibrato : IMPL.VERIFIED if (paramX) chn->iVibratoSpeed = paramX; if (paramY) chn->iVibratoDepth = paramY; if ((chn->iVibratoWC & 4) == 0) chn->iVibratoPos = 0; break; case '5': // 5xy = Portamento + Volume Slide case '6': // 6xy = Vibrato + Volume slide if (param) chn->iLastVolSlideParam = param; break; case '7': // 7xy = Tremolo if (paramX) chn->iTremoloSpeed = paramX; if (paramY) chn->iTremoloDepth = paramY; if ((chn->iTremoloWC & 4) == 0) chn->iTremoloPos = 0; break; case '8': // 8xx = Set Panning JMPDEBUG("Set Panning used, UNIMPLEMENTED"); break; case '9': // 9xx = Set Sample Offset : IMPL.VERIFIED if (chn->iCNewDataFlags & cdfNewPitch) { chn->iCPosition = param * 0x100; JMPSETNDFLAGS(cdfNewPos); } break; case 'A': // Axy = Volume Slide : IMPL.VERIFIED if (param) chn->iLastVolSlideParam = param; break; case 'B': // Bxx = Pattern Jump : IMPL.VERIFIED mp->newOrder = param; mp->newOrderSet = TRUE; mp->jumpFlag = TRUE; mp->lastPatLoopRow = 0; break; case 'C': // Cxx = Set Volume : IMPL.VERIFIED jmpSetVolume(chn, channel, param); break; case 'D': // Dxx = Pattern Break : IMPL.VERIFIED // Compute the new row mp->newRow = (paramX * 10) + paramY; if (mp->newRow >= mp->pattern->nrows) mp->newRow = 0; mp->newRowSet = TRUE; // Now we do some tricky tests if (!mp->breakFlag && !mp->jumpFlag) { mp->newOrder = mp->order + 1; mp->newOrderSet = TRUE; } mp->breakFlag = TRUE; break; case 'E': // Exy = Special Effects switch (paramX) { case 0x00: // E0x - Set filter (NOT SUPPORTED) JMPDEBUG("Set Filter used, UNSUPPORTED"); break; case 0x01: // E1x - Fine Portamento Up if (paramY) chn->iCLastFinePortamentoUpParam = paramY; jmpChangePitch(chn, channel, -(chn->iCLastFinePortamentoUpParam * 4)); break; case 0x02: // E2x - Fine Portamento Down if (paramY) chn->iCLastFinePortamentoDownParam = paramY; jmpChangePitch(chn, channel, (chn->iCLastFinePortamentoDownParam * 4)); break; case 0x03: // E3x - Glissando Control (NOT SUPPORTED) break; case 0x04: // E4x - Set Vibrato waveform chn->iVibratoWC = paramY; break; case 0x05: // E5x - Set Finetune JMPDEBUG("Set Finetune used, UNIMPLEMENTED"); break; case 0x06: // E6x - Set Pattern Loop jmpDoPatternLoop(mp, chn, channel, paramY); break; case 0x07: // E7x - Set Tremolo waveform chn->iTremoloWC = paramY; break; case 0x08: // E8x - Set Pan Position chn->iCPanning = (paramY * 16); JMPSETNDFLAGS(cdfNewPanPos); break; case 0x09: // E9x - Retrig note JMPDEBUG("Retrig Note used, UNIMPLEMENTED"); break; case 0x0a: // EAx - Fine Volumeslide Up if (paramY) chn->iCLastFineVolumeslideUpParam = paramY; jmpChangeVolume(chn, channel, chn->iCLastFineVolumeslideUpParam); break; case 0x0b: // EBx - Fine Volumeslide Down if (paramY) chn->iCLastFineVolumeslideDownParam = paramY; jmpChangeVolume(chn, channel, -(chn->iCLastFineVolumeslideDownParam)); break; case 0x0c: // ECx - Set Note Cut (NOT PROCESSED IN TICK0) break; case 0x0d: // EDx - Set Note Delay : IMPL.VERIFIED if (paramY > 0) { // Save the ND-flags, then clear chn->iSaveNDFlags = chn->iCNewDataFlags; chn->iCNewDataFlags = 0; // TODO .. does this only affect NOTE or also instrument? } break; case 0x0e: // EEx - Set Pattern Delay : IMPL.VERIFIED mp->patternDelay = paramY; break; case 0x0f: // EFx - Invert Loop (NOT SUPPORTED) JMPDEBUG("Invert Loop used, UNSUPPORTED"); break; default: JMPDEBUG("Unsupported special command used"); } break; case 'F': // Fxy = Set Speed / Tempo : IMPL.VERIFIED if (param > 0) { if (param < 0x20) mp->speed = param; else jmpSetTempo(mp, param); } break; case 'G': // Gxx = Global Volume mp->globalVol = param; JMPSETNDFLAGS(cdfNewGlobalVol); break; case 'H': // Hxx = Global Volume Slide JMPDEBUG("Global Volume Slide used, UNIMPLEMENTED"); break; case 'K': // Kxx = Key-off (Same as key-off note) chn->iCKeyOff = TRUE; break; case 'L': // Lxx = Set Envelope Position JMPDEBUG("Set Envelope Position used, NOT verified with FT2"); chn->iCPanEnv_Frames = param; chn->iCVolEnv_Frames = param; chn->iCPanEnv_Exec = TRUE; chn->iCVolEnv_Exec = TRUE; break; case 'R': // Rxy = Multi Retrig note JMPDEBUG("Multi Retrig Note used, UNIMPLEMENTED"); break; case 'T': // Txy = Tremor if (param) chn->iLastTremorParam = param; break; case 'X': // Xxy = Extra Fine Portamento switch (paramX) { case 0x01: // X1y - Extra Fine Portamento Up if (paramY) chn->iCLastExtraFinePortamentoUpParam = paramY; jmpChangePitch(chn, channel, - chn->iCLastExtraFinePortamentoUpParam); break; case 0x02: // X2y - Extra Fine Portamento Down if (paramY) chn->iCLastExtraFinePortamentoDownParam = paramY; jmpChangePitch(chn, channel, chn->iCLastExtraFinePortamentoUpParam); break; default: JMPDEBUG("Unsupported value in Extra Fine Portamento command!"); break; } break; default: JMPDEBUG("Unsupported effect"); break; } } static void jmpProcessNewRow(JSSPlayer * mp, int channel) { JSSNote *currNote; JSSExtInstrument *extInst = NULL; JSSInstrument *inst = NULL; BOOL newNote = FALSE; int tmp, paramX, paramY; JSSPlayerChannel *chn = &(mp->channels[channel]); JMPGETNOTE(currNote, mp->row, channel); // Check for a new note/keyoff here if (currNote->note == jsetNoteOff) chn->iCKeyOff = TRUE; else if (currNote->note >= 0 && currNote->note <= 96) { // New note was set newNote = TRUE; chn->iCNote = currNote->note; } // Check for new instrument if (currNote->instrument != jsetNotSet) { /* Envelopes and ext.instrument fadeout are initialized always if * new instrument is set, even if the instrument does not exist. */ jmpResetEnvelopes(chn); chn->iCKeyOff = FALSE; chn->iCFadeOutVol = mpMaxFadeoutVol; // We save the instrument number here for later use if (currNote->instrument >= 0 && currNote->instrument < mp->module->nextInstruments) chn->iCExtInstrumentN = currNote->instrument; } /* ONLY if newNote was SET NOW and ExtInstrument HAS BEEN set, we can * set new pitches, and other things... */ if (newNote) { if (chn->iCExtInstrumentN != jsetNotSet) extInst = mp->module->extInstruments[chn->iCExtInstrumentN]; else extInst = NULL; if (extInst) { // Set instrument int note = chn->iCNote; chn->iCExtInstrument = extInst; // We set new Instrument ONLY if NEW NOTE has been set if (note != jsetNotSet) { // Get instrument number tmp = extInst->sNumForNotes[note]; if (tmp >= 0 && tmp < mp->module->ninstruments) { // Set the new instrument inst = mp->module->instruments[tmp]; chn->iCInstrumentN = tmp; chn->iCInstrument = inst; chn->iCVolume = inst->volume; chn->iCPanning = inst->EPanning; chn->iCPosition = 0; // Set NDFlags JMPSETNDFLAGS(cdfNewInstr | cdfNewPos | cdfNewPanPos | cdfNewVolume); } } } } if (inst) { // Save old pitch for later use chn->iCOldPitch = chn->iCPitch; // Compute new pitch tmp = (chn->iCNote + inst->ERelNote); //fprintf(stderr, "HEH: %d + %d = %d\n", chn->iCNote, inst->ERelNote, tmp); if (tmp < 0) tmp = 0; else if (tmp > 119) tmp = 119; chn->iCPitch = jmpGetPeriodFromNote(mp, tmp, inst->EFineTune); JMPSETNDFLAGS(cdfNewPitch); } // Process the volume column JMPMAKEPARAM(currNote->volume, paramX, paramY); switch (paramX) { case 0x00: case 0x01: case 0x02: case 0x03: case 0x04: jmpSetVolume(chn, channel, currNote->volume); break; case 0x07: // Dx = Fine Volumeslide Down : IMPL.VERIFIED jmpChangeVolume(chn, channel, -paramY); break; case 0x08: // Ux = Fine Volumeslide Up : IMPL.VERIFIED jmpChangeVolume(chn, channel, paramY); break; case 0x09: // Sx = Set vibrato speed : IMPL.VERIFIED chn->iVibratoSpeed = paramY; break; case 0x0a: // Vx = Vibrato : IMPL.VERIFIED if (paramY) chn->iVibratoDepth = paramY; break; case 0x0e: // Mx = Porta To Note : IMPL.VERIFIED if (paramY) chn->iLastPortaToNoteParam = paramY; if (currNote->note != jsetNotSet && currNote->note != jsetNoteOff) { chn->iLastPortaToNotePitch = chn->iCPitch; chn->iCPitch = chn->iCOldPitch; JMPUNSETNDFLAGS(cdfNewPitch | cdfNewInstr | cdfNewPanPos); } break; } // ...And finally process the Normal effects if (currNote->effect != jsetNotSet) jmpProcessRowEffect(mp, channel, currNote); } static void jmpProcessEffects(JSSPlayer * mp, int channel) { JSSPlayerChannel *chn = &(mp->channels[channel]); JSSNote *currNote; int param, paramX, paramY, tmp; char effect; // Process the volume column effects JMPGETNOTE(currNote, mp->row, channel); JMPMAKEPARAM(currNote->volume, paramX, paramY); switch (paramX) { case 0x05: // -x = Volumeslide Down : IMPL.VERIFIED jmpChangeVolume(chn, channel, -paramY); break; case 0x06: // +x = Volumeslide Down : IMPL.VERIFIED jmpChangeVolume(chn, channel, paramY); break; case 0x0a: // Vx = Vibrato : IMPL.VERIFIED jmpDoVibrato(mp, chn, channel); break; case 0x0e: // Mx = Porta To Note : IMPL.VERIFIED jmpDoPortamento(chn, channel); break; } // ...And finally process the Normal effects if (currNote->effect == jsetNotSet) return; param = currNote->param; JMPMAKEPARAM(param, paramX, paramY); JMPGETEFFECT(effect, currNote->effect); switch (effect) { case '0': // 0xy = Arpeggio jmpDoArpeggio(mp, chn, channel, paramX, paramY); break; case '1': // 1xy = Portamento Up if (chn->iLastPortaParam > 0) jmpChangePitch(chn, channel, -(chn->iLastPortaParam * 4)); break; case '2': // 2xy = Portamento Down if (chn->iLastPortaParam > 0) jmpChangePitch(chn, channel, (chn->iLastPortaParam * 4)); break; case '3': // 3xy = Porta To Note jmpDoPortamento(chn, channel); break; case '4': // 4xy = Vibrato jmpDoVibrato(mp, chn, channel); break; case '5': // 5xy = Portamento + Volume Slide jmpDoPortamento(chn, channel); jmpDoVolumeSlide(chn, channel, chn->iLastVolSlideParam); break; case '6': // 6xy = Vibrato + Volume Slide jmpDoVibrato(mp, chn, channel); jmpDoVolumeSlide(chn, channel, chn->iLastVolSlideParam); break; case '7': // 7xy = Tremolo jmpDoTremolo(mp, chn, channel); break; case 'A': // Axy = Volume slide jmpDoVolumeSlide(chn, channel, chn->iLastVolSlideParam); break; case 'E': // Exy = Special Effects switch (paramX) { case 0x0c: // ECx - Set Note Cut if (mp->tick == paramY) jmpSetVolume(chn, channel, jsetMinVol); break; case 0x0d: // EDx - Set Note Delay if (mp->tick == paramY) chn->iCNewDataFlags = chn->iSaveNDFlags; break; } break; case 'T': // Txy = Tremor JMPMAKEPARAM(chn->iLastTremorParam, paramX, paramY) paramX++; paramY++; tmp = (chn->iTremorCount % (paramX + paramY)); if (tmp < paramX) jmpCSetVolume(mp, chn, channel, chn->iCVolume); else jmpCSetVolume(mp, chn, channel, jsetMinVol); chn->iTremorCount = (tmp + 1); break; } } /* This is the main processing callback-loop of a module player. * It processes the ticks, calling the needed jmpProcessNewRow() * and jmpProcessEffects() methods for processing the module playing. */ void jmpExec(void *pDEV, void *pMP) { JSSPlayer *mp; JSSMixer *dev; int channel, flags; // Check some things via assert() assert(pMP != NULL); mp = (JSSPlayer *) pMP; JSS_LOCK(mp); dev = (JSSMixer *) pDEV; assert(mp->device == dev); assert(mp->module != NULL); // Check if we are playing if (!mp->isPlaying) goto out; // Clear channel new data flags mp->jumpFlag = FALSE; mp->breakFlag = FALSE; for (channel = 0; channel < jsetNChannels; channel++) mp->channels[channel].iCNewDataFlags = 0; //fprintf(stderr, "1: tick=%d, order=%d, iPattern=%d, row=%d\n", mp->tick, mp->order, mp->npattern, mp->row); // Check for init-tick if (mp->tick < 0) { // Initialize pattern if (mp->order != jsetNotSet) jmpSetNewOrder(mp, mp->order); mp->newRow = 0; mp->newRowSet = TRUE; mp->tick = mp->speed; } //fprintf(stderr, "2: tick=%d, order=%d, iPattern=%d, row=%d\n", mp->tick, mp->order, mp->npattern, mp->row); // Check if we are playing if (!mp->isPlaying) goto out; assert(mp->pattern); // Update the tick mp->tick++; if (mp->tick >= mp->speed) { // Re-init tick counter mp->tick = 0; // Check pattern delay if (mp->patternDelay > 0) mp->patternDelay--; else { // New pattern row if (mp->newRowSet) { mp->row = mp->newRow; mp->newRowSet = FALSE; } else mp->row++; // Check for end of pattern if (mp->row >= mp->pattern->nrows) { // Go to next order if (mp->order != jsetNotSet) jmpSetNewOrder(mp, mp->order + 1); else mp->isPlaying = FALSE; // Check for FT2 quirks if (JMPGETMODFLAGS(mp, jmdfFT2Replay)) mp->row = mp->lastPatLoopRow; else mp->row = 0; } if (!mp->isPlaying) goto out; // Check current order if (mp->newOrderSet) { jmpSetNewOrder(mp, mp->newOrder); mp->newOrderSet = FALSE; } //fprintf(stderr, "3: tick=%d, order=%d, iPattern=%d, row=%d\n", mp->tick, mp->order, mp->npattern, mp->row); if (!mp->isPlaying) goto out; // TICK #0: Process new row for (channel = 0; channel < mp->module->nchannels; channel++) jmpProcessNewRow(mp, channel); } // patternDelay } // tick else { // Implement FT2's pattern delay-effect: don't update effects while on patdelay if (!JMPGETMODFLAGS(mp, jmdfFT2Replay) || (JMPGETMODFLAGS(mp, jmdfFT2Replay) && mp->patternDelay <= 0)) { // TICK n: Process the effects for (channel = 0; channel < mp->module->nchannels; channel++) jmpProcessEffects(mp, channel); } } // Check if playing has stopped if (!mp->isPlaying) goto out; // Update player data to audio device/mixer for (channel = 0; channel < mp->module->nchannels; channel++) { JSSPlayerChannel *chn = &mp->channels[channel]; // Process extended instruments jmpProcessExtInstrument(chn, channel); // Check NDFlags and update channel data flags = chn->iCNewDataFlags; if (flags) { // Check if we stop? if (flags & cdfStop) { jvmStop(mp->device, channel); } else { // No, handle other flags if (flags & cdfNewInstr) { JSSInstrument *instr = chn->iCInstrument; if (instr != NULL) { jvmSetSample(mp->device, channel, instr->data, instr->size, instr->loopS, instr->loopE, instr->flags); } jvmPlay(mp->device, channel); } if (flags & cdfNewPitch) jmpCSetPitch(mp, channel, chn->iCPitch); if (flags & cdfNewPos) jmpCSetPosition(mp, channel, chn->iCPosition); if (flags & cdfNewVolume) jmpCSetVolume(mp, chn, channel, chn->iCVolume); if (flags & cdfNewPanPos) jmpCSetPanning(mp, chn, channel, chn->iCPanning); if (flags & cdfNewGlobalVol) jvmSetGlobalVol(mp->device, mp->globalVol); } } } out: JSS_UNLOCK(mp); }