From ff3cfe5377aa680ddf9b024a7f7a16a2dd24749c Mon Sep 17 00:00:00 2001 From: tildearrow Date: Mon, 20 Oct 2025 06:29:45 -0500 Subject: [PATCH] finish documenting playback code --- src/engine/playback.cpp | 99 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 92 insertions(+), 7 deletions(-) diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index 26c34b44d..d4f18322c 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -2585,6 +2585,7 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) { if (stepPlay!=1) { if (!noAccum) { double dt=divider*tickMult; + // TODO: is this responsible for timing differences when skipping? if (skipping) { dt*=(double)virtualTempoN/(double)MAX(1,virtualTempoD); } @@ -2617,19 +2618,25 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) { return ret; } +// returns the buffer position. used by audio export. int DivEngine::getBufferPos() { return bufferPos; } +// runs MIDI clock. void DivEngine::runMidiClock(int totalCycles) { + // not in freelance mode if (freelance) return; midiClockCycles-=totalCycles; + // run by the amount of cycles while (midiClockCycles<=0) { + // send MIDI clock event curMidiClock++; if (output) if (!skipping && output->midiOut!=NULL && midiOutClock) { output->midiOut->send(TAMidiMessage(TA_MIDI_CLOCK,0,0)); } + // calculate tempo using highlight, timeBase, tick rate, speeds and virtual tempo double hl=curSubSong->hilightA; if (hl<=0.0) hl=4.0; double timeBase=curSubSong->timeBase+1; @@ -2643,10 +2650,13 @@ void DivEngine::runMidiClock(int totalCycles) { if (speedSum<1.0) speedSum=1.0; if (vD<1) vD=1; double bpm=((24.0*divider)/(timeBase*hl*speedSum))*(double)virtualTempoN/vD; + // avoid a division by zer if (bpm<1.0) bpm=1.0; int increment=got.rate/(bpm); + // increment should be at least 1 if (increment<1) increment=1; + // drift is for precision midiClockCycles+=increment; midiClockDrift+=fmod(got.rate,(double)(bpm)); if (midiClockDrift>=(bpm)) { @@ -2656,9 +2666,13 @@ void DivEngine::runMidiClock(int totalCycles) { } } +// runs MIDI timecode. void DivEngine::runMidiTime(int totalCycles) { + // not in freelance mode if (freelance) return; + // not if the rate is too low if (got.rate<1) return; + // run by the amount of cycles midiTimeCycles-=totalCycles; while (midiTimeCycles<=0) { if (curMidiTimePiece==0) { @@ -2668,6 +2682,7 @@ void DivEngine::runMidiTime(int totalCycles) { double frameRate=96.0; int timeRate=midiOutTimeRate; + // determine the rate depending on tick rate if set to automatic if (timeRate<1 || timeRate>4) { if (curSubSong->hz>=47.98 && curSubSong->hz<=48.02) { timeRate=1; @@ -2680,6 +2695,7 @@ void DivEngine::runMidiTime(int totalCycles) { } } + // calculate the current time int hour=0; int minute=0; int second=0; @@ -2724,6 +2740,7 @@ void DivEngine::runMidiTime(int totalCycles) { break; } + // output timecode if (output) if (!skipping && output->midiOut!=NULL && midiOutTime) { unsigned char val=0; switch (curMidiTimePiece) { @@ -2766,6 +2783,8 @@ void DivEngine::runMidiTime(int totalCycles) { } } +// these two functions are either leftovers or something or they are there for test purposes. +// I don't remember very well. void _runDispatch1(void* d) { } @@ -2773,24 +2792,32 @@ void _runDispatch2(void* d) { } +// this fills the audio buffer and runs tbe engine. +// called by the audio backend and during audio export. void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsigned int size) { + // debug information lastNBIns=inChans; lastNBOuts=outChans; lastNBSize=size; + // don't fill a buffer if the size is 0 if (!size) { logW("nextBuf called with size 0!"); return; } lastLoopPos=-1; + // clear the output if (out!=NULL) { for (int i=0; imidiIn) while (!output->midiIn->queue.empty()) { TAMidiMessage& msg=output->midiIn->queue.front(); + // print MIDI events if MIDI debug is enabled if (midiDebug) { if (msg.type==TA_MIDI_SYSEX) { logD("MIDI debug: %.2X SysEx",msg.type); @@ -2819,17 +2849,28 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi logD("MIDI debug: %.2X %.2X %.2X",msg.type,msg.data[0],msg.data[1]); } } + // call the MIDI callback, which may process this event further. + // the function should return an instrument index, which will be used + // for all forthcoming notes. + // special values: + // - -1: don't change + // - -2: "preview" instrument + // - -3: cancel event (do not add to pending notes) int ins=-1; if ((ins=midiCallback(msg))!=-3) { + // process event if not canceled int chan=msg.type&15; switch (msg.type&0xf0) { case TA_MIDI_NOTE_OFF: { if (midiIsDirect) { + // in direct mode, map the event directly to the channel if (chan<0 || chan>=chans) break; pendingNotes.push_back(DivNoteEvent(chan,-1,-1,-1,false,false,true)); } else { + // find a suitable channel and add this event to the queue autoNoteOff(msg.type&15,msg.data[0]-12,msg.data[1]); } + // start the engine if necessary if (!playing) { reset(); freelance=true; @@ -2838,24 +2879,31 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi break; } case TA_MIDI_NOTE_ON: { + // trigger note off if the velocity is 0 if (msg.data[1]==0) { if (midiIsDirect) { + // in direct mode, map the event directly to the channel if (chan<0 || chan>=chans) break; pendingNotes.push_back(DivNoteEvent(chan,-1,-1,-1,false,false,true)); } else { + // find a suitable channel and add this event to the queue autoNoteOff(msg.type&15,msg.data[0]-12,msg.data[1]); } } else { if (midiIsDirect) { + // in direct mode, map the event directly to the channel if (chan<0 || chan>=chans) break; pendingNotes.push_back(DivNoteEvent(chan,ins,msg.data[0]-12,msg.data[1],true,false,true)); } else { + // find a suitable channel and add this event to the queue autoNoteOn(msg.type&15,ins,msg.data[0]-12,msg.data[1]); } } break; } case TA_MIDI_PROGRAM: { + // program changes in direct mode are handled here + // the GUI should cancel this event and change the current instrument if (midiIsDirect && midiIsDirectProgram) { pendingNotes.push_back(DivNoteEvent(chan,msg.data[0],0,0,false,true,true)); } @@ -2869,24 +2917,31 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi output->midiIn->queue.pop(); } - // process sample/wave preview + // process sample/wave preview (not during audio export) if (((sPreview.sample>=0 && sPreview.sample<(int)song.sample.size()) || (sPreview.wave>=0 && sPreview.wave<(int)song.wave.size())) && !exporting) { + // we use blip_buf to pitch the sample unsigned int samp_bbOff=0; + // if there are samples, flush them (this can happen when the playback + // rate is less than the output rate) unsigned int prevAvail=blip_samples_avail(samp_bb); if (prevAvail>size) prevAvail=size; if (prevAvail>0) { blip_read_samples(samp_bb,samp_bbOut,prevAvail,0); samp_bbOff=prevAvail; } + // prepare to fill the buffer size_t prevtotal=blip_clocks_needed(samp_bb,size-prevAvail); + // play the sample if (sPreview.sample>=0 && sPreview.sample<(int)song.sample.size()) { DivSample* s=song.sample[sPreview.sample]; for (size_t i=0; i=(int)s->samples || (sPreview.pEnd>=0 && sPreview.pos>=sPreview.pEnd)) { + // zero if out of bounds samp_temp=0; } else { + // fetch sample samp_temp=s->data16[sPreview.pos]; if (--sPreview.posSub<=0) { sPreview.posSub=sPreview.rateMul; @@ -2897,9 +2952,11 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi } } } + // insert sample blip_add_delta(samp_bb,i,samp_temp-samp_prevSample); samp_prevSample=samp_temp; + // check playback direction and move needle if (sPreview.dir) { // backward if (sPreview.posloopStart || (sPreview.pBegin>=0 && sPreview.posisLoopable() && sPreview.posloopEnd) { @@ -3016,24 +3073,30 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi memset(samp_bbOut,0,size*sizeof(short)); } - // process audio + // process audio (run the engine) bool mustPlay=playing && !halted; if (mustPlay) { // logic starts here + // first reset the run position of all dispatches for (int i=0; i0) { remainingLoops--; if (!remainingLoops) { @@ -3064,6 +3129,7 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi } } } + // check whether we gotta insert a metronome tick if (pendingMetroTick) { unsigned int realPos=size-runLeftG; if (realPos>=size) realPos=size-1; @@ -3071,6 +3137,7 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi pendingMetroTick=0; } } else { + // we don't have to tick yet. run chip dispatches. // 3. run MIDI clock int midiTotal=MIN(cycles,runLeftG); runMidiClock(midiTotal); @@ -3079,7 +3146,9 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi runMidiTime(midiTotal); // 5. tick the clock and fill buffers as needed + // check which is nearest: a tick or end of audio buffer if (cyclesbb[0],dc->cycles); if (total>(int)dc->bbInLen) { logD("growing dispatch %p bbIn to %d",(void*)dc,total+256); @@ -3107,6 +3177,7 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi } dc->acquire(total); dc->fillBuf(total,dc->runPos,dc->cycles); + // advance run position dc->runPos+=dc->cycles; },&disCont[i]); } @@ -3114,6 +3185,7 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi runLeftG-=cycles; cycles=0; } else { + // the buffer will end before a tick happens // run until the end of this audio buffer cycles-=runLeftG; for (int i=0; ifillBuf(total,dc->runPos,dc->cycles); },&disCont[i]); } + // at this point runLeftG will be zero and we can break out of the loop runLeftG=0; renderPool->wait(); } } } + // complain and stop playback if we believe the engine has stalled //logD("attempts: %d",attempts); if (attempts>=(int)(size+10)) { logE("hang detected! stopping! at %d seconds %d micro (%d>=%d)",totalSeconds,totalTicks,attempts,(int)size); @@ -3156,8 +3230,11 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi playing=false; extValuePresent=false; } + // this is also used by audio export to cut out unnecessary silence after a stop song effect (FFxx) totalProcessed=size-runLeftG; + // complain if a dispatch's audio buffer must be flushed and our audio buffer is too small for it + // this may happen when a chip's output rate is lower than the sample rate for (int i=0; i0.0f) { for (int j=0; j>16; const unsigned short destPort=i&0xffff; @@ -3215,10 +3298,10 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi const unsigned char srcSubPort=srcPort&15; const unsigned char destSubPort=destPort&15; - // null portset + // null portset (disconnected) if (destPortSet==0xfff) continue; - // system outputs + // system outputs (the audio buffer) if (destPortSet==0x000) { if (destSubPort>=outChans) continue; @@ -3227,6 +3310,7 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi if (srcSubPortgetOutputCount()) { float vol=song.systemVol[srcPortSet]*disCont[srcPortSet].dispatch->getPostAmp()*song.masterVol; + // apply volume and panning switch (destSubPort&3) { case 0: vol*=MIN(1.0f,1.0f-song.systemPan[srcPortSet])*MIN(1.0f,1.0f+song.systemPanFR[srcPortSet]); @@ -3264,7 +3348,7 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi // nothing/invalid } - // dump to oscillator buffer + // dump to oscillator buffer (a ring buffer) for (unsigned int i=0; i(ts_processEnd-ts_processBegin).count(); }