TuneC: a written tune becomes the bytes the player reads

The compiler, and the last thing the ladder was waiting for. A tune names
its instruments, writes sequences of notes and durations, and gives each
voice an order list of sequence names - which is where repetition comes
from, since a phrase played four times is written once and named four
times.

  #Tick 0d125000
  #Patch Oboe oboe.patch
  #Voice 0d0 Oboe
  #Sequence Verse
    0d64 0d4  0d67 0d4  0d72 0d8
  #Order 0d0
    Verse Verse Ending

"#" is a directive and ";" is a comment, exactly as in SplitBit assembly
and in the shell's scripts, and numbers are written the way the assembler
writes them. One rule across the machine rather than a third dialect -
and the rule earned itself immediately: the first tune I wrote said
"#Voice 0" and was refused, correctly, for a bare number.

WHAT IT REFUSES IS EVERYTHING THE PLAYER CANNOT NOTICE. The machine has
no names, so it cannot say a sequence does not exist. It has no lengths,
so it cannot say the voices will come apart four bars after the mistake.
A duration of nought is counted down to 255 and held, which sounds like a
hang rather than an error. And by the time a tune is loaded, "no starting
instrument" and "instrument nought" are the same byte - so the user's
ruling, that a voice with a part and no instrument is an error, can only
be kept here.

SoundPatch gains --blob, writing the same table as raw bytes. It stays
the only thing that reads soundThing's JSON: a second program parsing
that format is a second opinion about what a patch means, and the seam
between two opinions is where the LFO bug lived for a fortnight. Patches
are found beside the tune and then on a -I path, the way an include is.

THE TEST IS THAT TWO IMPLEMENTATIONS AGREE. maketune.py lays the fixture
out by hand and TuneC compiles a written source, and the suite checks
they match byte for byte - the discipline SplitDisk and sbfs.asm are held
to, for the same reason: either alone is only self-consistent. The
fixture predates the compiler, so this is also TuneC checked against
something written before it existed. Four more checks cover the four
refusals.

Also: the SoundPatch binary was tracked, alone among the six tools, and
.gitignore lists every other one. Untracked, and TuneC added beside it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-09-05 21:11:36 -04:00
co-authored by Claude Opus 5
parent bfc46d982e
commit 2808688fa1
10 changed files with 713 additions and 12 deletions
+37 -3
View File
@@ -25,6 +25,7 @@ wrote Asm.sbx: program 7533, data 4099, labels 555
| [`Source/DiskTool`](Source/DiskTool) | SplitDisk, which reads and writes SplitBit's filesystem |
| [`Source/Linter`](Source/Linter) | SplitLint, which points out needlessly long assembly forms |
| [`Source/Patch`](Source/Patch) | SoundPatch, which turns a soundThing patch into a table the sound device takes |
| [`Source/Tune`](Source/Tune) | TuneC, which turns a written tune into the bytes the music player reads |
| [`Programs/Examples`](Programs/Examples) | Programs to read: hello, a calculator, Fibonacci, a prime sieve, Life, the colours |
| [`Programs/Libraries`](Programs/Libraries) | Code included by name rather than linked, since there is no linker |
| [`Programs/Sounds`](Programs/Sounds) | Patches as soundThing saved them, and the tables SoundPatch made from them, both checked in so the build never needs soundThing |
@@ -116,7 +117,7 @@ Manual.
## Getting Started:
Clone it and build the five tools. You need gcc and make, or similar:
Clone it and build the six tools. You need gcc and make, or similar:
```
git clone https://github.com/RealBusinessAccount/SplitBit-Emulator.git
@@ -306,10 +307,43 @@ this is what turns such a count into a list of routine names.
Without `-o` the output takes the source file's name, in the directory you called the assembler from, with the extension the format asks for: `.bin` for a boot image and `.sbx` for a loadable program. Included files are looked for beside the file that includes them, and then along the directories given with `-I`.
## Writing A Tune: TuneC
```
./TuneC [-I <dir>] <tune> <output.tune>
```
Four voices on one clock. A tune names its instruments, writes sequences of notes and durations, and gives each voice an order list of sequence names - which is where repetition comes from, since a phrase played four times is written once and named four times.
```
; 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
#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 the shell's scripts, and numbers are written the way the assembler writes them - `0d` and `0x`, and no bare numbers. One rule across the machine rather than a third dialect.
Patches come from `SoundPatch --blob`, which stays the only thing that understands what a soundThing patch means, and are looked for beside the tune and then on the `-I` path the way an include is.
**What it refuses is everything the player cannot notice for itself.** The machine has no names, so it cannot say a sequence does not exist; it has no lengths, so it cannot say the voices will come apart four bars after the mistake; and a duration of nought is counted down to 255 and held, which sounds like a hang rather than an error. A voice with a part and no instrument is refused too, because by the time a tune is loaded, "never said" and "instrument nought" are the same byte.
## Making A Patch: SoundPatch
```
./SoundPatch <patch.json> <label> [output.asm]
./SoundPatch --blob <patch.json> <name> <output.patch>
```
The sound device is soundThing's voice engine with the editor taken off, so a patch designed
@@ -640,7 +674,7 @@ $(BUILD)/%.bin: %.asm
make test
```
Builds the five tools, checks they build clean under strict ISO C, and runs nine scripts.
Builds the six tools, checks they build clean under strict ISO C, and runs nine scripts.
`Tests/run.sh` assembles and runs every program in `Programs/` and compares the results
against recorded output; six more ask the questions a recorded file cannot answer. Between
them they check the two assemblers against each other byte for byte, the two SBFS
@@ -659,7 +693,7 @@ correct.
make sanitize
```
Rebuilds all five tools with the address and undefined behaviour sanitizers and runs the
Rebuilds all six tools with the address and undefined behaviour sanitizers and runs the
whole suite under them. It catches reads and writes past the end of an array, use after
free, leaks, and undefined arithmetic, takes about twice as long, and puts the ordinary
binaries back when it finishes.