This is a learn-by-building guide for the Crusty Studio SID Music Workshop. We will make an original, roughly 30-second cue, Neon Patrol, from an empty tune to a working crustyBASIC player include. The Workshop also includes a separate, roughly three-minute Neon Patrol — Warehouse Mix that turns the same ideas into a complete rave cue.
The Workshop is inspired by the practical tradition of C64 music programming: a patch is a small program, a player updates the SID every refresh, and three voices can imply a much larger arrangement through timing, motion, and careful rests. The goal is to understand those decisions and apply them to your own music.
Compose three reusable note streams, assign programmable instruments, add per-row effects and shared filter movement, audition both SID models, then export a generated .cbi player include.
.c64music.json asset in the Explorer.music/ folder; in browser preview, Save downloads the JSON file.The Workshop preview is a musical workbench, not a cycle-exact electrical simulation. Use it to compose and compare ideas, then test the generated player in your target emulator or hardware.
The Craft tab includes Load 3-minute demo. Use it when you want to hear a finished reference before composing, or when you want a rich project to take apart and study.
Neon Patrol — Warehouse Mix is about 3:04 at 120 BPM and contains 20 named patterns across 46 song-order entries. It hits immediately with a four-on-the-floor groove, jumping bass runs, a nonstop acid lead, and chord/percussion stabs layered across all three voices. The groove drops away twice for short, high-register lead breaks before the final overdrive. Its late-80s/early-90s arcade-rave character is an original nod to the energy of acts such as 808 State and Monty on the Run, not a quotation of any one track. The arrangement demonstrates every Workshop technique: fast arpeggios, delayed vibrato, pitch slides, filter events, pulse-width motion, programmed waveforms, voice time-sharing, hard sync, ring modulation, and chip-model colour.
The full demo is intentionally a listening reference rather than the first exercise in this guide. Choose New tune for the plain baseline, then come back to the demo and compare how each technique changes the same kind of three-voice material. Loading it does not overwrite a file until you choose Save tune.
The SID has three independent voices, each with an oscillator, pulse width, waveform, control register, and ADSR envelope. One shared filter can colour selected voices. A classic player writes those values repeatedly, so a note can evolve after it starts.
Think in four layers:
The Workshop keeps these layers separate so you can change an instrument once and hear that change everywhere it is used.
The Oscillator waveform grid picks a patch's base waveform, and it is the chip's waveform selector rather than a menu: four independent bits, each switching one generator on. Its symbols are:
| Button | Register | Character |
|---|---|---|
△ |
$10 |
Triangle: soft and hollow. The only waveform ring modulation reaches. |
╱ |
$20 |
Sawtooth: bright and buzzy, every harmonic at once. |
▯ |
$40 |
Pulse: hollow, and Pulse width decides which harmonics are missing. $800 is a square. |
▦ |
$80 |
Noise: an unpitched hiss, which is the chip's percussion. |
∅ |
$00 |
Silent: no generator at all. The gate and envelope still run — that is how a voice is taken away for a frame. |
A + between glyphs means those generators run together and the chip combines their outputs, so △ + ╱ is a shape neither makes alone. The grid offers all sixteen states: the four plain waveforms, the eleven combinations, and ∅ Silent. Pulse width is only audible with ▯, and ring modulation only reaches △. Hovering a button names the state, and the line under the grid describes the current selection in words.
Use Import → MIDI in the Workshop header to convert a Standard MIDI file into an editable SID project. The converter quantises PPQN timing to sixteenth-note tracker rows, maps low notes to the bass voice, the upper line to the lead voice, and a second melodic track to the counter voice. MIDI drum hits are time-shared onto that third voice. Up to 64 patterns (2,048 rows) are preserved; longer files are clearly marked as clipped. The result is an intentionally editable sketch: use the tracker to clean up overlaps, choose SID patches, and add arpeggios, filter movement, or voice-time-sharing after import.
Use Import → GoatTracker SNG to convert a GoatTracker v2 .sng file. The importer reads the first subtune's three channel order lists, patterns, instruments, and modulation tables, then creates an editable .c64music.json-style project. Notes, ADSR, core waveform controls, pulse width, basic vibrato, and simple modulation are carried across; advanced per-row/table commands that do not have a direct Workshop equivalent are listed in the import status. Multiple subtunes are currently limited to the first subtune. Save the converted tune under a new name, then export its .cbi player include for the game.
Dropping a file on the Workshop does the same thing. With the SID Music Workshop on screen, drag a .mid, .midi, .sng or .sid file anywhere onto it: a dashed cue appears while the file is over the Workshop naming the formats it takes, and the drop runs the matching converter and opens the result as a new tune. The drop is the Workshop's own — a .sng dropped on the code editor still means "copy this into music/ and open it as an asset" — so nothing changes about importing files into the project itself.
Use Import → SID file to convert a PSID tune. A .sid file is not a song: it is a small C64 program - a player routine plus its data - wrapped in a header that says where to load it and which addresses start it and advance it a frame. Crusty Studio therefore loads the tune's own player, runs it, watches every write it makes to the SID, and transcribes that into an editable project. Nothing of the original program is copied into your tune; what you get out is notes, patterns and patches, exactly as with the other importers.
A patch only moves its pulse width when the width really moves. The rate comes from counting how often the width crosses its own middle - a sweep that resets is not a faster sweep - and a width that drifts once, or changes between sections and holds, leaves the patch still at the median of what its notes used. The player can only wobble a patch at one rate, and an invented wobble is worse than a still patch on almost every frame of a fifty-eight-tune sample; Stairway to Heaven, a tune of slow drifts, came out wobbling at two hertz until this was measured.
A gliding melody is not transcribed as a slide. Plenty of players hold the gate and glide the frequency register - Stairway to Heaven rewrites it on 99% of its first voice's gated frames - and the transcription writes held notes for it. The tracker's U/D effect is a linear ramp across one row, which fits a glide that is spread evenly over the row and not a scoop that reaches its new pitch in the first few frames and holds; writing slides for the scoops was measured four points worse on the harness's pitch agreement, because the ramp moves the pitch on every frame the tune sits still. So the import leaves the effect empty, and a U/D you write yourself is played correctly by the preview, the model and the exported player.
The volume and the filter come from what the tune plays, not from its first frame. Both used to be read off the state the player's init left, and on about half the collection that state is not the one the tune plays with: 41 tunes in a 195-tune sample hold volume 0 in their first frame while they play at 15, so the export wrote SID.MASTER_VOLUME 0 - every note right and no sound at all - and a player that only writes its filter while it plays has no mode selected yet, which is silence of a different kind: on the chip, a voice routed into the filter by $D417 with no mode selected at $D418 goes nowhere, because the filter outputs nothing and a routed voice is not mixed around it. The import therefore takes the volume the tune holds while a voice is gated and the filter mode it holds while a voice is routed into the filter, and the cutoff and resonance from the frames that carry that mode. All eight of the chip's filters are modelled, not five: the three mode bits are independent, so low-pass + high-pass (the notch), low-pass + band-pass, band-pass + high-pass and all three at once are each their own mode. The preview plays a routed voice in a mode-less filter as silence rather than as a bypass, and the instrument panel says so when a patch is set up that way.
The pulse width is measured from every note a patch plays. It is twelve bits - the top nibble of its high register is not part of it - and a patch's width is the median of what its notes actually used, not the value of the one note that happened to create it. Its motion is what happens inside those notes, or the tune's own shared sweep when all the voices are writing one width that moves: Gauntlet II sweeps 1297..2048 on all three voices for the whole tune, and its patches take that sweep's middle, depth and measured rate so the voices sweep together the way the tune's do. What the preview plays is a sine of that motion, and the exported player swings the width as a square at its own tick rate, so a tune whose sweep is a ramp or a square is approximated - the notes, the waveform and the timing are not.
Where the loop begins is read from the tune, not guessed from its first repeat. The capture runs the tune's own player to the end - or to sixty seconds of silence, whichever comes first - and the arrangement it makes is the introduction plus one loop. A repeat counts as the loop only if it keeps repeating: the state has to agree for two full periods, verified from the end of the capture backwards, so a tune that plays its opening phrase twice and then moves on is not mistaken for a six-second tune. It is judged on the note, the gate, the envelope and the volume rather than on every register, because pulse width and filter sweeps run continuously in most players; and a repeat that only begins near the end of the run is an ending rather than a loop, so it has to account for most of what was captured. 2 Hours = NOT Enough - fifty-six seconds of tune, imported as five patterns - found the first of those rules the hard way.
The row grid is the coarsest one that puts 95% of a tune's notes on it, and one row per player call when nothing does. It is chosen from that whole play-through, so a tune whose loop turns out to be a minute long is transcribed at a coarser grid than a four-second one, because the Workshop holds 2,048 rows and the music has to fit inside them. The margin matters: a note half a row from where the tune plays it leaves the voice silent just before it and on the previous note's pitch just after, and over the collection that single-frame misplacement is the largest remaining class of wrong pitch. Requiring every note to land exactly is worse rather than better - it pushes tunes onto one row per call, which reaches the Workshop's 2,048-row limit sooner and merges more of the tune.
The player is run at the rate the tune was written for and one captured frame is one call to the player. The row grid is chosen so that every voice's notes land on it - spacing alone is not enough, because three voices can sit at different phases of the same grid - and the grid starts where the tune's notes do rather than at frame zero. That matters more than it sounds: a subtune driven by its own CIA timer may be called 25 times a second or 104, and sampling it at the video rate aliases everything it does - an arpeggio moving every call appears to move every other frame, and the repeated register file that marks a loop can never match, so the capture runs to its limit without ever finding the four-second tune inside it. The tempo follows the player's own rate, and the transcription lands on the tune's own tick - the coarsest grid that explains the gaps between notes - so a player that moves a voice every six frames becomes six frames per row. Notes are found at the frame they happen rather than by sampling each row: a player that gates a voice for two frames in the middle of a row is otherwise invisible, and that is most of what a busy tune does.
A SID chord is one voice cycling a chord's notes, faster than the ear separates them, and that is what the import writes down: one note whose patch arpeggiates. Where the player holds one note of the chord longer than another the cycle is written out a step at a time, so the chord keeps its timing rather than being evened out and drifting. A cycle is told from a melody by its speed - a step is never longer than four frames - and from a slide by repeating, because only a run that comes back to where it started is a chord. Where three or four fast steps sit inside one gate with no second cycle to prove them by, they are still taken as one chord: the gate never rose between them, and writing them as separate notes would retrigger an envelope the tune never retriggered. A pitch that is still moving - a glide writes a new pitch every frame - starts no note at all; a pitch has to hold to be a note.
Notes are held for as long as the tune holds its gate. A note shorter than its own row uses the patch's tick count, which reproduces it exactly; anything longer is held across its rows and let go by an OFF cell on the row the tune's gate actually closed. Patches are then reduced to the thirty-two the Workshop holds, by merging the two most alike over and over until thirty-two are left, so near-duplicates collapse into one and the patches that are genuinely different keep their own slot; anything still over the limit plays as the nearest survivor, and the status line says how many notes moved, which is typically a twentieth of a tune. A busy tune really does use them: on a 600-tune sample forty per cent carried more patches than there were slots when the limit was sixteen, and those tunes lost up to three points of waveform agreement to the merging.
A tune that repeats is imported as one play-through, not one loop. Everything before the point the tune comes round to is the introduction the loop never returns to, and it is kept: a fifteen-second tune whose last three seconds repeat imports as fifteen seconds of patterns, with the repeat collapsed into the song order.
Patches are read from what each note actually selects: waveform, ADSR, pulse width, filter routing, the arpeggio it cycles and whether its gate holds the voice or lets go within the row. The waveform is one of five: as well as triangle, saw, pulse and noise there is Silent, which is the SID's own state with no oscillator selected - a gate with nothing to shape it, which is how a hard-restarted hit begins and how a player takes a voice away for a frame. An imported note that starts that way keeps the silence as part of its patch and brings the sound in on the tick the tune does, and a player that blanks the control in the middle of a note gets a wave-program step back to silence - which is what the machine does with that byte. Two notes that differ in any of those are two patches, because the patch is what carries them - which is also why the patch limit is worth watching on a busy tune. A waveform the player switches to after gate-on becomes a Wave program step, timed in frames since the row started - which is what the player counts, and what the preview's tick means for an imported tune.
Some things a SID player does have no tracker equivalent, and the import lists them instead of hiding them: pitch movement and pulse-width modulation inside a held note, a tune that uses a second SID chip, and notes that could belong to either of two semitones. Three behaviours are worth knowing about:
A tune's own tuning is measured, not assumed. Plenty of players carry a frequency table a fixed distance from equal temperament - one built for a clock the header does not name, or a tune detuned on purpose. When every note agrees on that distance, it is taken out before the notes are rounded, so a tune more than a quarter-tone out still imports the right notes; when the notes disagree - a detuned drone held against the lead, which is a real technique - nothing is taken out, because subtracting one shared number would push the other group a semitone wrong. Either way the import says what it found.
The header's PAL/NTSC flag is checked against the tune's own frequency table, for the tempo as well as the pitch. If every note lands the same distance from a semitone under the stated clock and exactly on pitch under the other one, the table is the better witness: the import uses it for the notes, and says so. It uses it for time too - a tune built for a PAL machine is called fifty times a second there whatever its header claims - so an import plays at the tempo the tune was written at rather than a fifth away from it. A subtune driven by a CIA timer keeps its own timer's rate instead, which a video-rate player cannot follow; the status line says when that is the case. This is not hypothetical - the tune this importer was written against is flagged NTSC and is a PAL tune.
RSID files are refused. An RSID tune calls the real C64's KERNAL and BASIC routines rather than carrying its own player, and Crusty Studio has no ROMs to run it with. PSID files are self-contained and are what this importer handles. A refused file - RSID or anything else the Workshop cannot read - is reported in a banner across the top of the Workshop, with the file's name and the reason, rather than failing quietly: dropping Arkanoid.sid used to leave the tracker looking as though nothing had happened.
A tune can also be exported as its own register writes, and that export is exact. A .sid import carries as much of the tune's per-frame register file as is worth carrying - the tune's loop when it has one, or the whole of a tune short enough to have none - and Export → Export the exact player writes an include in which a small player replays those writes verbatim: no notes, no patches, no interpretation, so nothing is lost. The Workshop's header shows what the song will take in a C64's memory, under the tune's own details, and the export dialog repeats both figures where the file is written: the tracker player (editable) and, when the import carried one, the exact player (the tune itself), each as bytes of table plus the player's own size. The exact export is an alternative to the transcription rather than the same thing said twice, so it is only carried while it is small: a loop of about half a minute, or a tune of that length. A four-minute capture would be some hundred and forty kilobytes of table, more than the machine it is exported to has, and past that the import keeps the transcription alone - which is what the dialog says, and what the readout then shows. Working out how to make the transcription smaller is the user's business - a shorter order is the lever that matters, because the per-row tables are four arrays of one entry a row for each of the three voices, while the per-patch tables are a fixed thirty-three entries whatever the patch count - and the readout is there to show what each choice costs.
The two halves of that number are checked separately, because they are different kinds of claim. The table is arithmetic: the generated include writes every array's length down, so counting it is exact. The player is code, so scripts/sid-export-size.analysis.ts compiles both players with the project's own toolchain and asserts the figures the editor reports against the assembled .prg. The exact player is one template, 566 bytes. The tracker player is not one number: the script measures each optional block by compiling a project with the feature and one without, and the editor prices a tune as a plain player of 2,120 bytes plus 200 for an arpeggio program, 187 for a wave program, 783 for vibrato, 638 for a shared-voice guard, and 130 more when a table passes 256 entries. And exactness itself is checked twice: scripts/sid-stream-player.analysis.ts runs the compiled program on the app's own 6502 and compares the SID with the tune on every frame, and scripts/c64-stream-vice.py asks VICE the same question through its monitor - the artefact carries a frame marker in the accumulator at $D419, one of the SID's unused addresses, so the monitor can stop once a frame and read $D400-$D418. VICE held the tune's own registers on every frame of a sixty-frame span of two different tunes. That script never touches a VICE you are running: it refuses to start if its monitor port is busy, uses its own port and its own launcher bundle, finds its own emulator by the path of the program it just built, and kills only that pid.
The export is checked on a machine too: scripts/sid-export-check.analysis.ts writes the include for a tune along with the notes it should play, scripts/c64-sid-compare.py runs that include on VICE and reads the SID back, and the two have to agree note for note. The player itself is checked against the rules the fidelity harness measures with - scripts/sid-player-machine.ts compiles the include with the project's own toolchain and runs the program on the app's own 6502, scripts/sid-player-run.analysis.ts pins one rule per synthetic fixture, and scripts/sid-player-batch.analysis.ts runs it over a corpus sample; over a 200-tune sample the 193 tunes that pitched anything played every frame exactly as the model says. The importer is measured against the whole High Voltage SID Collection rather than against a handful of tunes: scripts/sid-corpus.analysis.ts imports a sample exactly as the app does and reports what came out, scripts/sid-capture-scan.analysis.ts reports only whether the capture holds the tune at all, and why not when it does not, scripts/sid-inspect.analysis.ts prints one tune's register traffic frame by frame, scripts/sid-runs.analysis.ts prints the note runs the transcriber found beside the registers the player wrote, scripts/sid-rows.analysis.ts prints what the exported include carries for a tune beside the capture, scripts/sid-trace.analysis.ts prints a player's instructions, and scripts/sid-fidelity.analysis.ts replays each imported project with the exported player's own rules and scores it against the original. Run any of them with npx vitest run --config scripts/vitest.analysis.config.ts <script>; over a 564-tune sample the fidelity report reads 98.1% of the original's sounding frames reproduced, 98.9% of its note starts, 84.7% of its pitched frames at the right pitch and 98.3% of them with the right oscillator. Of those pitched frames, 92.5% of the ones where the tune holds a pitch are in tune against 75.5% of the ones where it moves the pitch under an open gate - which is a third of the collection, and the part a tracker row cannot say on its own. Every non-RSID tune in that sample captured: none crashed, none was truncated, none came back empty.
A tune longer than 64 patterns (2,048 rows) is fitted by lengthening its rows rather than by cutting it short, and the longer rows are chosen so the tune's notes still land on them. The status line says when a tune has been fed through that. Import always starts a new asset, so Save asks for a name rather than replacing the file you imported.
The browser preview is a lightweight musical model, not a replacement for the silicon. It renders the song sample at a time with one oscillator, one pulse-width LFO and one ADSR envelope per voice, so a note held across rows is one note and not one a row, and so the two effects that are relationships between voices work at all: ring modulation inverts a voice's triangle while the previous voice's oscillator is in the top half of its cycle, and hard sync resets a voice when the previous voice's oscillator wraps. Both read the previous voice's live oscillator - voice 1's modulator is voice 3, voice 2's is voice 1, voice 3's is voice 2 - and a voice whose gate is closed keeps running, because on the chip it does.
That has one consequence worth knowing when a patch does not sound the way you expect: ring modulation only reaches the triangle waveform, so a ring bit set on a pulse, sawtooth or noise voice changes nothing at all. Black Thunder writes gate, ring and pulse to all three voices, which on a real 6581 is a plain pulse; the instrument panel says so when a patch has the bit set where nothing can hear it. The preview applies the instrument's pulse-width motion, ADSR envelope, arpeggio, vibrato, pitch slide and programmed waveform changes as before, and the waveform selector is the chip's: four bits whose outputs are ANDed, so a patch that selects a pulse and a sawtooth at once plays the combined shape rather than one of them. The instrument picker and the wave program offer all sixteen states the selector can be in.
The filter is modelled too: the voices routed through it (each patch's own "route through filter" flag, which is the mask the exported player writes every row), one shared filter whose cutoff and resonance come from the project, the F effect moving the cutoff on the rows that carry it, and the 6581/8580 colour as a waveshaper after the mix. A third of the collection imports with the filter on, so this is most tunes' colour rather than a detail.
If the tune's own frequency table is not on equal temperament, the import keeps the offset as the project's Detune (the song strip shows it, in cents) and both the preview and the exported table apply it. Black Thunder's table is 26 cents flat; the notes are still semitones, but the tune plays where the original did rather than a quarter-tone sharp of it.
The patch play button in the instrument list auditions at the note in the Audition note selector beside it, held for a second and a half before its release, so a decay, a pulse motion, a delayed vibrato or a wave program can be heard rather than guessed at.
Long songs are rendered into complete preview buffers before playback begins, three of them, one a voice, and the render is sample-at-a-time oscillator arithmetic on the same thread that draws the editor. The Workshop shows rendering progress and a memory estimate, and past about two minutes of music - around sixty megabytes of buffers - it asks instead of rendering: the dialog says how long the song is and what it would cost, and offers to play the first two minutes, which it renders within the same budget and plays once through rather than looping. A song under two minutes, and any single pattern, plays in full. The limit is about the editor staying responsive, not about the tune, and it affects only the browser preview, never the exported game player.
Why a small file is a big preview: a .sid is the tune's code - a player routine and its data - while the preview is audio. Playing four minutes of music means 44,100 numbers a second for each of three voices, so 241 seconds is 122 MB of sample buffers however little the file weighs. The same arithmetic applies to the tracker export: rows are cheap, and the export's tables grow with rows, not with the file.
The real SID is register-driven: three voices have independent oscillator and envelope controls, while the filter is shared. The player include therefore writes SID.FREQ, SID.ADSR, SID.CTRL, SID.PULSE_WIDTH, and filter registers as a crustyBASIC routine. The browser model helps you compose and audition; an emulator or physical SID is the authority for cycle timing, analogue filter behaviour, oscillator leakage, and chip-to-chip variation.
The song preview renders one bounded mixed buffer and plays it through a single loopable source. That keeps the audio graph small and the loop boundary sample-stable; the UI follows the same Web Audio clock for row highlighting. Patch audition remains a short per-note render, so it can be used safely while experimenting with an instrument.
Make the musical roles explicit before entering notes:
| Voice | Role | Suggested range | Why it works |
|---|---|---|---|
| 1 | Bass and low percussion | C-2–A-2 | Anchors the harmony and leaves space above. |
| 2 | Lead and answer phrase | C-4–A-4 | Carries the tune where the ear can follow it. |
| 3 | Chords, echo, and drums | C-3–G-4 | Changes jobs over time instead of playing continuously. |
Use 125 BPM, Speed 6, PAL, and MOS 6581 as the starting point. Each 32-row pattern is one phrase. The compact starter arrangement uses eight song-order entries across four related patterns, giving the learning cue room to develop; the full demo expands that idea to 46 entries. Rests are intentional: they create contrast and leave room for sound effects.
neon-patrol.c64music.json.Neon Patrol and fill in Author.125 and Speed to 6.The default starter arrangement is safe to replace. Its purpose is to show the shape of a three-voice pattern while you learn the controls.
Before using SID tricks, make a version that would still work as a conventional three-part song. From a blank tune, press + Instrument three times so you have four patches. Turn every pulse motion, arpeggio, vibrato, wave-program step, sync, ring modulation, and row effect off for now.
Set the patches like this:
| Patch | Waveform | Envelope (A/D/S/R) | Gate | Other settings |
|---|---|---|---|---|
Neon Bass (1) |
Pulse | 0 / 4 / 9 / 2 |
5 ticks | Pulse width $800, no motion, filter off |
Neon Lead (2) |
Saw | 2 / 6 / 10 / 4 |
6 ticks | Arpeggio 0, vibrato depth 0, empty Wave program |
Neon Chord (3) |
Triangle | 1 / 5 / 8 / 3 |
5 ticks | Static, no filter routing |
Plain Drum (4) |
Noise | 0 / 2 / 0 / 1 |
2 ticks | Static noise, no sync or ring modulation |
Audition each patch. This is your control recording: save it as Neon Patrol — Plain or simply remember the sound before adding colour. Every later change should be something you can hear against this baseline.
Every tracker cell contains a note, an instrument number, and an optional three-character effect. For the baseline, leave the effect field empty and leave unlisted cells as rests (···). Enter these accented rows:
Row Voice 1 Voice 2 Voice 3
00 C-2 · 1 C-4 · 2 C-3 · 3
04 G-2 · 1 E-4 · 2 G-4 · 3
08 A#2 · 1 G-4 · 2 ··· · 4
0C G-2 · 1 E-4 · 2 C-4 · 3
10 C-2 · 1 C-4 · 2 ··· · 4
14 G-2 · 1 D-4 · 2 G-4 · 3
18 A#2 · 1 G-4 · 2 ··· · 4
1C G-2 · 1 E-4 · 2 C-4 · 3
Type notes directly: g2 becomes G-2, while g#2 becomes G#2. Keep the rhythm simple. Voice 1 is the bass, voice 2 is the melody, and voice 3 alternates chord and drum jobs. Press Play song, then use the voice checkboxes to hear the bass, lead, and harmony separately. The Volume slider in the transport changes only the browser preview level; it does not change the exported SID player’s chip volume.
Now make the baseline long enough to feel like a real cue before changing its sound:
Main riff.Neon lift, and move the final lead answer up an octave on rows 18 and 1C.Night drive, and enter a lower, sparser bass for its first half. Let the lead carry more of the rhythm.Night drive, rename the copy Finale sparks, and raise the lead one octave for the last four accented rows.Main riff → Neon lift → Main riff → Night drive → Finale sparks → Neon lift → Night drive → Finale sparks. Select a pattern and use ↑ Up or ↓ Down to move it; the selected pattern stays visible while you reorder the timeline.This is the “plain” version of Neon Patrol. Save it now so you can return to it after experimenting.
Make one listening pass with all three voices enabled, then repeat with each voice muted. The purpose is to hear the musical structure without relying on timbral tricks. Check that:
If the plain version does not work, stop here and rewrite the notes. Effects should reveal a good song, not conceal a weak one.
Duplicate your saved plain project or keep it open as the reference. After each change, press Play song, then compare the result with the plain version:
Neon lift, set Instrument 2's Arpeggio to 0, 4, 7. Replay voice 2 alone, then the full mix. The single oscillator now implies a major chord.A47 on voice 2 row 08 of Night drive. Compare that one entrance with the same row in Main riff.12¢, speed 6 Hz, delay 3. Compare a held lead note before and after; the attack should stay clear while the tail moves.U18 on the final bass entrance of Neon lift, then D12 on the answer in Night drive. These should sound like pickups and replies, not permanent bends.180 at 1.5 Hz. Toggle it back to 0 and listen for the change in width and brightness.0 = noise, tick 1 = pulse, and tick 4 = triangle. Replace one chord stab with this patch and compare the transient/body/tail shape with Plain Drum.F5A on the first voice-3 entrance of the finale. Compare the closed, plain entrance with the opening filter colour.Keep a note of which changes are improvements. The goal is not to use every effect everywhere; it is to make the same notes tell a more expressive story than the plain recording.
Once the comparisons are complete, turn the experiment into a deliberate final arrangement:
Main riff by itself. It should establish the key and rhythm with no sudden effect that sounds like an accident.Neon lift next. Check that the higher answer, brighter filter event, and faster movement feel like a lift rather than a new unrelated song.Main riff reprise. If it feels too static after the lift, add one small variation—an octave change, a passing note, or a different voice-3 hand-off—not a full rewrite.Night drive with voice 3 muted, then enabled. Its bass and lead should work alone; voice 3 should add colour, not be required to explain the harmony.Finale sparks and listen to the last four accented rows. Use a rising Uxx, falling Dxx, or filter opening only if it makes the arrival clearer. Remove anything that makes the ending feel unfinished.Main riff → Neon lift → Main riff → Night drive → Finale sparks → Neon lift → Night drive → Finale sparks. Rename any copy that still says copy..cbi. Keep Loop enabled while editing; turn it off for a one-shot cue so the generated player stops at the end.Effects are three hexadecimal characters. The first letter selects the operation; x and y are hexadecimal parameters.
| Code | Meaning | Example |
|---|---|---|
Axy |
Arpeggio: root, +x, +y semitones |
A47 = major-colour triad. |
Vxy |
Vibrato depth and speed override | V26 = gentle delayed movement. |
Uxx |
Slide pitch upward over ticks | U18 = rising bend. |
Dxx |
Slide pitch downward over ticks | D12 = falling bend. |
Fxx |
Set filter cutoff from the row | F5A = a darker-to-brighter transition. |
Use effects as punctuation. A bend on every row stops sounding like a bend; a single filter event at the start of a phrase makes the next entrance feel intentional.
Open the SID chip tab after the notes are working.
Audition both models before committing to a narrow filter setting. If the musical idea only works on one exact cutoff, simplify the patch or leave more headroom.
Choose PAL · 985248 Hz or NTSC · 1022727 Hz to match the machine you will target. The Workshop stores musical notes and derives the correct frequency words for the selected clock.
Start with low-pass mode, cutoff 1180, resonance 7, and master volume 13–15. A voice only reaches the filter when its instrument has Route through filter enabled. The live register view shows the selected voice's frequency, pulse, control, ADSR, and the shared filter registers at $D415–$D418.
Use filter movement as colour: open it for a chorus, close it for an intro, and avoid maximum resonance unless the result is deliberately unstable.
Press Play song and make one kind of change at a time:
The Craft tab's cards are reminders of the same approach: time-share voices, use fast arpeggios, program the note after gate-on, compose in three lines, and treat chip variation as part of the design.
Clear tune empties the song you are editing without making another file. Every note and pattern, the song order, the patches and their programs, the chip settings and an imported .sid's exact replay are removed, and the tune goes back to one empty pattern and one instrument at the default tempo, model and clock.
What survives is what identifies the tune rather than what it contains: its name, its author and the file it was opened from. That is the difference from New tune - clearing leaves the Workshop editing this song, so Save tune afterwards writes the empty tune over the same .c64music.json. Nothing is written to disk until you save, so clearing and changing your mind costs nothing as long as you do not save. Use New tune when you want a second tune beside this one rather than a clean slate.
Choose Save tune. In a desktop project this writes a .c64music.json file under music/ and updates the .crusty manifest's assets.music list. The file preserves:
Use Open tune to load a saved JSON project. The Workshop normalises invalid or missing values to safe defaults so an older hand-edited file can still be opened.
Choose Export .cbi to download the generated include. The generated source flattens the ordered patterns into arrays and includes a player routine.
@INCLUDE "neon-patrol.cbi"
PROC MAIN
SIDM_INIT
WHILE 1 = 1
FRAME_WAIT
SIDM_TICK
ENDWHILE
ENDPROC
The generated .cbi file includes this same example, with the actual .cbi
file name filled in. If the include is beside main.cbs in src/, paste the
shown @INCLUDE line as-is; change the path only when you store the include in
a subfolder.
Call SIDM_INIT once. Call SIDM_TICK at the display refresh cadence named in the generated file: 50 Hz for PAL or 60 Hz for NTSC. The player updates SID.FREQ, SID.ADSR, SID.CTRL, SID.PULSE_WIDTH, gate state, filter state, and master volume. Your game loop owns scheduling, pausing, and deciding how music shares time with sound effects.
A patch's Wave program is exported too. Each step writes the control register at its own tick, so a noise transient that becomes a pulse or a saw plays on the machine the way it does in the preview. A step that lands after the gate has closed is written without the gate bit, so it changes the oscillator during the release without restarting the envelope. Steps are carried in tick order and a repeated tick keeps the last of them, which is the one the preview ends up playing; at most sixteen steps per instrument are exported. A tune whose patches never change waveform carries none of it.
A patch's Arpeggio is exported whole. arpeggioRate used to be preview-only and only the first three steps reached the machine, so a four-note chord lost its top note and a rate of two played twice as fast on the C64 as in the editor; the cycle is now flattened with each step repeated arpeggioRate times, which is the sequence the previews play. A patch that does not arpeggiate generates exactly the player it always did.
A cycle's speed is measured from its second time round, not its first. The first step of a note is usually held a frame or two longer than the rest, because a player's cycle starts a frame or two after its gate does, and a speed read from it is a step too long. Zax 3 steps a four-note chord every two frames and holds the first step for three; read from the first step the import gave every note a twelve-frame cycle against the tune's eight, so the two drifted apart and by the third time round the voice was on the wrong note of its own chord. Reading the second round - and walking the compact form at the speed the steps are actually held - took the frames where the import wrote an arpeggio from 70% in tune to 83%, and 20 tunes in the 200-tune sample improved with none worse.
The instrument column is read on the rows that carry a note. Only a note starts a patch on a voice; a row with an empty note cell leaves the note still sounding on its own patch, as the pattern editor and the preview do. This matters on imported tunes, where every row carries an instrument whether it has a note or not: a held note used to be re-patched - and so re-arpeggiated - by whatever instrument the empty rows beneath it named.
A patch's Vibrato is exported too: a sine on the note's frequency, vibratoDepth cents deep at vibratoSpeed Hz, starting vibratoDelay ticks after the note. The LFO is a 32-step table read with one phase per voice rather than a sine call, because the player runs once a frame and three sine calls a frame is weight the frame cannot spare. The speed is baked into a phase step at export, since the player does not know what rate it is called at and the export does; the depth is baked as the fraction of the note's frequency those cents work out to, so it is a shade in the bass and a shade in the top octave alike. A note held across rows keeps its phase - the vibrato belongs to the note, not the row. A patch with no vibrato generates exactly the player it always did.
The .sid importer measures a tune's vibrato and sets those fields, which is where most of the collection's pitch movement lives: over a 200-tune sample, 44% of the frames the import got wrong were frames where the tune moved the pitch under an open gate, and 87% of those move the way a vibrato moves rather than the way a slide does. It is careful about it - the depth, speed and starting phase are fitted, then the LFO is simulated exactly as the player will play it and compared with the tune frame by frame in the same half-semitone the fidelity harness scores with, and a vibrato that does not put more of the note's frames in tune than a flat note does is not exported at all. That test is the reason it is worth having: the first version measured the depth and speed, skipped the phase, and made the import worse (88.2% of pitched frames in tune down to 84.9%), because a note whose vibrato begins at its peak was played back half a cycle out. It fires on about one tune in seventeen, and two tunes gained three points of pitch agreement from it.
Not exported: tremolo, pulse-motion speed, and hard restart are preview-only. The player runs the patch's base waveform, ADSR, pulse width, per-tick arpeggio, vibrato, pulse motion and row effects. Keep a note of any setting a game relies on, and confirm the result in an emulator.
If Loop is off, the generated player clears its playing flag and calls SID.ALL_OFF at the end. If you need a title-screen loop and a gameplay loop, export separate tune files or add a second player state in your game code.
The Workshop's design follows documented SID practice rather than a particular commercial score. Rob Hubbard's 1986 interview describes programmable patches and software that flips between them; Martin Galway's released Wizball source shows compact stream interpreters, patch changes, arpeggio data, pitch movement, pulse-width movement, and filter programs. The Commodore reference manual explains the underlying SID registers and envelope controls.
Read the originals for technique and hardware context, not for copying melodies or copyrighted game data:
docs/SID_MUSIC_WORKSHOP.md research and design notes in the repositoryNeon Patrol, the Workshop starter arrangement, and the generated data in this project are original examples.
music/ and appears in the project Explorer..cbi player is included and SIDM_TICK is scheduled at the documented refresh rate.