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:
co-authored by
Claude Opus 5
parent
bfc46d982e
commit
2808688fa1
@@ -152,11 +152,30 @@ static int toByte(const Mapping *m, double value) {
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
// ---- Two ways out, one conversion ----
|
||||
//
|
||||
// Assembly for a program that includes the patch, and raw bytes for a tune that embeds
|
||||
// one. THE SAME TABLE EITHER WAY: a count and that many parameter and value pairs, which
|
||||
// is what the sound device is written with. What differs is only whether an assembler or
|
||||
// a tune compiler is going to be the one holding it.
|
||||
//
|
||||
// It matters that this 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 last sound bug lived for a fortnight.
|
||||
int raw = 0;
|
||||
if (argc > 1 && strcmp(argv[1], "--blob") == 0) {
|
||||
raw = 1;
|
||||
argv++;
|
||||
argc--;
|
||||
}
|
||||
if (argc < 3) {
|
||||
fprintf(stderr,
|
||||
"Usage: %s <patch.json> <label> [output.asm]\n\n"
|
||||
"Usage: %s <patch.json> <label> [output.asm]\n"
|
||||
" %s --blob <patch.json> <name> <output.patch>\n\n"
|
||||
"Turns a soundThing patch into a table for the sound device: a count, then that\n"
|
||||
"many parameter and value pairs. Hand the label to playPatch.\n", argv[0]);
|
||||
"many parameter and value pairs. Hand the label to playPatch.\n\n"
|
||||
"With --blob it writes those bytes and nothing else, for TuneC to embed.\n",
|
||||
argv[0], argv[0]);
|
||||
return 2;
|
||||
}
|
||||
const char *path = argv[1];
|
||||
@@ -222,6 +241,28 @@ int main(int argc, char **argv) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < MAPPING_COUNT; i++) if (seen[i]) count++;
|
||||
|
||||
// ---- Raw bytes, for something that is not an assembler ----
|
||||
//
|
||||
// A name is still asked for and still ignored here, so that the two forms are converted
|
||||
// by the same command with the same arguments and a build can switch between them by
|
||||
// adding a word.
|
||||
if (raw) {
|
||||
if (argc < 4) {
|
||||
fprintf(stderr, "SoundPatch: --blob needs somewhere to write it.\n");
|
||||
return 1;
|
||||
}
|
||||
FILE *blob = fopen(argv[3], "wb");
|
||||
if (!blob) { fprintf(stderr, "SoundPatch: cannot write %s\n", argv[3]); return 1; }
|
||||
fputc(count, blob);
|
||||
for (int i = 0; i < MAPPING_COUNT; i++) {
|
||||
if (!seen[i]) continue;
|
||||
fputc(mappings[i].parameter, blob);
|
||||
fputc(values[i], blob);
|
||||
}
|
||||
fclose(blob);
|
||||
return 0;
|
||||
}
|
||||
|
||||
FILE *out = stdout;
|
||||
if (argc > 3) {
|
||||
out = fopen(argv[3], "w");
|
||||
|
||||
Reference in New Issue
Block a user