Files
SplitBit-Emulator/SplitBit Tune Manual.md
T
AnachronautandClaude Opus 5 fb672d7761 A manual for writing tunes
The fifth document, and the one somebody would want first if they had a
piece of music and this machine. What a tune is, how to write one, what
the compiler refuses and why, how to build and play one, and the bytes it
holds for anything that would rather write one itself.

The last of those matters more than it looks: Tests/maketune.py writes
tunes without going through TuneC, and the suite checks the two agree
byte for byte. A format with two implementations needs a specification
they are both held to rather than one of them being the specification.

The refusals get a table of their own with the reason beside each,
because every one of them is something the PLAYER cannot notice - it has
no names, no lengths, and no way to tell "no starting instrument" from
"instrument nought" once a tune is loaded. Documented as reasons rather
than as rules, so that somebody meeting one knows what it saved them
from.

docs.sh now settles the manual against the compiler both ways: every
directive TuneC takes has a row, and the limits the manual quotes are the
compiler's own #defines. Verified with break.sh - a directive removed
from the manual and a limit raised in the compiler are both caught.

One that was not caught first time and should not have been: removing the
#Use row from one table left it documented in the other, which is the
check being right and my break being wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 21:20:55 -04:00

146 lines
9.3 KiB
Markdown

# The SplitBit Tune Manual
A tune is four voices on one clock. This is how you write one, what the compiler refuses, and what the bytes look like for anything that wants to read or write them without going through the compiler.
The player is `Play`, which comes with CosmOS. `Play` on its own plays the tune built into it; `Play <file>` plays a tune from a disk.
## What A Tune Is:
Three tables and one indirection, which is a tracker's shape.
**Instruments** are patches, designed in soundThing and converted by SoundPatch. **Sequences** are one voice's phrase: notes, rests, and commands. An **order list** names sequences, one list to a voice, and playing a tune is walking those four lists.
That last part is where repetition comes from, and it costs no notation at all. A bass line that plays under four different melodies is written once and named four times. A thirty-two bar piece that reuses four phrases is four phrases and a list.
**The four voices share the tick and nothing else.** Each keeps its own place in its own sequence and its own count of how much longer the note it is holding lasts, so a voice playing whole notes and a voice playing sixteenths cost the same and never have to know about each other. Nothing holds them together except that the sequences they play at the same position last the same number of ticks - which is the first thing the compiler checks, because it is the mistake that is hardest to hear and easiest to count.
## Writing One:
```
; Four bars, and the bass is the same one under two of them.
#Tick 0d125000 ; cycles a tick: a sixteenth note at 120 beats a minute
#Patch Oboe oboe.patch
#Patch Strings strings.patch
#Voice 0d0 Oboe ; which instrument each voice starts on
#Voice 0d1 Strings
#Sequence Verse
0d64 0d4 0d67 0d4 0d72 0d8
#Sequence Ending
#Use Strings ; a command, which takes no time at all
0d72 0d16
#Order 0d0
Verse Verse Ending
```
**`#` is a directive and `;` is a comment**, exactly as in SplitBit assembly and in CosmOS's scripts. One rule across the machine rather than a third dialect.
**Numbers are written the way the assembler writes them**: `0d` for decimal and `0x` for hexadecimal. A bare number is refused rather than guessed at, so `#Voice 0` is an error and `#Voice 0d0` is what you meant. There is one way to write a number on this machine and a tune is not the place to introduce a second.
Whitespace and line breaks mean nothing. A sequence runs until the next directive that is not `#Use`.
## The Directives:
| Directive | Means |
| --- | --- |
| #Tick \<cycles\> | How long one tick lasts, in the machine's own cycles. From `0d1` to `0d16777215`, which reaches from one cycle to sixteen and a half seconds. |
| #Patch \<name\> \<file\> | Names an instrument and where its bytes are. The file is what `SoundPatch --blob` writes. |
| #Voice \<voice\> \<patch\> | Which instrument a voice starts on. Voices are `0d0` to `0d3`. |
| #Sequence \<name\> | Begins a sequence. What follows, until the next directive, is its events. |
| #Use \<patch\> | Inside a sequence: play the rest of this voice on that instrument. Takes no time. |
| #Order \<voice\> | Begins a voice's order list. What follows, until the next directive, is sequence names. |
## Inside A Sequence:
Pairs: what to play, then how many ticks it lasts.
| Written | Means |
| --- | --- |
| 0d1 to 0d127 | A MIDI note. 60 is middle C and every 12 is an octave. |
| 0d0 | A rest. The ticks pass with nothing sounding. |
| #Use \<patch\> | A command. It happens between notes and consumes no tick. |
A duration is from `0d1` to `0d255` ticks.
**A note's duration is its whole life.** The gate goes down when the count runs out and the next event begins on the same tick, so a gap between two notes is *written*, as a rest, rather than invented by the player out of some fraction it decided on.
**A patch change reshapes whatever is still ringing on that voice.** Nothing can be done about that: a channel has one set of parameters and a note in its release is using them, so deferring the change to the next note would reshape the same tail. It is a fact about the sound device rather than a choice about the format. If it matters, leave a rest long enough for the release - which is the composer's to write and not the player's to guess.
## The Tick, And Why It Is In Cycles:
Cycles are what everything else on this machine is counted in: it is what the cost model counts and what a frame is measured in, so a tick counting anything else would be a second unit to remember.
At a megahertz, a beat at 120 beats a minute is 500,000 cycles. A sixteenth note is 125,000, which is what the examples here use.
**Choose a tick that divides what you want to write.** The tick is the smallest subdivision in the piece, because it is the only unit four parts can agree on. A piece with triplets wants a tick that divides by three; a piece with long held notes wants a coarser one, since a duration only reaches 255.
Before the timer existed this was not a choice. A screen frame is 16,667 cycles, so a sixteenth note at 120 was seven and a half frames and could not be asked for at all, and `Examples/tune.asm` wrote its arpeggio as seven whole frames - six and a half per cent fast.
## What The Compiler Refuses:
Everything here is something the player cannot notice for itself, which is why the compiler is the only place it can be caught.
| Refused | Because |
| --- | --- |
| Sequences at one position lasting different lengths | The voices would come apart, quietly, some bars after the mistake. The machine has no lengths and cannot tell you. |
| A voice with a part and no instrument | By the time a tune is loaded, "never said" and "instrument nought" are the same byte. Only the compiler ever sees the absence. |
| A duration of `0d0` | The player counts a duration down and reads the next event at nought, so a count starting there wraps to 255 and holds. It sounds like a hang rather than a mistake. |
| A name that is not there | The machine has no names in it at all. |
| A bare number | There is one way to write a number on this machine. |
| A patch file that is not a patch | A patch is a count and that many pairs. Anything else would write rubbish at the sound device. |
Nothing is written when anything is refused, so a failed build cannot leave a half-made tune on the disk.
## Building One:
```
./SoundPatch --blob Programs/Sounds/Oboe.json Oboe oboe.patch
./TuneC [-I <dir>] theme.tune.txt theme.tune
./SplitDisk put mydisk.img theme.tune
```
Then `Play theme.tune` on the machine.
Patches are looked for beside the tune first and then on the `-I` path, the way the assembler looks for an include - so a tune can name its instruments by bare name and travel with them.
**SoundPatch is the only thing that understands soundThing's JSON.** TuneC embeds the bytes it is given and knows nothing about what a patch means. A second program parsing that format would be a second opinion about it, and the seam between two opinions is where a sound bug can live for a fortnight.
## Limits:
| Thing | How many |
| --- | --- |
| Voices | 4 |
| Patches | 64 |
| Sequences | 200. The ceiling is 255, because 0xFF ends an order list. |
| Events in a sequence | 1024 bytes of them |
| Entries in an order list | 256 |
| Ticks in a duration | 255 |
| Cycles in a tick | 16,777,215 |
## The Bytes:
For anything that wants to read or write a tune without going through the compiler. `Tests/maketune.py` does exactly that, and the suite checks that it and TuneC agree byte for byte - two implementations of one format, which is the same discipline SplitDisk and `sbfs.asm` are held to, and for the same reason: either alone is only self-consistent.
| Offset | Size | Holds |
| --- | --- | --- |
| 0 | 4 | `SBTU` |
| 4 | 1 | Version, 1 |
| 5 | 3 | The tick in cycles, most significant byte first |
| 8 | 1 | How many patches |
| 9 | 1 | How many sequences |
| 10 | 2 | Offset of the patch table |
| 12 | 2 | Offset of the sequence table |
| 14 | 8 | Offset of each voice's order list, four of them |
| 22 | 4 | The patch index each voice starts on, four of them |
| 26 | 2 | Reserved, zero |
The **patch table** is one two-byte offset per patch, each naming a count and that many parameter and value pairs. The **sequence table** is one two-byte offset per sequence. An **order list** is one-byte sequence indices ending in `0xFF`. A **sequence** is events, ending in `0xFF`, where 1 to 127 is a note and 0 is a rest - each followed by a duration - and 0x80 is a command followed by one byte of patch index.
**Everything in a tune is an offset from its first byte**, so loading one is adding the load address to the two tables and pointing four voices at their order lists. No sequence is walked and nothing inside one is an address, which is what makes a malformed tune something that plays wrongly rather than something that takes the loader with it. The magic is checked first, because from there on the loader follows what the offsets name.
**An offset of zero means a voice has no part.** That is safe here for a reason worth stating, because the same trick was got wrong once: offset zero is where the magic lives, so no structure can ever begin there. It is impossible by construction rather than by a fact about one program's base address - which is what an earlier version of the order list terminator relied on, and why a boot image based at zero read its own first sequence as the end of the list and played silence.