SIDin #7 — xxlarge: Ninja's 256-byte cover of 'X large' analysed (Stefano Tognon, 2005)

author Stefano Tognon (ice00), SIDin magazine; code by Ninja/theDream
date 2005-01-09
article SIDin issue 7 (version 1.00, 2005-01-09 — the PDF prints 2004, a typo), pp. 48–52: 'xxlarge' — sections Code (the released source, complete), Analysis, In depth, Conclusion (announcing the Tiny SID Competition 2005)
created 2026-08-30 · updated 2026-08-30

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/789 hold the hardware IRQ vector the KERNAL jumps through; the default $EA31 becomes $0831, so the player's mus_irq simply has to start at $0831 and "we don't need anymore BASIC instructions"; the bytes before $0831 hold 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 = $3b with voices 2 and 3 at +7 and +14, pattern_idx = $3d, mus_bufferv23 = $b3, and mus_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_list of 8 patterns (pattern3, pattern2, pattern3, pattern2, pattern1, pattern1, pattern5, pattern4); pulse_tab of 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) with filter_cut ($60,$78,$90,$a8,$c0,$DB) and filter_res ($01,$41,$71,$a1,$e1,$1f); 14-entry notes_lo/notes_hi tables; patterns of packed nibbles ending in a 0 byte; the string "NINJA" closes the file.
  • The voice loop (loop_base = $100 - 3*7): Y starts at $EB and advances by 7 per voice until the carry sets after voice 3; each pass writes the control register ($41 pulse for voice 1, $11 triangle for voices 2 and 3), the SR byte from the interleaved table and the note's frequency low/high from the note tables, all with sta $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, x is written to both $D402 and $D403 ("High and low value of wave are the same, but values are taken from a table, so they are not fixed"); after an asl its 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_pnt is incremented and a byte fetched; a 0 ends the pattern, pattern_idx is decremented and the next pattern taken from the list ("only the 3 lower bits are used"). Each byte holds two nibbles: $0 = change mus_filtercnt, $1 unused, $2–$F = note index; a nibble's note is written to mus_v1pos, doubled and added to filter_cut,y for $D416.
  • The filter goes up, then down: before mus_filterchange sits a BIT opcode ($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 between inc ($E6) and ror ($66) by lda #$66<<1 / ror / sta mus_filterchange — carry 1 gives inc, carry 0 ror ("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 from filter_res,y goes to $D417 (filter-programming).
  • A debug block under if 0 writes mus_filtercnt, mus_pnt and pattern_idx to 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 .prg files "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_res block 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 → v2pos on 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,y with y = mus_filtercnt lands on $D418 only 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

source file wiki/summaries/s-sidin07-xxlarge.md · 4 unverified · graph