SIDin #7 — xxlarge: Ninja's 256-byte cover of 'X large' analysed (Stefano Tognon, 2005)
xxlarge is a C64 tune by Ninja/theDream that fits in less than 256 bytes — player, tables and music — and is a cover of Jeff's "X large", "played a little slower that the original", whose own data is "large about 800 bytes". Tognon prints the released source (CSDb release 14337) and explains its size tricks, then announces the tiny-sid-compo for which the article is the model. The article turns out to be a size-coding lesson: how a sid-player-routine shrinks when init, IRQ setup, attack/decay and even the pattern format are sacrificed.
Key claims#
- No init, and the IRQ set up by BASIC: the program is a BASIC line
POKE 789,8.788/789hold the hardware IRQ vector the KERNAL jumps through; the default$EA31becomes$0831, so the player'smus_irqsimply has to start at$0831and "we don't need anymore BASIC instructions"; the bytes before$0831hold the tables. "The player did not initialize itself … as all that is needed is done inside the play routine (and with variables already initialized)." - Zero-page variables borrowed from the system:
mus_pnt = $17,mus_filtercnt = $39(the BASIC line number low byte),mus_v1pos = $3bwith voices 2 and 3 at+7and+14,pattern_idx = $3d,mus_bufferv23 = $b3, andmus_pulsecnt = $cd— "Cursor Blink Cnt :)", the KERNAL's cursor-blink counter, "goes from $14 to $1", used as the index into the pulse table. - Data:
pattern_listof 8 patterns (pattern3, pattern2, pattern3, pattern2, pattern1, pattern1, pattern5, pattern4);pulse_tabof 20 bytes ($07,$47,$87,$B7,$08,$38,$88,$B8, $09,$39,$89,$49,$09,$B8,$88,$38, $08,$B7,$87,$37); a 15-byte block "3 + 2*6" interleaving the three voices' sustain/release bytes ($d7,$77,$38, at offsets 0, 7, 14) withfilter_cut($60,$78,$90,$a8,$c0,$DB) andfilter_res($01,$41,$71,$a1,$e1,$1f); 14-entrynotes_lo/notes_hitables; patterns of packed nibbles ending in a0byte; the string "NINJA" closes the file. - The voice loop (
loop_base = $100 - 3*7): Y starts at$EBand advances by 7 per voice until the carry sets after voice 3; each pass writes the control register ($41pulse for voice 1,$11triangle for voices 2 and 3), the SR byte from the interleaved table and the note's frequency low/high from the note tables, all withsta $d404 - loop_base, y-style addressing. What is missing: "it not set the Attack/Decay value of each voices: according to Ninja there where no more space to add this features" (adsr-envelope). - Pulse and tempo: the pulse value from
pulse_tab - 1, xis written to both$D402and$D403("High and low value of wave are the same, but values are taken from a table, so they are not fixed"); after anaslits top bit decides "skipping or not the rest of the player: this give some delay in calling the player" — the tune's tempo rides on the cursor-blink counter (pulse-width-modulation). - Pattern format:
mus_pntis incremented and a byte fetched; a0ends the pattern,pattern_idxis decremented and the next pattern taken from the list ("only the 3 lower bits are used"). Each byte holds two nibbles:$0= changemus_filtercnt,$1unused,$2–$F= note index; a nibble's note is written tomus_v1pos, doubled and added tofilter_cut,yfor$D416. - The filter goes up, then down: before
mus_filterchangesits aBITopcode ($2C) so that normally "no action is taken to the current mus_filtercnt"; a zero nibble branches into the instruction itself, which is self-modified betweeninc($E6) andror($66) bylda #$66<<1 / ror / sta mus_filterchange— carry 1 givesinc, carry 0ror("ror is shorted than dec, but according to Ninja it is stil good enough"). "Each patterns are played twice, so during the first time the filter table is incremented, in the second it is decremented." The resonance/routing byte fromfilter_res,ygoes to$D417(filter-programming). - A debug block under
if 0writesmus_filtercnt,mus_pntandpattern_idxto the screen at$0400. - Tiny SID Competition: "from 15 January 2005 to 21 April 2005 instead of the 'SidWine compo'" ("SidWine compo take me too many months to organize"); categories 256 bytes and 512 bytes ("Maybe a size of 1K could be another chance if required"); entries must be
.prgfiles "runnable with run after loading"; covers and remixes allowed ("cover with less byte is hard in every case"); authors may allow their (reverse-engineered or original) source to be published in SIDin; site digilander.iol.it/ice00/tsid/tinysid. Thanks to Ninja "for allowing me to write this article about his work and to have give some in-depth description of the engine".
Practical takeaways#
- Size-coding checklist from a working 256-byte tune: no init routine; an IRQ installed by one BASIC
POKE; variables in zero page the KERNAL already keeps; a system counter as a free LFO index; one nibble per note; tables overlapped by stride; self-modifying opcodes instead of flags; features dropped in order of audibility (attack/decay first). - The interleaved
SR_tab/filter_cut/filter_resblock is a general trick: lay out per-voice bytes at the stride the loop already steps by, and fill the gaps with other tables.
Notable quotes#
"Now I think you are wondering how music and code can be fitted in so little space, as Jeff original is large about 800 bytes!"
Relevance to the wiki#
The first size-coded player in the wiki, for sid-player-routine (a player with no init and no ADSR) and for tiny-sid-compo (the announcement, dates and rules); minor examples for pulse-width-modulation and filter-programming.
To verify#
Readings of the listing that the article's prose does not spell out:
- (unverified) Voices 2 and 3 replay what voice 1 played one and two player-steps earlier (
mus_bufferv23 → v3pos,v2pos → bufferv23,v1pos → v2poson every advancing call) — a three-voice canon/echo from a single nibble stream, which is how one pattern set feeds three voices (fake-echo). - (unverified) The note nibble doubled and added to the cutoff makes the filter track the note, a crude keyboard tracking.
- (unverified)
STA $D418 - 5,ywithy = mus_filtercntlands on$D418only for the sixth table entry ($1f= volume 15 + low-pass); for smaller indices the same value hits$D413–$D417. - (unverified) "Jeff" is not identified further in the article; the original "X large" is only sized ("about 800 bytes").
Pages touched#
sid-player-routine · tiny-sid-compo · pulse-width-modulation · filter-programming · adsr-envelope · fake-echo · stefano-tognon · sidin