From 03aa58b1e1c104ad70f1f7811d1898ea91823daa Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sat, 18 Oct 2025 21:42:57 -0500 Subject: [PATCH] comment the playback code --- src/engine/engine.h | 3 +- src/engine/playback.cpp | 640 +++++++++++++++++++++++++++++++++++++--- src/gui/debugWindow.cpp | 1 - 3 files changed, 605 insertions(+), 39 deletions(-) diff --git a/src/engine/engine.h b/src/engine/engine.h index 9fb3f34a8..01a846e81 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -140,7 +140,7 @@ struct DivChannelState { int panDepth, panRate, panPos, panSpeed; int sampleOff; unsigned char arp, arpStage, arpTicks, panL, panR, panRL, panRR, lastVibrato, lastPorta, cutType; - bool doNote, legato, portaStop, keyOn, keyOff, nowYouCanStop, stopOnOff, releasing; + bool doNote, legato, portaStop, keyOn, keyOff, stopOnOff, releasing; bool arpYield, delayLocked, inPorta, scheduledSlideReset, shorthandPorta, wasShorthandPorta, noteOnInhibit, resetArp, sampleOffSet; bool wentThroughNote, goneThroughNote; @@ -197,7 +197,6 @@ struct DivChannelState { portaStop(false), keyOn(false), keyOff(false), - nowYouCanStop(true), stopOnOff(false), releasing(false), arpYield(false), diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index 9df1f6899..26c34b44d 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -17,6 +17,11 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +// this file contains most of the playback code. +// it is a mess due to the amount of compatibility flags that have been +// added over time, so I have tried my best to comment it. +// these compatibility flags are preceded by a "COMPAT FLAG" comment upon use. + #include "macroInt.h" #include #define _USE_MATH_DEFINES @@ -26,22 +31,27 @@ #include "../ta-log.h" #include +// go to next order void DivEngine::nextOrder() { curRow=0; if (repeatPattern) return; if (++curOrder>=curSubSong->ordersLen) { logV("end of orders reached"); endOfSong=true; + // the walked array is used for loop detection + // since we've reached the end, we are guaranteed to loop here, so + // just reset it. memset(walked,0,8192); curOrder=0; } } +// used for the pattern visualizer in console mode. static const char* notes[12]={ "C-", "C#", "D-", "D#", "E-", "F-", "F#", "G-", "G#", "A-", "A#", "B-" }; -// update this when adding new commands. +// update this when adding new commands in dispatch.h. const char* cmdName[]={ "NOTE_ON", "NOTE_OFF", @@ -318,8 +328,12 @@ const char* cmdName[]={ "FM_AMS2" }; +// fail build if you forgot to update the array static_assert((sizeof(cmdName)/sizeof(void*))==DIV_CMD_MAX,"update cmdName!"); +// formats a note +// used for the pattern visualizer in console mode, justifying the use +// of a static array. const char* formatNote(short note) { static char ret[16]; if (note==DIV_NOTE_OFF) { @@ -335,8 +349,11 @@ const char* formatNote(short note) { return ret; } +// send a command to a dispatch. int DivEngine::dispatchCmd(DivCommand c) { + // used for the commands visualizer in console mode if (view==DIV_STATUS_COMMANDS) { + // don't print if we are "skipping" (seeking to a position, usually after channel reset on loop) if (!skipping) { switch (c.cmd) { // strip away hinted/useless commands @@ -353,55 +370,73 @@ int DivEngine::dispatchCmd(DivCommand c) { case DIV_CMD_PRE_NOTE: break; default: + // print command printf("%8d | %d: %s(%d, %d)\n",totalTicksR,c.chan,cmdName[c.cmd],c.value,c.value2); } } } totalCmds++; + // up to 2000 commands can be queued in the command queue (used by the GUI for pattern visualizer) if (cmdStreamEnabled && cmdStream.size()<2000) { cmdStream.push_back(c); } + // MIDI output code + // we turn this command into MIDI messages if the output mode is "melodic" + // if the channel is outside the range 0-15, it will be wrapped back if (output) if (!skipping && output->midiOut!=NULL && !isChannelMuted(c.chan)) { if (output->midiOut->isDeviceOpen()) { if (midiOutMode==DIV_MIDI_MODE_NOTE) { + // scale volume to MIDI velocity range int scaledVol=(chan[c.chan].volume*127)/MAX(1,chan[c.chan].volMax); if (scaledVol<0) scaledVol=0; if (scaledVol>127) scaledVol=127; + + // process the command switch (c.cmd) { case DIV_CMD_NOTE_ON: case DIV_CMD_LEGATO: + // turn the previous note off (if we have one) if (chan[c.chan].curMidiNote>=0) { output->midiOut->send(TAMidiMessage(0x80|(c.chan&15),chan[c.chan].curMidiNote,scaledVol)); } + // set current MIDI note if (c.value!=DIV_NOTE_NULL) { chan[c.chan].curMidiNote=c.value+12; if (chan[c.chan].curMidiNote<0) chan[c.chan].curMidiNote=0; if (chan[c.chan].curMidiNote>127) chan[c.chan].curMidiNote=127; } + // send note on (if we have one) if (chan[c.chan].curMidiNote>=0) { output->midiOut->send(TAMidiMessage(0x90|(c.chan&15),chan[c.chan].curMidiNote,scaledVol)); } break; case DIV_CMD_NOTE_OFF: case DIV_CMD_NOTE_OFF_ENV: + // turn the current note off (if we have one) + // we don't do this for macro release... if (chan[c.chan].curMidiNote>=0) { output->midiOut->send(TAMidiMessage(0x80|(c.chan&15),chan[c.chan].curMidiNote,scaledVol)); } chan[c.chan].curMidiNote=-1; break; case DIV_CMD_INSTRUMENT: + // instrument changes mapped to program change + // only first 128 instruments if (chan[c.chan].lastIns!=c.value && midiOutProgramChange) { output->midiOut->send(TAMidiMessage(0xc0|(c.chan&15),c.value&0x7f,0)); } break; case DIV_CMD_VOLUME: + // volume changes are sent as MIDI aftertouch, as long as there isn't a note + // (processRow will set midiAftertouch to true on every row without note) if (chan[c.chan].curMidiNote>=0 && chan[c.chan].midiAftertouch) { chan[c.chan].midiAftertouch=false; output->midiOut->send(TAMidiMessage(0xa0|(c.chan&15),chan[c.chan].curMidiNote,scaledVol)); } break; case DIV_CMD_PITCH: { + // map pitch changes to pitch bend (including vibrato) int pitchBend=8192+(c.value<<5); if (pitchBend<0) pitchBend=0; if (pitchBend>16383) pitchBend=16383; @@ -412,6 +447,7 @@ int DivEngine::dispatchCmd(DivCommand c) { break; } case DIV_CMD_PANNING: { + // this is mapped to General MIDI panning CC int pan=convertPanSplitToLinearLR(c.value,c.value2,127); if (pan<0) pan=0; if (pan>127) pan=127; @@ -419,25 +455,40 @@ int DivEngine::dispatchCmd(DivCommand c) { break; } case DIV_CMD_HINT_PORTA: { + // portamento handling is complicated + // in General MIDI, portamento consists of sending a CC for duration and another for target note + // this differs from Furnace, where the parameter is speed rather than duration + // it is also impossible to perform an indefinite slide down/up other than + // by using pitch bend, but the default range is limited and we already + // use it for pitch changes/vibrato + + // only send portamento if it is enabling if (c.value2>0) { + // and only if we have a target note if (c.value<=0 || c.value>=255) break; //output->midiOut->send(TAMidiMessage(0x80|(c.chan&15),chan[c.chan].curMidiNote,scaledVol)); int target=c.value+12; if (target<0) target=0; if (target>127) target=127; + // set the source note? if (chan[c.chan].curMidiNote>=0) { output->midiOut->send(TAMidiMessage(0xb0|(c.chan&15),0x54,chan[c.chan].curMidiNote)); } + // set the duration + // no effort whatsoever is done to predict how long will the slide last output->midiOut->send(TAMidiMessage(0xb0|(c.chan&15),0x05,1/*MIN(0x7f,c.value2/4)*/)); + // turn portamento on output->midiOut->send(TAMidiMessage(0xb0|(c.chan&15),0x41,0x7f)); - + // send a note on (why?) output->midiOut->send(TAMidiMessage(0x90|(c.chan&15),target,scaledVol)); } else { + // disable portamento otherwise output->midiOut->send(TAMidiMessage(0xb0|(c.chan&15),0x41,0)); } break; } + // other commands are simply ignored default: break; } @@ -445,47 +496,61 @@ int DivEngine::dispatchCmd(DivCommand c) { } } + // map the channel to channel of chip + // c.dis is a copy of c.chan because we'll use it in the next call c.chan=dispatchChanOfChan[c.dis]; + // dispatch command to chip dispatch return disCont[dispatchOfChan[c.dis]].dispatch->dispatch(c); } +// this function handles per-chip normal effects bool DivEngine::perSystemEffect(int ch, unsigned char effect, unsigned char effectVal) { + // don't process invalid chips DivSysDef* sysDef=sysDefs[sysOfChan[ch]]; if (sysDef==NULL) return false; + // find the effect handler auto iter=sysDef->effectHandlers.find(effect); if (iter==sysDef->effectHandlers.end()) return false; EffectHandler handler=iter->second; int val=0; int val2=0; + // map values using the handler's function try { val=handler.val?handler.val(effect,effectVal):effectVal; val2=handler.val2?handler.val2(effect,effectVal):0; } catch (DivDoNotHandleEffect& e) { return false; } + // dispatch command // wouldn't this cause problems if it were to return 0? return dispatchCmd(DivCommand(handler.dispatchCmd,ch,val,val2)); } +// this handles per-chip post effects... bool DivEngine::perSystemPostEffect(int ch, unsigned char effect, unsigned char effectVal) { + // don't process invalid chips DivSysDef* sysDef=sysDefs[sysOfChan[ch]]; if (sysDef==NULL) return false; + // find the effect handler auto iter=sysDef->postEffectHandlers.find(effect); if (iter==sysDef->postEffectHandlers.end()) return false; EffectHandler handler=iter->second; int val=0; int val2=0; + // map values using the handler's function try { val=handler.val?handler.val(effect,effectVal):effectVal; val2=handler.val2?handler.val2(effect,effectVal):0; } catch (DivDoNotHandleEffect& e) { return true; } + // dispatch command // wouldn't this cause problems if it were to return 0? return dispatchCmd(DivCommand(handler.dispatchCmd,ch,val,val2)); } +// ...and this handles chip pre-effects bool DivEngine::perSystemPreEffect(int ch, unsigned char effect, unsigned char effectVal) { DivSysDef* sysDef=sysDefs[sysOfChan[ch]]; if (sysDef==NULL) return false; @@ -504,39 +569,64 @@ bool DivEngine::perSystemPreEffect(int ch, unsigned char effect, unsigned char e return dispatchCmd(DivCommand(handler.dispatchCmd,ch,val,val2)); } +// this is called by nextRow() before it calls processRow() +// `i` is the channel void DivEngine::processRowPre(int i) { int whatOrder=curOrder; int whatRow=curRow; DivPattern* pat=curPat[i].getPattern(curOrders->ord[i][whatOrder],false); + // check all effects for (int j=0; jnewData[whatRow][DIV_PAT_FX(j)]; short effectVal=pat->newData[whatRow][DIV_PAT_FXVAL(j)]; + // empty effect value is the same as zero if (effectVal==-1) effectVal=0; effectVal&=255; + + // per-chip pre-effects (that's it for now!) + // the other pre-effects are handled in processRow() perSystemPreEffect(i,effect,effectVal); } } +// this is called by nextRow() or nextTick() (in the case of delay). it processes the next row for a channel. +// `i` is the channel and `afterDelay` determines whether this happens after EDxx or not. +// the processing order is: +// 1. pre-effects (delay and song control) +// 2. instrument +// 3. note reading (note off is done immediately) +// 4. volume +// 5. effects +// 6. note on +// 7. post-effects void DivEngine::processRow(int i, bool afterDelay) { + // if this is after delay, use the order/row where delay occurred int whatOrder=afterDelay?chan[i].delayOrder:curOrder; int whatRow=afterDelay?chan[i].delayRow:curRow; DivPattern* pat=curPat[i].getPattern(curOrders->ord[i][whatOrder],false); // pre effects + // these include song control ones such as speed, tempo or jumps which shall not be delayed + // it also includes EDxx (delay) itself so we can handle it if (!afterDelay) { + // set to true if we found an EDxx effect bool returnAfterPre=false; + // check all effects for (int j=0; jnewData[whatRow][DIV_PAT_FX(j)]; short effectVal=pat->newData[whatRow][DIV_PAT_FXVAL(j)]; + // empty effect value is the same as zero if (effectVal==-1) effectVal=0; effectVal&=255; switch (effect) { case 0x09: // select groove pattern/speed 1 if (song.grooves.empty()) { + // special case: sets speed 1 if the song lacks groove patterns if (effectVal>0) speeds.val[0]=effectVal; } else { + // sets the groove pattern and resets current speed index if (effectVal<(short)song.grooves.size()) { speeds=song.grooves[effectVal]; curSpeed=0; @@ -544,9 +634,12 @@ void DivEngine::processRow(int i, bool afterDelay) { } break; case 0x0f: // speed 1/speed 2 + // if the value is 0 then ignore it if (speeds.len==2 && song.grooves.empty()) { + // if there are two speeds and no groove patterns, set the second speed if (effectVal>0) speeds.val[1]=effectVal; } else { + // otherwise set the first speed if (effectVal>0) speeds.val[0]=effectVal; } break; @@ -557,6 +650,15 @@ void DivEngine::processRow(int i, bool afterDelay) { if (effectVal>0) virtualTempoD=effectVal; break; case 0x0b: // change order + // this actually schedules an order change + // we perform this change at the end of nextRow() + + // COMPAT FLAG: simultaneous jump treatment + // - 0: normal (another 0Bxx effect will override the previous one) + // - 1: old Furnace (only the first 0Bxx effect in a row takes effect) + // - 2: DefleMask (same as 1) + // in the case of normal, the jump row (changePos) is not reset to 0 + // this means that you can do 0Dxx 0Byy and it'll work, taking you to row xx of order yy if (changeOrd==-1 || song.jumpTreatment==0) { changeOrd=effectVal; if (song.jumpTreatment==1 || song.jumpTreatment==2) { @@ -565,18 +667,28 @@ void DivEngine::processRow(int i, bool afterDelay) { } break; case 0x0d: // next order + // COMPAT FLAG: ignore 0Dxx on the last order (ignoreJumpAtEnd) + // if there is a 0Dxx effect on the very last order, it is ignored + + // COMPAT FLAG: simultaneous jump treatment if (song.jumpTreatment==2) { + // - 2: DefleMask (jump to next order unless it is the last one and ignoreJumpAtEnd is on) if ((curOrder<(curSubSong->ordersLen-1) || !song.ignoreJumpAtEnd)) { + // changeOrd -2 means increase order by 1 + // it overrides a previous 0Bxx effect changeOrd=-2; changePos=effectVal; } } else if (song.jumpTreatment==1) { + // - 1: old Furnace (same as 2 but ignored if 0Bxx is present) if (changeOrd<0 && (curOrder<(curSubSong->ordersLen-1) || !song.ignoreJumpAtEnd)) { changeOrd=-2; changePos=effectVal; } } else { + // - 0: normal if (curOrder<(curSubSong->ordersLen-1) || !song.ignoreJumpAtEnd) { + // set the target order if not set, allowing you to use 0B and 0D regardless of position if (changeOrd<0) { changeOrd=-2; } @@ -586,17 +698,35 @@ void DivEngine::processRow(int i, bool afterDelay) { break; case 0xed: // delay if (effectVal!=0) { + // COMPAT FLAG: cut/delay effect policy (delayBehavior) + // - 0: strict + // - delays equal or greater to the speed * timeBase are ignored + // - 1: strict old + // - delays equal or greater to the speed are ignored + // - 2: lax (default) + // - no delay is ever ignored unless overridden by another bool comparison=(song.delayBehavior==1)?(effectVal<=nextSpeed):(effectVal<(nextSpeed*(curSubSong->timeBase+1))); if (song.delayBehavior==2) comparison=true; if (comparison) { + // set the delay row, order and timer chan[i].rowDelay=effectVal; chan[i].delayOrder=whatOrder; chan[i].delayRow=whatRow; + + // this here was a compatibility hack for DefleMask... + // if the delay time happens to be equal to the speed, it'll + // result in "delay lock" which halts all row processing + // until another good EDxx effect is found + // for some reason this didn't occur on Neo Geo... + // this hack is disabled due to its dirtiness and the fact I + // don't feel like being compatible with a buggy tracker any further if (effectVal==nextSpeed) { //if (sysOfChan[i]!=DIV_SYSTEM_YM2610 && sysOfChan[i]!=DIV_SYSTEM_YM2610_EXT) chan[i].delayLocked=true; } else { chan[i].delayLocked=false; } + + // once we're done with pre-effects, get out and don't process any further returnAfterPre=true; } else { logV("higher than nextSpeed! %d>%d",effectVal,nextSpeed); @@ -606,20 +736,27 @@ void DivEngine::processRow(int i, bool afterDelay) { break; } } + // stop processing if EDxx was found if (returnAfterPre) return; } else { //logV("honoring delay at position %d",whatRow); } + // stop processing if delay lock is on (won't happen, ever) if (chan[i].delayLocked) return; + // now we start reading... // instrument bool insChanged=false; if (pat->newData[whatRow][DIV_PAT_INS]!=-1) { + // only send an instrument change if it differs from the current ins if (chan[i].lastIns!=pat->newData[whatRow][DIV_PAT_INS]) { dispatchCmd(DivCommand(DIV_CMD_INSTRUMENT,i,pat->newData[whatRow][DIV_PAT_INS])); chan[i].lastIns=pat->newData[whatRow][DIV_PAT_INS]; insChanged=true; + + // COMPAT FLAG: legacy volume slides + // - sets volume to max once a vol slide down has finished (thus setting volume to volMax+1) if (song.legacyVolumeSlides && chan[i].volume==chan[i].volMax+1) { logV("forcing volume"); chan[i].volume=chan[i].volMax; @@ -628,34 +765,46 @@ void DivEngine::processRow(int i, bool afterDelay) { } } } - // note + + // note reading + // note offs are sent immediately if (pat->newData[whatRow][DIV_PAT_NOTE]==DIV_NOTE_OFF) { // note off - //chan[i].note=-1; chan[i].keyOn=false; chan[i].keyOff=true; + + // COMPAT FLAG: reset slides on note off (inverted in the GUI) + // - a portamento/pitch slide will be halted upon encountering note off + // - this will not occur if the stopPortaOnNoteOff flag is on and this is a portamento if (chan[i].inPorta && song.noteOffResetsSlides) { + // stopOnOff will be false if stopPortaOnNoteOff flag is off if (chan[i].stopOnOff) { chan[i].portaNote=-1; chan[i].portaSpeed=-1; dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,CLAMP(chan[i].portaNote,-128,127),MAX(chan[i].portaSpeed,0))); chan[i].stopOnOff=false; } + // depending on the system, portamento may still be disabled if (disCont[dispatchOfChan[i]].dispatch->keyOffAffectsPorta(dispatchChanOfChan[i])) { chan[i].portaNote=-1; chan[i].portaSpeed=-1; dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,CLAMP(chan[i].portaNote,-128,127),MAX(chan[i].portaSpeed,0))); + // this here is a now-disabled hack which makes the noise channel also stop when square 3 is /*if (i==2 && sysOfChan[i]==DIV_SYSTEM_SMS) { chan[i+1].portaNote=-1; chan[i+1].portaSpeed=-1; }*/ } + // another compatibility hack which schedules a second reset later just in case chan[i].scheduledSlideReset=true; } + + // send note off dispatchCmd(DivCommand(DIV_CMD_NOTE_OFF,i)); } else if (pat->newData[whatRow][DIV_PAT_NOTE]==DIV_NOTE_REL) { // note off + env release //chan[i].note=-1; chan[i].keyOn=false; chan[i].keyOff=true; + // same thing here regarding reset slide behavior if (chan[i].inPorta && song.noteOffResetsSlides) { if (chan[i].stopOnOff) { chan[i].portaNote=-1; @@ -674,21 +823,30 @@ void DivEngine::processRow(int i, bool afterDelay) { } chan[i].scheduledSlideReset=true; } + + // send note release dispatchCmd(DivCommand(DIV_CMD_NOTE_OFF_ENV,i)); chan[i].releasing=true; } else if (pat->newData[whatRow][DIV_PAT_NOTE]==DIV_MACRO_REL) { // env release + // send macro release dispatchCmd(DivCommand(DIV_CMD_ENV_RELEASE,i)); chan[i].releasing=true; } else if (pat->newData[whatRow][DIV_PAT_NOTE]!=-1) { + // prepare/schedule a new note chan[i].oldNote=chan[i].note; chan[i].note=pat->newData[whatRow][DIV_PAT_NOTE]-60; + // I have no idea why is this check here since keyOn is guaranteed to be false at this point + // ...unless there's a way to trigger keyOn twice if (!chan[i].keyOn) { + // the behavior of arpeggio reset upon note off varies per system if (disCont[dispatchOfChan[i]].dispatch->keyOffAffectsArp(dispatchChanOfChan[i])) { chan[i].arp=0; dispatchCmd(DivCommand(DIV_CMD_HINT_ARPEGGIO,i,chan[i].arp)); } } chan[i].doNote=true; + // COMPAT FLAG: compatible arpeggio + // - once a new note plays, arp will not be applied for this tick if (chan[i].arp!=0 && song.compatibleArpeggio) { chan[i].arpYield=true; } @@ -697,11 +855,13 @@ void DivEngine::processRow(int i, bool afterDelay) { // volume int volPortaTarget=-1; bool noApplyVolume=false; + // here we read all effects and check for a volume slide with target/volume "portamento"/"scivolando" (a term I invented as an equivalent) for (int j=0; jnewData[whatRow][DIV_PAT_FX(j)]; if (effect==0xd3 || effect==0xd4) { // vol porta volPortaTarget=pat->newData[whatRow][DIV_PAT_VOL]<<8; // can be -256 + // empty effect value is treated as 0 short effectVal=pat->newData[whatRow][DIV_PAT_FXVAL(j)]; if (effectVal==-1) effectVal=0; effectVal&=255; @@ -711,19 +871,28 @@ void DivEngine::processRow(int i, bool afterDelay) { } } + // don't apply volume if a scivolando is set if (pat->newData[whatRow][DIV_PAT_VOL]!=-1 && !noApplyVolume) { + // COMPAT FLAG: legacy ALWAYS_SET_VOLUME behavior (oldAlwaysSetVolume) + // - prior to its addition, volume changes wouldn't be effective depending on the system if the volume is the same as the current one + // - afterwards, volume change is made regardless in order to set the bottom byte of volume ("subvolume") if (!song.oldAlwaysSetVolume || disCont[dispatchOfChan[i]].dispatch->getLegacyAlwaysSetVolume() || (MIN(chan[i].volMax,chan[i].volume)>>8)!=pat->newData[whatRow][DIV_PAT_VOL]) { + // here we let dispatchCmd() know we can do MIDI aftertouch if there isn't a note if (pat->newData[whatRow][DIV_PAT_NOTE]==-1) { chan[i].midiAftertouch=true; } + // set the volume (bottom byte is set to 0) chan[i].volume=pat->newData[whatRow][DIV_PAT_VOL]<<8; dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8)); dispatchCmd(DivCommand(DIV_CMD_HINT_VOLUME,i,chan[i].volume>>8)); } } + // reset retrigger status + // this is the only effect that takes place only in the row it is placed, in a ProTracker-like fashion (why?) chan[i].retrigSpeed=0; + // stuff necessary for effect processing short lastSlide=-1; bool calledPorta=false; bool panChanged=false; @@ -735,17 +904,22 @@ void DivEngine::processRow(int i, bool afterDelay) { short effect=pat->newData[whatRow][DIV_PAT_FX(j)]; short effectVal=pat->newData[whatRow][DIV_PAT_FXVAL(j)]; + // an empty effect value is treated as zero if (effectVal==-1) effectVal=0; effectVal&=255; // per-system effect + // if there isn't one, go through normal effects if (!perSystemEffect(i,effect,effectVal)) switch (effect) { + /// PANNING case 0x08: // panning (split 4-bit) chan[i].panL=(effectVal>>4)|(effectVal&0xf0); chan[i].panR=(effectVal&15)|((effectVal&15)<<4); + // panning command isn't sent until later panChanged=true; break; case 0x80: { // panning (linear) + // convert to splir unsigned short pan=convertPanLinearToSplit(effectVal,8,255); chan[i].panL=pan>>8; chan[i].panR=pan&0xff; @@ -762,6 +936,7 @@ void DivEngine::processRow(int i, bool afterDelay) { break; case 0x83: // pan slide if (effectVal!=0) { + // set the pan slide speed if ((effectVal&15)!=0) { chan[i].panSpeed=(effectVal&15); } else { @@ -774,6 +949,7 @@ void DivEngine::processRow(int i, bool afterDelay) { } else { chan[i].panSpeed=0; } + // send hint (for command stream export) dispatchCmd(DivCommand(DIV_CMD_HINT_PAN_SLIDE,i,chan[i].panSpeed&0xff)); break; case 0x84: // panbrello @@ -783,8 +959,10 @@ void DivEngine::processRow(int i, bool afterDelay) { chan[i].panDepth=effectVal&15; chan[i].panRate=effectVal>>4; if (chan[i].panDepth!=0) { + // panbrello and slides are incompatible chan[i].panSpeed=0; } + // send hint (for command stream export) dispatchCmd(DivCommand(DIV_CMD_HINT_PANBRELLO,i,effectVal)); break; case 0x88: // panning rear (split 4-bit) @@ -800,7 +978,14 @@ void DivEngine::processRow(int i, bool afterDelay) { chan[i].panRR=effectVal; surroundPanChanged=true; break; - case 0x01: // ramp up + /// PITCH and more + // internally, slides and portamento share the same variables + case 0x01: // pitch slide up + // COMPAT FLAG: ignore duplicate slides + // - only the first 01xx effect is considered + // - 02xx still works + // - a previous portamento (03xx) will prevent this slide from occurring + // - E1xy/E2xy also will if *another* flag is set if (song.ignoreDuplicateSlides && (lastSlide==0x01 || lastSlide==0x1337)) break; lastSlide=0x01; if (effectVal==0) { @@ -808,21 +993,39 @@ void DivEngine::processRow(int i, bool afterDelay) { chan[i].portaSpeed=-1; dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,CLAMP(chan[i].portaNote,-128,127),MAX(chan[i].portaSpeed,0))); chan[i].inPorta=false; + // COMPAT FLAG: arpeggio inhibits non-porta slides + // - the PRE_PORTA command is used to let the dispatch know we're entering a pitch slide + // - this prompts dispatch to stop processing arp macros during a slide + // - this only happens if pitch linearity is set to None + // - if we don't let dispatch know, the slide will never occur as arp takes over if (!song.arpNonPorta) dispatchCmd(DivCommand(DIV_CMD_PRE_PORTA,i,false,0)); } else { + // COMPAT FLAG: limit slide range + // - this confines pitch slides from dispatch->getPortaFloor to C-8 (I think) + // - yep, the lowest portamento note depends on the system... chan[i].portaNote=song.limitSlides?0x60:255; chan[i].portaSpeed=effectVal; dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,CLAMP(chan[i].portaNote,-128,127),MAX(chan[i].portaSpeed,0))); + // most of these are used for compat flag handling chan[i].portaStop=true; - chan[i].nowYouCanStop=false; chan[i].stopOnOff=false; chan[i].scheduledSlideReset=false; chan[i].wasShorthandPorta=false; chan[i].inPorta=false; + // COMPAT FLAG: arpeggio inhibits non-porta slides + // - the PRE_PORTA command is used to let the dispatch know we're entering a pitch slide + // - this prompts dispatch to stop processing arp macros during a slide + // - this only happens if pitch linearity is set to None + // - if we don't let dispatch know, the slide will never occur as arp takes over if (!song.arpNonPorta) dispatchCmd(DivCommand(DIV_CMD_PRE_PORTA,i,true,0)); } break; - case 0x02: // ramp down + case 0x02: // pitch slide down + // COMPAT FLAG: ignore duplicate slides + // - only the first 02xx effect is considered + // - 01xx still works + // - a previous portamento (03xx) will prevent this slide from occurring + // - E1xy/E2xy also will if *another* flag is set if (song.ignoreDuplicateSlides && (lastSlide==0x02 || lastSlide==0x1337)) break; lastSlide=0x02; if (effectVal==0) { @@ -830,21 +1033,27 @@ void DivEngine::processRow(int i, bool afterDelay) { chan[i].portaSpeed=-1; dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,CLAMP(chan[i].portaNote,-128,127),MAX(chan[i].portaSpeed,0))); chan[i].inPorta=false; + // COMPAT FLAG: arpeggio inhibits non-porta slides if (!song.arpNonPorta) dispatchCmd(DivCommand(DIV_CMD_PRE_PORTA,i,false,0)); } else { + // COMPAT FLAG: limit slide range + // - this confines pitch slides from dispatch->getPortaFloor to C-8 (I think) + // - yep, the lowest portamento note depends on the system... chan[i].portaNote=song.limitSlides?disCont[dispatchOfChan[i]].dispatch->getPortaFloor(dispatchChanOfChan[i]):-60; chan[i].portaSpeed=effectVal; dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,CLAMP(chan[i].portaNote,-128,127),MAX(chan[i].portaSpeed,0))); chan[i].portaStop=true; - chan[i].nowYouCanStop=false; chan[i].stopOnOff=false; chan[i].scheduledSlideReset=false; chan[i].wasShorthandPorta=false; chan[i].inPorta=false; + // COMPAT FLAG: arpeggio inhibits non-porta slides if (!song.arpNonPorta) dispatchCmd(DivCommand(DIV_CMD_PRE_PORTA,i,true,0)); } break; case 0x03: // portamento + // exception: the arpNonPorta flag is not checked here. + // a portamento shall override arp macros on non-linear pitch. if (effectVal==0) { chan[i].portaNote=-1; chan[i].portaSpeed=-1; @@ -852,34 +1061,54 @@ void DivEngine::processRow(int i, bool afterDelay) { chan[i].inPorta=false; dispatchCmd(DivCommand(DIV_CMD_PRE_PORTA,i,false,0)); } else { + // lastPorta is used for the 06xy effect chan[i].lastPorta=effectVal; + // this here is for a compatibility flag... calledPorta=true; + // COMPAT FLAG: buggy portamento after sliding + // - you might want to slide up or down and then 03xx to return to the original note + // - if a porta to the same note is attempted after slide, for some reason it does not occur if (chan[i].note==chan[i].oldNote && !chan[i].inPorta && song.buggyPortaAfterSlide) { chan[i].portaNote=chan[i].note; chan[i].portaSpeed=-1; } else { + // compat flags get on my way chan[i].portaNote=chan[i].note; chan[i].portaSpeed=effectVal; chan[i].inPorta=true; + // ...but this one is for ANOTHER compat flag. yuck! chan[i].wasShorthandPorta=false; } dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,CLAMP(chan[i].portaNote,-128,127),MAX(chan[i].portaSpeed,0))); + // TODO; portaStop is guaranteed to be true anyway. what's the point of this? chan[i].portaStop=true; + // this is why we didn't send noye on before. + // there may be a portamento which of course prevents a note on if (chan[i].keyOn) chan[i].doNote=false; + // COMPAT FLAG: stop portamento on note off + // - if a portamento is called and then a note off occurs, stop portamento before the next note + // - ...unless noteOffResetsSlides is disabled chan[i].stopOnOff=song.stopPortaOnNoteOff; // what?! chan[i].scheduledSlideReset=false; dispatchCmd(DivCommand(DIV_CMD_PRE_PORTA,i,true,1)); + // this is used to inhibit any other slide commands if the respective compat flag is enabled lastSlide=0x1337; // i hate this so much } break; + // vibratos and pitch changes are mixed in. case 0x04: // vibrato + // remember the last vibrato for 05xy if (effectVal) chan[i].lastVibrato=effectVal; chan[i].vibratoDepth=effectVal&15; chan[i].vibratoRate=effectVal>>4; dispatchCmd(DivCommand(DIV_CMD_HINT_VIBRATO,i,(chan[i].vibratoDepth&15)|(chan[i].vibratoRate<<4))); + // update pitch now dispatchCmd(DivCommand(DIV_CMD_PITCH,i,chan[i].pitch+(((chan[i].vibratoDepth*vibTable[chan[i].vibratoPos]*chan[i].vibratoFine)>>4)/15))); break; + /// VOLUME-RELATED case 0x05: // vol slide + vibrato + // this effect is weird. it shouldn't be here considering we have more + // than one effect column, but I guess it had to be done if (effectVal==0) { chan[i].vibratoDepth=0; chan[i].vibratoRate=0; @@ -888,6 +1117,7 @@ void DivEngine::processRow(int i, bool afterDelay) { chan[i].vibratoRate=chan[i].lastVibrato>>4; } dispatchCmd(DivCommand(DIV_CMD_HINT_VIBRATO,i,(chan[i].vibratoDepth&15)|(chan[i].vibratoRate<<4))); + // update pitch now dispatchCmd(DivCommand(DIV_CMD_PITCH,i,chan[i].pitch+(((chan[i].vibratoDepth*vibTable[chan[i].vibratoPos]*chan[i].vibratoFine)>>4)/15))); // TODO: non-0x-or-x0 value should be treated as 00 if (effectVal!=0) { @@ -906,6 +1136,7 @@ void DivEngine::processRow(int i, bool afterDelay) { dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed)); break; case 0x06: // vol slide + porta + // same thing here. this is another effect that doesn't need to exist. if (effectVal==0 || chan[i].lastPorta==0) { chan[i].portaNote=-1; chan[i].portaSpeed=-1; @@ -913,7 +1144,10 @@ void DivEngine::processRow(int i, bool afterDelay) { chan[i].inPorta=false; dispatchCmd(DivCommand(DIV_CMD_PRE_PORTA,i,false,0)); } else { + // this here is for a compatibility flag... calledPorta=true; + // COMPAT FLAG: buggy portamento after sliding + // yes, this also affects 06xy. if (chan[i].note==chan[i].oldNote && !chan[i].inPorta && song.buggyPortaAfterSlide) { chan[i].portaNote=chan[i].note; chan[i].portaSpeed=-1; @@ -924,6 +1158,7 @@ void DivEngine::processRow(int i, bool afterDelay) { chan[i].wasShorthandPorta=false; } dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,CLAMP(chan[i].portaNote,-128,127),MAX(chan[i].portaSpeed,0))); + // this is the same as 03xx. chan[i].portaStop=true; if (chan[i].keyOn) chan[i].doNote=false; chan[i].stopOnOff=song.stopPortaOnNoteOff; // what?! @@ -931,6 +1166,7 @@ void DivEngine::processRow(int i, bool afterDelay) { dispatchCmd(DivCommand(DIV_CMD_PRE_PORTA,i,true,1)); lastSlide=0x1337; // i hate this so much } + // now handle volume slide // TODO: non-0x-or-x0 value should be treated as 00 if (effectVal!=0) { if ((effectVal&15)!=0) { @@ -948,21 +1184,28 @@ void DivEngine::processRow(int i, bool afterDelay) { dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed)); break; case 0x07: // tremolo + // the implementation of tremolo in Furnace is more like that of + // vibrato. it oscillates according to a waveform. + // this differs from Defle where it is a consecutive vol slide + // up/down and exhibits a numbee of bugs. if (chan[i].tremoloDepth==0) { chan[i].tremoloPos=0; } chan[i].tremoloDepth=effectVal&15; chan[i].tremoloRate=effectVal>>4; dispatchCmd(DivCommand(DIV_CMD_HINT_TREMOLO,i,effectVal)); + // unfortunately, we cannot run both tremolo and vol slide at once. if (chan[i].tremoloDepth!=0) { chan[i].volSpeed=0; chan[i].volSpeedTarget=-1; } else { + // restore the volume if tremolo is disabled dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8)); dispatchCmd(DivCommand(DIV_CMD_HINT_VOLUME,i,chan[i].volume>>8)); } break; - case 0x0a: // volume ramp + case 0x0a: // volume slide + // the speed multipler is 64, which means 4 ticks between volume changes with a value of 1 // TODO: non-0x-or-x0 value should be treated as 00 if (effectVal!=0) { if ((effectVal&15)!=0) { @@ -976,29 +1219,42 @@ void DivEngine::processRow(int i, bool afterDelay) { } else { chan[i].volSpeed=0; } + // reset the volume target chan[i].volSpeedTarget=-1; dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed)); break; + /// NOTE case 0x00: // arpeggio chan[i].arp=effectVal; + // COMPAT FLAG: reset note to base on arp stop (inverted in the GUI) + // - a 0000 effect resets arpeggio position if (chan[i].arp==0 && song.arp0Reset) { chan[i].resetArp=true; } dispatchCmd(DivCommand(DIV_CMD_HINT_ARPEGGIO,i,chan[i].arp)); break; case 0x0c: // retrigger + // this is the only non-continuous effect. it takes place exclusively + // within one row like most PC/Amiga trackers. + // consecutive 0Cxx effects will reset on each row... if (effectVal!=0) { chan[i].retrigSpeed=effectVal; chan[i].retrigTick=0; } break; + /// MISC case 0x90: case 0x91: case 0x92: case 0x93: case 0x94: case 0x95: case 0x96: case 0x97: case 0x98: case 0x99: case 0x9a: case 0x9b: case 0x9c: case 0x9d: case 0x9e: case 0x9f: // set samp. pos + // COMPAT FLAG: old sample offset effect + // - before 0.6.3 the sample offset effect was 9xxx, where `xxx` is multiplied by 256 + // - the effect was then changed to 90xx/91xx/92xx, allowing you to set the low, mid and high bytes of the offset respectively if (song.oldSampleOffset) { + // send sample position now dispatchCmd(DivCommand(DIV_CMD_SAMPLE_POS,i,(((effect&0x0f)<<8)|effectVal)*256)); } else { + // change one byte and schedule sample position if (effect<0x93) { chan[i].sampleOff&=~(0xff<<((effect-0x90)<<3)); chan[i].sampleOff|=effectVal<<((effect-0x90)<<3); @@ -1007,6 +1263,11 @@ void DivEngine::processRow(int i, bool afterDelay) { } break; case 0xc0: case 0xc1: case 0xc2: case 0xc3: // set Hz + // Cxxx, where `xxx` is between 1 and 1023 + // divider is the tick rate in Hz + // cycles is the number of samples between ticks + // clockDrift is used for accuracy and subticks for low-latency mode + // (where we run faster thsn the tick rate to allow sub-tick note events from live playback) divider=(double)(((effect&0x3)<<8)|effectVal); if (divider<1) divider=1; cycles=got.rate/divider; @@ -1014,20 +1275,32 @@ void DivEngine::processRow(int i, bool afterDelay) { subticks=0; break; case 0xdc: // delayed mute + // used on XM import, where ECx actually mutes the note + // COMPAT FLAG: cut/delay effect policy (delayBehavior) + // - 0: strict + // - ignore cut if equal or greater than speed + // - 1: strict old + // - ignore cut if equal or greater than speed + // - 2: lax (default) + // - no cut is ever ignored unless overridden by another if (effectVal>0 && (song.delayBehavior==2 || effectValchan[i].volume ? effectVal : -effectVal; chan[i].volSpeedTarget=chan[i].volSpeed==0 ? -1 : volPortaTarget; dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE_TARGET,i,chan[i].volSpeed,chan[i].volSpeedTarget)); break; case 0xd4: // volume portamento fast (vol porta fast) + // this is the same as D3xx, but 256 times faster. // tremolo and vol slides are incompatible chan[i].tremoloDepth=0; chan[i].tremoloRate=0; @@ -1036,46 +1309,63 @@ void DivEngine::processRow(int i, bool afterDelay) { dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE_TARGET,i,chan[i].volSpeed,chan[i].volSpeedTarget)); break; case 0xe0: // arp speed + // the arp speed is global. I have no idea why. if (effectVal>0) { curSubSong->arpLen=effectVal; dispatchCmd(DivCommand(DIV_CMD_HINT_ARP_TIME,i,curSubSong->arpLen)); } break; case 0xe1: // portamento up + // this is a shortcut for 03xx and a higher note. + // it has the benefit of being able to be used in conjunction with a note. chan[i].portaNote=chan[i].note+(effectVal&15); chan[i].portaSpeed=(effectVal>>4)*4; dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,CLAMP(chan[i].portaNote,-128,127),MAX(chan[i].portaSpeed,0))); + // these are for compatibility stuff chan[i].portaStop=true; - chan[i].nowYouCanStop=false; + // COMPAT FLAG: stop portamento on note off chan[i].stopOnOff=song.stopPortaOnNoteOff; // what?! chan[i].scheduledSlideReset=false; + // only enter portamento if the speed is set if ((effectVal&15)!=0) { chan[i].inPorta=true; + // these are for compatibility flaga. chan[i].shorthandPorta=true; chan[i].wasShorthandPorta=true; + // COMPAT FLAG: broken shortcut slides + // - oddly enough, shortcut slides are not communicated to the dispatch + // - this was fixed in 0.5.7 if (!song.brokenShortcutSlides) dispatchCmd(DivCommand(DIV_CMD_PRE_PORTA,i,true,0)); + // COMPAT FLAG: E1xy/E2xy also take priority over slides + // - another Defle hack. it places shortcut slides above pitch slides. if (song.e1e2AlsoTakePriority) lastSlide=0x1337; // ... } else { chan[i].inPorta=false; + // COMPAT FLAG: broken shortcut slides if (!song.brokenShortcutSlides) dispatchCmd(DivCommand(DIV_CMD_PRE_PORTA,i,false,0)); } break; case 0xe2: // portamento down + // this is the same as E1xy but in the opposite direction. chan[i].portaNote=chan[i].note-(effectVal&15); chan[i].portaSpeed=(effectVal>>4)*4; dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,CLAMP(chan[i].portaNote,-128,127),MAX(chan[i].portaSpeed,0))); chan[i].portaStop=true; - chan[i].nowYouCanStop=false; + // COMPAT FLAG: stop portamento on note off chan[i].stopOnOff=song.stopPortaOnNoteOff; // what?! chan[i].scheduledSlideReset=false; if ((effectVal&15)!=0) { chan[i].inPorta=true; + // these are for compatibility flaga. chan[i].shorthandPorta=true; chan[i].wasShorthandPorta=true; + // COMPAT FLAG: broken shortcut slides if (!song.brokenShortcutSlides) dispatchCmd(DivCommand(DIV_CMD_PRE_PORTA,i,true,0)); + // COMPAT FLAG: E1xy/E2xy also take priority over slides if (song.e1e2AlsoTakePriority) lastSlide=0x1337; // ... } else { chan[i].inPorta=false; + // COMPAT FLAG: broken shortcut slides if (!song.brokenShortcutSlides) dispatchCmd(DivCommand(DIV_CMD_PRE_PORTA,i,false,0)); } break; @@ -1084,21 +1374,23 @@ void DivEngine::processRow(int i, bool afterDelay) { dispatchCmd(DivCommand(DIV_CMD_HINT_VIBRATO_SHAPE,i,chan[i].vibratoShape)); break; case 0xe4: // vibrato fine + // this sets the multiplier for vibrato depth chan[i].vibratoFine=effectVal; dispatchCmd(DivCommand(DIV_CMD_HINT_VIBRATO_RANGE,i,chan[i].vibratoFine)); break; case 0xe5: // pitch chan[i].pitch=effectVal-0x80; + // why isn't this removed yet? we shpuldn't give this chip special treatment! if (sysOfChan[i]==DIV_SYSTEM_YM2151) { // YM2151 pitch oddity chan[i].pitch*=2; if (chan[i].pitch<-128) chan[i].pitch=-128; if (chan[i].pitch>127) chan[i].pitch=127; } - //chan[i].pitch+=globalPitch; + // send pitch now dispatchCmd(DivCommand(DIV_CMD_PITCH,i,chan[i].pitch+(((chan[i].vibratoDepth*vibTable[chan[i].vibratoPos]*chan[i].vibratoFine)>>4)/15))); dispatchCmd(DivCommand(DIV_CMD_HINT_PITCH,i,chan[i].pitch)); break; - case 0xe6: // Delayed legato + case 0xe6: // delayed legato // why does this have to follow FamiTracker verbatim // couldn't you do better? if ((effectVal&15)!=0) { @@ -1114,8 +1406,16 @@ void DivEngine::processRow(int i, bool afterDelay) { } break; case 0xe7: // delayed macro release + // COMPAT FLAG: cut/delay effect policy (delayBehavior) + // - 0: strict + // - ignore cut if equal or greater than speed + // - 1: strict old + // - ignore cut if equal or greater than speed + // - 2: lax (default) + // - no cut is ever ignored unless overridden by another // "Bruh" if (effectVal>0 && (song.delayBehavior==2 || effectVal0 && (song.delayBehavior==2 || effectVal