Audio
Four channels, 64 sound effect slots, 64 music patterns. All of it is RAM — sfx data and music patterns can be poked from code and heard the next time the slot plays. Audio is inside the determinism contract: the machine renders the same samples for the same state, on every host.
These sections teach the synthesized SFX/music format shared by all tiers. New editor projects also support priority/gain/pitch/envelope control. O16/O32 add four PCM voices: open sfx, import/audition WAV slots in the sample bank editor, and use sample() in Lua. A legacy O16/O32 project offers enable PCM samples as an explicit contract upgrade. O8 keeps synthesis. See the extended audio API and bank limits; keep export source archives to retain original WAV files and import settings.
sfx slots
Each slot is 64 bytes: 31 notes of two bytes, plus a speed byte at position 31. The two bytes of a note:
| byte | bits | field |
|---|---|---|
| 0 | 0-5 | pitch 0-63 (33 is 440 Hz, one semitone per step) |
| 0 | 6-7 | wave, low 2 bits |
| 1 | 0 | wave, high bit |
| 1 | 1-2 | effect: 0 none, 1 slide |
| 1 | 3-5 | volume 0-7 |
| 1 | 6-7 | reserved, zero |
Waves: 0 square, 1 triangle, 2 saw, 3 noise, 4 buzz (25% duty square). A note with both bytes zero is the end marker.
The fastest way to a sound is the editor: the sfx tab edits notes directly and auditions them in place. Its stop button ends an audition early. From code, the same data is two poke calls per note. The API reference's blip as a complete cart:
function _init()
-- sfx 0: two square notes at full volume, then the end marker
poke(0x8000, 45) poke(0x8001, 0x38)
poke(0x8002, 50) poke(0x8003, 0x38)
poke(0x8004, 0) poke(0x8005, 0)
end
function _update()
if btnp(4) then sfx(0) end
end
function _draw()
cls(0)
print("a: blip", 46, 60, 13)
endSlot 0 starts at 0x8000; slot n starts at 0x8000 + n * 64. A full-volume note with no effect is always 0x38 for byte 1 — volume 7 lives at bits 3-5, and 7 << 3 is 0x38.
Packing the wave into byte 0: the wave's low 2 bits occupy bits 6-7, so byte0 = pitch | (wave << 6) for waves 0-3. A triangle A (pitch 33, volume 7) is 33 | 0x40 = 0x61 and 0x38. Buzz is wave 4 — the one value that uses byte 1's high bit: 45 and 0x38 | 0x01 = 0x39.
Volume, speed, and slides
The volume field is per note, so fades are a note sequence, not an envelope. With a short speed the fade is heard as one sound:
function _init()
-- sfx 1: one pitch dying away, three frames per note
local vol = {7, 6, 5, 4, 3, 2, 1}
for i = 0, 6 do
local a = 0x8040 + i * 2
poke(a, 45)
poke(a + 1, vol[i + 1] << 3)
end
poke(0x8040 + 14, 0) poke(0x8040 + 15, 0)
poke(0x8040 + 62, 3)
end
function _update()
if btnp(4) then sfx(1) end
end
function _draw()
cls(0)
print("a: fade out", 38, 60, 13)
endThe speed byte (position 31, byte 0) is frames per note, 1-63; zero means the default 6. Fast speeds make note-by-note arpeggios sound like one instrument — try pitches 36, 40, 43, 48 at speed 2 for a classic chip arpeggio.
Effect 1 is a slide: the note glides in semitone space from its own pitch to the next note's pitch, across its whole duration. It lives at bits 1-2 of byte 1, so a full-volume slide is 0x38 | 0x02 = 0x3a:
function _init()
-- sfx 2: slide up from pitch 36 to pitch 48, then hold the target
poke(0x8080, 36) poke(0x8081, 0x3a)
poke(0x8082, 48) poke(0x8083, 0x38)
poke(0x8084, 0) poke(0x8085, 0)
end
function _update()
if btnp(4) then sfx(2) end
end
function _draw()
cls(0)
print("a: slide", 46, 60, 13)
endNoise (wave 3) over a falling pitch sequence is the standard explosion: poke(a, 30 | 0xc0) descending to 24 | 0xc0. The first game guide uses exactly that for its stomp sound.
sfx(n) plays slot n on the first free channel. When all four channels are busy, that call has no effect. sfx(-1) or sfx() stops sound effects while music continues.
Use sfx(n, channel) to put an effect on a specific channel 0-3. It replaces an earlier effect on that channel, so a high-priority sound does not disappear behind a long, less important one. sfx(-1, channel) stops that one effect. A channel assigned in the active music pattern is reserved even after its track finishes; leave it silent when you want to use it for effects. Out-of-range channel numbers do nothing.
For example, leave channel 3 silent in every music pattern, play routine sounds normally, and send a damage cue to channel 3:
if picked_up_coin then sfx(4) end
if took_damage then sfx(5, 3) endIf a routine sound is already playing on channel 3, damage replaces it. The other three channels and the music timeline continue. This control is deterministic: the same calls and machine state select the same voices in replays.
Music
Music sequences sfx slots. A pattern is four bytes at 0x9200 + n * 4 — one per channel: the sfx slot to play, or 0x7f for silent. Bit 7 of byte 0 marks a loop begin, bit 7 of byte 1 a loop end. A pattern plays until its longest track finishes, then the next pattern starts; empty patterns are skipped.
music(n) starts from the first non-empty pattern at or after n; music() stops. Slots held by music tracks keep their channels, so sfx() calls land on whatever is free. The music editor shows how many channels each pattern assigns; reserve a silent channel across patterns for critical effects.
A two-pattern loop built entirely from code — two slots, both channels, looping forever (this is the API reference's worked example):
function _init()
-- sfx 0: two square notes; sfx 1: one triangle note
poke(0x8000, 40) poke(0x8001, 0x38)
poke(0x8002, 45) poke(0x8003, 0x38)
poke(0x8040, 0x61) poke(0x8041, 0x38)
-- pattern 0: slot 0 on ch0 + slot 1 on ch1, loop begins here
poke(0x9200, 0x80 | 0) poke(0x9201, 1) poke(0x9202, 0x7f) poke(0x9203, 0x7f)
-- pattern 1: slot 1 on ch0 + slot 0 on ch1, loop ends here
poke(0x9204, 1) poke(0x9205, 0x80 | 0) poke(0x9206, 0x7f) poke(0x9207, 0x7f)
music(0)
end
function _draw()
cls(0)
print("two-pattern loop", 26, 60, 13)
endPattern 0 sounds slot 0 and slot 1 together; when the longer slot ends, pattern 1 swaps the channels; the end flag jumps back to the pattern with the begin flag — forever, until music().
The music tab assigns slots to channels per pattern and sets the loop flags. Preview pattern auditions the selected pattern; preview song starts there and plays through the first loop end (one pass), or through the remaining patterns if there is no loop. Preview is capped at 30 seconds and the status line says when that limit is reached. Stop cancels rendering or stops playback. Both previews use the machine's own audio renderer, and the player's mute control also mutes editor previews. Code like the example above is useful for understanding what the tab stores and for carts that build music at runtime.
A practical arrangement shape: channel 0 lead, channel 1 bass, channel 2 arpeggio, channel 3 drums (noise slots). Give each channel its own slots; a pattern is just which slot each channel plays. The seed cart bro is four patterns of two channels, built in the tab.
Tempo
There is no tempo field. A pattern's length is its longest track, and a track's length is its slot's note count times the slot's speed — so tempo is the speed byte. Speed 6 (the default) at 60 frames per second is ten notes per second; speed 12 is half that. To change the tempo of a finished sequence, change the speed of every slot it uses.
Determinism
The VM renders 22050 Hz mono PCM per frame as part of the frame output, integer-exact, bit-identical on every host. Two consequences worth knowing:
- A cart's soundtrack is not an asset alongside the code — it is state
the state-hash covers. Same cart, same inputs, same audio, down to the sample.
- The sfx and music editors cannot drift from the runtime format,
because they edit the same RAM the machine plays.