// TuneC.c // Turns a written tune into the bytes SplitBit's music player reads. // Written by Anachronaut // // ---- What a tune is ---- // // Three tables and one indirection, which is a tracker's own shape. INSTRUMENTS are patches // designed in soundThing and converted by SoundPatch. SEQUENCES are one voice's phrase, notes // and rests and commands. An ORDER LIST names sequences, one list a voice, and that is where // repetition comes from: a bass line that plays under four different melodies is written once // and named four times. // // ---- The syntax is SplitBit's ---- // // `#` is a directive and `;` is a comment, exactly as in SplitBit assembly and in the shell's // scripts. One rule across the machine rather than a third dialect, and numbers are written // the way the assembler writes them: 0d for decimal and 0x for hexadecimal, with no bare // numbers, so that a person reading a tune is reading something they already know. // // ; 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 0 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 0 // Verse Verse Ending // // ---- What it checks, and why those ---- // // Everything here is something the player cannot notice for itself. It has no names, so it // cannot say that a sequence does not exist; it has no lengths, so it cannot say that the // voices will come apart four bars in; and by the time a tune is loaded, "no starting patch" // and "starting patch nought" are the same byte. // // A DURATION OF NOUGHT IS THE DANGEROUS ONE. The player counts a duration down and reads the // next event when it reaches nought, so a count that starts there is decremented to 255 and // holds for a very long time. It is the one mistake in a tune that sounds like a hang. #include #include #include #include #define MAX_PATCHES 64 #define MAX_SEQUENCES 200 // 0xFF ends an order list, so 255 is the ceiling. #define MAX_EVENTS 1024 #define MAX_ORDER 256 #define MAX_NAME 64 #define VOICES 4 #define HEADER_BYTES 28 #define RED "\x1B[31m" #define RESET "\x1B[0m" typedef struct { char name[MAX_NAME]; unsigned char bytes[512]; int length; int line; } Patch; typedef struct { char name[MAX_NAME]; unsigned char bytes[MAX_EVENTS]; int length; int ticks; // What it adds up to, which is what the columns are checked on. int line; } Sequence; static Patch patches[MAX_PATCHES]; static int patchCount = 0; static Sequence sequences[MAX_SEQUENCES]; static int sequenceCount = 0; static int order[VOICES][MAX_ORDER]; static int orderLength[VOICES]; static int voiceStart[VOICES]; // -1 until said, which is how "never said" is told from nought. static long tick = -1; static const char *sourceName = ""; static int problems = 0; // ---- Where a patch is looked for ---- // // Beside the tune first, then wherever -I says, which is the assembler's rule for an // #Include. A tune that names its instruments by bare name can then be moved about with them, // and a build that generates its patches somewhere else can say where without the tune having // to know a build directory exists. #define MAX_INCLUDES 16 static const char *includes[MAX_INCLUDES]; static int includeCount = 0; static char sourceDirectory[512] = ""; static FILE *openPatch(const char *name, char *found, size_t room) { FILE *in = NULL; if (sourceDirectory[0]) { snprintf(found, room, "%s/%s", sourceDirectory, name); if ((in = fopen(found, "rb"))) return in; } for (int i = 0; i < includeCount; i++) { snprintf(found, room, "%s/%s", includes[i], name); if ((in = fopen(found, "rb"))) return in; } snprintf(found, room, "%s", name); return fopen(found, "rb"); } static void complain(int line, const char *what, const char *detail) { fprintf(stderr, RED "%s:%d: %s%s%s\n" RESET, sourceName, line, what, detail ? ": " : "", detail ? detail : ""); problems++; } // ---- Tokens ---- // // A token is a run of anything that is not a space. A semicolon takes the rest of its line // with it, which is the assembler's rule and the shell's. typedef struct { char text[MAX_NAME]; int line; } Token; static Token tokens[16384]; static int tokenCount = 0; static int at = 0; static void readTokens(FILE *in) { int c, line = 1; while ((c = fgetc(in)) != EOF) { if (c == '\n') { line++; continue; } if (isspace(c)) continue; if (c == ';') { while ((c = fgetc(in)) != EOF && c != '\n'); line++; continue; } if (tokenCount >= (int)(sizeof(tokens) / sizeof(tokens[0]))) { fprintf(stderr, RED "%s: too many words in one tune.\n" RESET, sourceName); exit(1); } Token *t = &tokens[tokenCount++]; t->line = line; int n = 0; while (c != EOF && !isspace(c) && c != ';') { if (n < MAX_NAME - 1) t->text[n++] = (char)c; c = fgetc(in); } t->text[n] = '\0'; if (c == ';') { ungetc(c, in); } else if (c == '\n') { line++; } } } static Token *next(void) { return at < tokenCount ? &tokens[at++] : NULL; } static Token *peek(void) { return at < tokenCount ? &tokens[at] : NULL; } static int isDirective(const Token *t) { return t && t->text[0] == '#'; } // ---- Numbers, spelled the way the assembler spells them ---- // // 0d and 0x and nothing else. A bare number is refused rather than guessed at, because this // machine has one way of writing one and a tune is not the place to introduce a second. static int number(const Token *t, long *out) { if (!t) return 0; const char *s = t->text; if (s[0] == '0' && (s[1] == 'd' || s[1] == 'D')) { char *end; *out = strtol(s + 2, &end, 10); return *end == '\0' && s[2] != '\0'; } if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) { char *end; *out = strtol(s + 2, &end, 16); return *end == '\0' && s[2] != '\0'; } return 0; } static int findPatch(const char *name) { for (int i = 0; i < patchCount; i++) if (strcmp(patches[i].name, name) == 0) return i; return -1; } static int findSequence(const char *name) { for (int i = 0; i < sequenceCount; i++) if (strcmp(sequences[i].name, name) == 0) return i; return -1; } // ---- Reading a tune ---- static void doTick(void) { Token *t = next(); long value; if (!number(t, &value)) { complain(t ? t->line : 0, "#Tick wants a number of cycles", t ? t->text : "nothing"); return; } if (value < 1 || value > 0xFFFFFF) { complain(t->line, "a tick has to be between 1 and 16,777,215 cycles", t->text); return; } tick = value; } static void doPatch(void) { Token *name = next(); Token *file = next(); if (!name || !file || isDirective(name) || isDirective(file)) { complain(name ? name->line : 0, "#Patch wants a name and a file", NULL); return; } if (patchCount >= MAX_PATCHES) { complain(name->line, "too many patches", name->text); return; } if (findPatch(name->text) >= 0) { complain(name->line, "that patch is named twice", name->text); return; } char where[1024]; FILE *in = openPatch(file->text, where, sizeof(where)); if (!in) { complain(file->line, "cannot find that patch beside the tune or on the -I path", file->text); return; } Patch *p = &patches[patchCount]; snprintf(p->name, MAX_NAME, "%s", name->text); p->length = (int)fread(p->bytes, 1, sizeof(p->bytes), in); p->line = name->line; fclose(in); // A patch is a count and that many pairs. Anything else is a file that is not one, and // embedding it would make a tune that writes rubbish at the sound device. if (p->length < 1 || p->length != 1 + p->bytes[0] * 2) { complain(file->line, "that is not a patch: SoundPatch --blob writes one", file->text); return; } patchCount++; } static void doVoice(void) { Token *which = next(); Token *name = next(); long v; if (!number(which, &v) || v < 0 || v >= VOICES) { complain(which ? which->line : 0, "#Voice wants a voice from 0d0 to 0d3", which ? which->text : "nothing"); return; } if (!name || isDirective(name)) { complain(which->line, "#Voice wants a patch to start that voice on", NULL); return; } int p = findPatch(name->text); if (p < 0) { complain(name->line, "no patch of that name", name->text); return; } voiceStart[v] = p; } static void doSequence(void) { Token *name = next(); if (!name || isDirective(name)) { complain(name ? name->line : 0, "#Sequence wants a name", NULL); return; } if (sequenceCount >= MAX_SEQUENCES) { complain(name->line, "too many sequences", name->text); return; } if (findSequence(name->text) >= 0) { complain(name->line, "that sequence is named twice", name->text); return; } Sequence *q = &sequences[sequenceCount]; snprintf(q->name, MAX_NAME, "%s", name->text); q->length = 0; q->ticks = 0; q->line = name->line; for (;;) { Token *t = peek(); if (!t) break; if (isDirective(t)) { // #Use is the one directive that belongs INSIDE a sequence. Anything else ends it. if (strcmp(t->text, "#Use") != 0) break; next(); Token *what = next(); if (!what || isDirective(what)) { complain(t->line, "#Use wants a patch", NULL); break; } int p = findPatch(what->text); if (p < 0) { complain(what->line, "no patch of that name", what->text); continue; } q->bytes[q->length++] = 0x80; q->bytes[q->length++] = (unsigned char)p; continue; } long note, ticksFor; if (!number(t, ¬e)) { complain(t->line, "a sequence holds notes and durations", t->text); next(); continue; } next(); Token *d = next(); if (!number(d, &ticksFor)) { complain(d ? d->line : t->line, "that note has no duration after it", t->text); break; } if (note < 0 || note > 127) { complain(t->line, "a note is 0d0 for a rest, or 0d1 to 0d127", t->text); } // The one that sounds like a hang. The player counts a duration down and reads the // next event at nought, so a count that starts there wraps to 255 and holds. if (ticksFor < 1 || ticksFor > 255) { complain(d->line, "a duration is from 0d1 to 0d255 ticks", d->text); } if (q->length + 2 >= MAX_EVENTS) { complain(t->line, "that sequence is too long", q->name); break; } q->bytes[q->length++] = (unsigned char)(note & 0x7F); q->bytes[q->length++] = (unsigned char)(ticksFor & 0xFF); q->ticks += (int)ticksFor; } q->bytes[q->length++] = 0xFF; sequenceCount++; } static void doOrder(void) { Token *which = next(); long v; if (!number(which, &v) || v < 0 || v >= VOICES) { complain(which ? which->line : 0, "#Order wants a voice from 0d0 to 0d3", which ? which->text : "nothing"); return; } for (;;) { Token *t = peek(); if (!t || isDirective(t)) break; next(); int q = findSequence(t->text); if (q < 0) { complain(t->line, "no sequence of that name", t->text); continue; } if (orderLength[v] >= MAX_ORDER - 1) { complain(t->line, "that order list is too long", t->text); break; } order[v][orderLength[v]++] = q; } } // ---- What the player cannot check for itself ---- static void check(void) { if (tick < 0) { fprintf(stderr, RED "%s: no #Tick, so there is no beat to play it at.\n" RESET, sourceName); problems++; } for (int v = 0; v < VOICES; v++) { // The ruling: a voice with a part and no instrument is an error rather than a default. // By the time a tune is loaded this cannot be noticed - "never said" and "patch // nought" are the same byte - so it has to be noticed here or not at all. if (orderLength[v] > 0 && voiceStart[v] < 0) { fprintf(stderr, RED "%s: voice %d has a part and no instrument. Say #Voice 0d%d.\n" RESET, sourceName, v, v); problems++; } } // ---- The columns have to add up ---- // // Nothing keeps four voices together except that the sequences they play at the same // position last the same number of ticks. Get one wrong and the parts come apart, quietly, // some bars after the mistake - which is the hardest kind of fault to find by ear and the // easiest kind to find here. int longest = 0; for (int v = 0; v < VOICES; v++) if (orderLength[v] > longest) longest = orderLength[v]; for (int i = 0; i < longest; i++) { int want = -1, wantVoice = -1; for (int v = 0; v < VOICES; v++) { if (i >= orderLength[v]) continue; Sequence *q = &sequences[order[v][i]]; if (want < 0) { want = q->ticks; wantVoice = v; continue; } if (q->ticks != want) { fprintf(stderr, RED "%s:%d: at position %d, %s lasts %d ticks and voice %d's %s" " lasts %d. The voices would come apart here.\n" RESET, sourceName, q->line, i, q->name, q->ticks, wantVoice, sequences[order[wantVoice][i]].name, want); problems++; } } } } // ---- Laying it out ---- static unsigned char out[65536]; static int outLength = 0; static void put(int byte) { if (outLength >= (int)sizeof(out)) { fprintf(stderr, RED "%s: that tune does not fit in 64K.\n" RESET, sourceName); exit(1); } out[outLength++] = (unsigned char)byte; } static void putWord(int at, int value) { out[at] = (unsigned char)((value >> 8) & 0xFF); out[at + 1] = (unsigned char)(value & 0xFF); } int main(int argc, char **argv) { while (argc > 2 && strcmp(argv[1], "-I") == 0) { if (includeCount < MAX_INCLUDES) includes[includeCount++] = argv[2]; argv += 2; argc -= 2; } if (argc < 3) { fprintf(stderr, "Usage: %s [-I ] \n\n" "Turns a written tune into the bytes SplitBit's player reads: a header, a table\n" "of patches, a table of sequences, and an order list for each of four voices.\n\n" "Patches come from SoundPatch --blob, which is the only thing that knows what a\n" "soundThing patch means. They are looked for beside the tune and then on the -I\n" "path, the way the assembler looks for an include.\n", argv[0]); return 2; } sourceName = argv[1]; const char *slash = strrchr(sourceName, '/'); if (slash) { size_t n = (size_t)(slash - sourceName); if (n >= sizeof(sourceDirectory)) n = sizeof(sourceDirectory) - 1; memcpy(sourceDirectory, sourceName, n); sourceDirectory[n] = '\0'; } FILE *in = fopen(sourceName, "r"); if (!in) { fprintf(stderr, RED "TuneC: cannot read %s\n" RESET, sourceName); return 1; } readTokens(in); fclose(in); for (int v = 0; v < VOICES; v++) { voiceStart[v] = -1; orderLength[v] = 0; } Token *t; while ((t = next()) != NULL) { if (!isDirective(t)) { complain(t->line, "that is not inside anything", t->text); continue; } if (strcmp(t->text, "#Tick") == 0) doTick(); else if (strcmp(t->text, "#Patch") == 0) doPatch(); else if (strcmp(t->text, "#Voice") == 0) doVoice(); else if (strcmp(t->text, "#Sequence") == 0) doSequence(); else if (strcmp(t->text, "#Order") == 0) doOrder(); else complain(t->line, "no such directive", t->text); } check(); if (problems) { fprintf(stderr, RED "TuneC: %d problem%s, so nothing was written.\n" RESET, problems, problems == 1 ? "" : "s"); return 1; } // The tables come first so that everything they name can be placed after them and its // offset known as it lands. outLength = HEADER_BYTES; int patchTableAt = outLength; outLength += 2 * patchCount; int sequenceTableAt = outLength; outLength += 2 * sequenceCount; for (int i = 0; i < patchCount; i++) { putWord(patchTableAt + 2 * i, outLength); for (int b = 0; b < patches[i].length; b++) put(patches[i].bytes[b]); } for (int i = 0; i < sequenceCount; i++) { putWord(sequenceTableAt + 2 * i, outLength); for (int b = 0; b < sequences[i].length; b++) put(sequences[i].bytes[b]); } int orderAt[VOICES]; for (int v = 0; v < VOICES; v++) { orderAt[v] = outLength; for (int i = 0; i < orderLength[v]; i++) put(order[v][i]); put(0xFF); } memcpy(out, "SBTU", 4); out[4] = 1; out[5] = (unsigned char)((tick >> 16) & 0xFF); out[6] = (unsigned char)((tick >> 8) & 0xFF); out[7] = (unsigned char)(tick & 0xFF); out[8] = (unsigned char)patchCount; out[9] = (unsigned char)sequenceCount; putWord(10, patchTableAt); putWord(12, sequenceTableAt); for (int v = 0; v < VOICES; v++) putWord(14 + 2 * v, orderAt[v]); // A voice with no part still needs a byte here, and nought is as good as any: the player // loads it and never plays a note with it. for (int v = 0; v < VOICES; v++) out[22 + v] = (unsigned char)(voiceStart[v] < 0 ? 0 : voiceStart[v]); out[26] = 0; out[27] = 0; FILE *o = fopen(argv[2], "wb"); if (!o) { fprintf(stderr, RED "TuneC: cannot write %s\n" RESET, argv[2]); return 1; } fwrite(out, 1, outLength, o); fclose(o); printf("Wrote %s: %d bytes, %d patch%s, %d sequence%s.\n", argv[2], outLength, patchCount, patchCount == 1 ? "" : "es", sequenceCount, sequenceCount == 1 ? "" : "s"); return 0; }