SIDin #13 — Inside JITT64: the player routine (Stefano Tognon, 2010)
The player behind jitt64, explained by its author with the source: how init and the IRQ routine are organised (and why the SID is written at the start of the interrupt), how a pattern row is decoded into command bits, how an instrument's sixteen table pointers are defined and stepped, how each pattern command is computed, and how the hard restart is timed. A rare case of a driver documented by the person who wrote it (sid-player-routine).
Key claims#
- Init clears every player variable, reads the first pattern of each track (so that the IRQ code is smaller) and sets the volume to maximum. Play loops over the voices from 3 to 1, computing values into
shadow_xxvariables (one per SID register:FL,FH,PL,PH,CTRL,AD,SR, and the globalsFCH,RES,TYPE,VOL); "only at the end of player calculation those values are putted into the sid chip … all in one passed, so there is not distortion in sound generation for that voice", then the four global registers (filter, volume). - Three blocks per voice: Play Pattern (only when the pattern's tempo has elapsed — "a tempo of 7 … the routine is executed only after 7 IRQ calling"), Play Instrument and Play Command (every IRQ). Play Pattern must run first, "otherwise there will be a delay of one frame when a new note/instrument/command is to be activated".
- Why the SID is written first: with the linear structure "the 3 voices are not outputted at the same time" because the blocks take variable time, "and this can cause some hard-restart of note problem too". The revised IRQ writes all shadow registers at its start — "at every frames the sid registers are updated all together" — then runs the blocks, and a fourth block, Play Hard-Restart, runs last so that an HR "had to take control on sid output over the others commands". Side effect: "sound output is always one frame later his calculation … JITT64 is born for write only music, so this is not a problem" (hard-restart).
- Play Pattern (
pl_play_pattern) decodes four bytes per row from four pointer tables (pattPoint1–4): note (PAT_END→ next pattern in the track viapl_read_track,PAT_NULL,PAT_KON,PAT_KOFF,PAT_REST, or a note: addtrackTransp, look upfrequencyLo/Hi, and clearactiveCommand— "stop all pattern command"), instrument (pl_new_instrif non-zero), command, parameter. Command state is bit fields:activeCommandbits 0–6 = arpeggio, portamento up, down, tone portamento, vibrato, pulse slide up, down (continuous);actCommand2bits 0–7 = AD, SR, key on, key off, rest, filter type, resonance, cut-off (one-shot);actCommand3bit 0 = gate SR. TheTBLRmacro reads(pointer table)_L/H,xintoADDR_LOW/HIGHand loads(ADDR_LOW),y. - Command decoding (from the listing): Tempo 0 reads the pattern's own tempo from row 0 of
pattPoint1; AD/SR/Gate-SR storecmdADSRand a flag; Volume writesshadow_VOLdirectly; Arpeggio splits the parameter into bits 7–6 = speed (0–3), bits 5–3 = first offset added to the note, bits 2–0 = second offset added to the first arpeggio note (note2 = note1 + offset2), cycling positions 0 (note), 1, 2 (chord-arpeggio); Portamento up/down store the amount; Tone portamento takes the new row's note frequency (already in the shadow) as the target, restores the previous frequency (copy_FL/FH) as the start and sets the direction from a 16-bit compare — so the command sits on the same row as the target note; Vibrato: speed =(param & $0F) + 1, depth =(param & $F0) + $10, withvibSpeed1 = 0andvibSpeed2 = speedso the first leg is a half period; Slide up/down set pulse amounts; Auto fade out stores a delay; Filter type/resonance/cut-off storecmdFilterand a one-shot flag (pitch-slide-and-portamento, vibrato). - Instrument definition (
ins_instrmacro, conditional onNUM_INSTRfrom the packer): 5 bytes —INSTR_HR,INSTR_AD,INSTR_SR,INSTR_CTRL1,INSTR_CTRL2(the hard-restart ticks/flag, HR ADSR and the two HR control bytes) — then 16 pointers:instrAD,instrSR,instrWave,instrFreq,instrPulse,instrFilter,instrRes,instrType,instrFixFreq,instrRelFreq,instrFixPulse,instrRelPulse,instrRelFilter,instrFixFilter,instrDelay,instrRepeat. On a new instrument they are copied toinstrPtr*_L/Hper voice (instrument-tables). - Play Instrument (
pl_intr_core) calls eight table cores — AD, SR, Wave, Freq, Pulse, Filter, Res, Type — generated by one macro (pl_instr_core_) with anOUTVmacro choosing the output: AD/SR/Type to the shadow, Wave toshadow_CTRLunless zero, Freq/Pulse/Filter/Res throughputFreq,putPulse,putFilterFreq,putFilterRes. A table starts with three bytes — dimension, allowed repeats (0 = infinite), step position — then pairs of (value, D/R index); the index selects a delay frominstrDelayand a repeat count frominstrRepeat. Each tick: count the delay down, else re-output while repeats remain, else advance two bytes; at the end jump to the step position while repeats remain (actNumRepeatvsallowRepeat). - Play Command (
pl_play_command), every tick: arpeggio (position 0/1/2 →actNote,note1Arpeggio,note2Arpeggio;speedArpeggiocounts down and reloads); portamento up/down add or subtract the amountPORT_MULTtimes to the 16-bit shadow frequency; tone portamento the same until the target is reached, then snaps to it and clears bit 3; vibrato adds or subtractsvibFreqwith two counters and a direction flag; pulse slides add/subtract toshadow_PL/PH; auto fade decrementsshadow_VOLeveryautoFadeOutticks until 0; then the one-shot commands: AD, SR, key on (ora #$01), key off (and #$FE), rest (frequency 0), filter type →shadow_TYPE, resonance →shadow_RES, cut-off →shadow_FCH, gate SR →shadow_SR. - Play Hard-Restart (
pl_play_hr): only ifhrActive,xis set for the row; ticks =instr_HR & $0F; withpattDelay= ticks remaining in the row: ifpattDelayis 0 (the last tick before the new note) andinstr_HRbit 7 is set, writeinstr_CTRL2to the control shadow; else ifpattDelay ≤ ticks: withhrActivebit 7 (full HR) writeinstr_AD,instr_SRandinstr_CTRL1to the shadows, otherwise only clear the gate bit (hard-restart). - Conclusion: the shipping code (JITT64 1.03) adds conditional compilation to drop unused features but keeps this structure; because everything is macros, a faster idiom ("maybe using undocumented instructions") can be dropped in once and applies everywhere. "The use of so freedom into instrument implementation have make the code to manage it very complex, and so raster time usage can be high."
Practical takeaways#
- Write the SID from shadow registers at the start of the interrupt when voices must change together and hard restarts must land on exact frames; accept one frame of latency, or keep the end-of-routine write if the music must sync with graphics.
- Decode the pattern row before running the per-tick engines, or new notes get a one-frame smear of old and new settings.
- A per-table (value, delay/repeat) pair with a header of length, repeat count and loop point is a complete "table engine" in about a screen of 6502 — the same idea as SID-Wizard's and GoatTracker's tables, but with the delay in its own field.
Notable quotes#
"It is very important that Play Pattern is executed before the others blocks, otherwise there will be a delay of one frame when a new note/instrument/command is to be activated."
Relevance to the wiki#
Primary source for jitt64's player and a row in the driver comparison on sid-player-routine; the timing rationale for buffered ("shadow") writes and the HR-last ordering on hard-restart; the arpeggio, vibrato, portamento and pulse-slide arithmetic on chord-arpeggio, vibrato, pitch-slide-and-portamento and pulse-width-modulation; a table format for instrument-tables.
To verify#
- (unverified)
PORT_MULT= 2, matching the user article's "amount (*2)"; the constant's definition is not in the printed excerpt. - The article gives no rastertime figure; "very rastertime consuming" is the only statement.
Pages touched#
jitt64 · stefano-tognon · sid-player-routine · hard-restart · instrument-tables · chord-arpeggio · vibrato · pitch-slide-and-portamento · pulse-width-modulation · filter-programming · adsr-envelope · sidin