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
@@ -9,6 +9,8 @@
|
||||
/SplitDisk
|
||||
/SplitLint
|
||||
/Voyager
|
||||
/SoundPatch
|
||||
/TuneC
|
||||
/CLAUDE.md
|
||||
/claudeResume.sh
|
||||
/codexResume.sh
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
; two.tune.txt
|
||||
; The smallest thing that is a tune: one voice, two sequences, and an instrument change.
|
||||
; Written by Anachronaut
|
||||
;
|
||||
; BOTH NOTES ARE NUMBER 60. The octave between them is the #Use in the second sequence, which
|
||||
; loads the other patch - so what this plays says the whole path works: a header, a tick taken
|
||||
; from it, two tables relocated, an order list followed, and a patch out of the file.
|
||||
|
||||
#Tick 0d125000 ; A sixteenth note at 120 beats a minute.
|
||||
|
||||
#Patch Low low.patch
|
||||
#Patch High high.patch
|
||||
|
||||
#Voice 0d0 Low ; What voice nought starts on. The other three have no part.
|
||||
|
||||
#Sequence Plain
|
||||
0d60 0d4
|
||||
|
||||
#Sequence Octave
|
||||
#Use High ; A command, which takes no tick at all.
|
||||
0d60 0d4
|
||||
|
||||
#Order 0d0
|
||||
Plain Octave
|
||||
@@ -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.
|
||||
|
||||
BIN
Binary file not shown.
@@ -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");
|
||||
|
||||
@@ -0,0 +1,513 @@
|
||||
// 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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#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 <dir>] <tune> <output.tune>\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;
|
||||
}
|
||||
@@ -37,7 +37,7 @@ Everything in between is somewhere on that line.
|
||||
make test
|
||||
```
|
||||
|
||||
Builds the five tools - and Voyager, where Raylib is installed - checks they compile under
|
||||
Builds the six tools - and Voyager, where Raylib is installed - checks they compile under
|
||||
strict ISO C, and runs the scripts in order. Takes a few seconds. Everything must pass; there are no expected failures at the
|
||||
level of the suite, only tests that record an expected failure of the assembler.
|
||||
|
||||
@@ -45,7 +45,7 @@ level of the suite, only tests that record an expected failure of the assembler.
|
||||
make sanitize
|
||||
```
|
||||
|
||||
The same suite with the five tools rebuilt under AddressSanitizer and
|
||||
The same suite with the six tools rebuilt under AddressSanitizer and
|
||||
UndefinedBehaviorSanitizer. See [The Sanitizer Run](#the-sanitizer-run).
|
||||
|
||||
Individual scripts can be run on their own, from anywhere:
|
||||
@@ -540,7 +540,7 @@ which stops the markers outliving the code they were about.
|
||||
make sanitize
|
||||
```
|
||||
|
||||
Rebuilds all five tools with `-fsanitize=address,undefined` and runs **the whole suite**
|
||||
Rebuilds all six tools with `-fsanitize=address,undefined` and runs **the whole suite**
|
||||
under them. What it reliably catches is invalid access: reads and writes off the end of an
|
||||
array, use after free, leaks, and arithmetic the standard does not define.
|
||||
|
||||
@@ -552,7 +552,7 @@ deliberate `calloc`. The machine's Program and Data memories are static arrays,
|
||||
sanitizers neither fill nor bound-check - which is the same fact, seen from a
|
||||
different side, as the overrun blind spot below.
|
||||
|
||||
It runs everything because it used to not. It built all five tools sanitized and then ran
|
||||
It runs everything because it used to not. It built all six tools sanitized and then ran
|
||||
only `run.sh` and `terminal.sh`, so SplitDisk was compiled with the sanitizers and never
|
||||
exercised, and `native.sh` - which drives the assembler and the emulator harder than
|
||||
anything else here - was skipped entirely. Those are exactly where block arithmetic on disk
|
||||
|
||||
@@ -713,6 +713,29 @@ printf 'system /System/Boot/cosmos.bin\n' > "$WORK/lonely.cfg"
|
||||
# directory listing that ten recorded tests quote.
|
||||
python3 "$ROOT/Tests/maketune.py" "$WORK/two.tune" >/dev/null
|
||||
"$TOOL" put "$DISKS/quiet.img" "$WORK/two.tune" two.tune >/dev/null
|
||||
|
||||
# ---- And the same tune, written down and compiled ----
|
||||
#
|
||||
# TWO IMPLEMENTATIONS OF ONE FORMAT, which is the discipline SplitDisk and sbfs.asm are held
|
||||
# to: maketune.py lays the bytes out by hand and TuneC compiles a written source, and the
|
||||
# suite checks that they agree byte for byte. Either one alone would only be self-consistent.
|
||||
#
|
||||
# The patches are SoundPatch's, in the raw form TuneC embeds. Two blobs differing in one
|
||||
# parameter, the octave, so the same note number sounds an octave apart.
|
||||
"$ROOT/SoundPatch" --blob "$ROOT/Programs/Sounds/Oboe.json" Low "$WORK/low.patch" >/dev/null
|
||||
python3 - "$WORK/low.patch" "$WORK/high.patch" <<'FIXTURE'
|
||||
# The fixture patches are written here rather than converted, because what they have to be is
|
||||
# two patches identical but for the octave - which is a fact about the test and not about any
|
||||
# instrument anybody designed.
|
||||
import sys
|
||||
def blob(octave):
|
||||
pairs = [(0x00, 2), (0x01, 0xFF), (0x05, 1), (0x20, 0),
|
||||
(0x21, 0), (0x22, 0xFF), (0x23, 5), (0x04, octave)]
|
||||
return bytes([len(pairs)]) + b"".join(bytes(p) for p in pairs)
|
||||
open(sys.argv[1], "wb").write(blob(128))
|
||||
open(sys.argv[2], "wb").write(blob(129))
|
||||
FIXTURE
|
||||
"$ROOT/TuneC" -I "$WORK" "$ROOT/Programs/Tunes/two.tune.txt" "$WORK/compiled.tune" >/dev/null
|
||||
"$ROOT/Assembler" -I "$ROOT/Programs/Sounds" -I "$ROOT/Programs/Libraries" \
|
||||
-I "$ROOT/Programs/CosmOS/Source" \
|
||||
"$ROOT/Programs/CosmOS/Apps/Play.asm" -o "$WORK/Play.sbx" >/dev/null
|
||||
|
||||
@@ -817,6 +817,55 @@ PY2
|
||||
|| result no "and a command in it loads a patch from it" "$SECOND hertz, wanted the octave"
|
||||
fi
|
||||
|
||||
# ---- The compiler and the hex editor agree ----
|
||||
#
|
||||
# TWO IMPLEMENTATIONS OF ONE FORMAT, which is the discipline SplitDisk and sbfs.asm are
|
||||
# held to and for the same reason: either alone is only self-consistent. maketune.py lays
|
||||
# the bytes out by hand and TuneC compiles a written source, and what the player reads is
|
||||
# right only if both agree about what it should say.
|
||||
#
|
||||
# The fixture is what Play was tested against before TuneC existed, so this is also the
|
||||
# compiler being checked against something that predates it.
|
||||
if cmp -s "$ROOT/Tests/build/.diskwork/compiled.tune" \
|
||||
"$ROOT/Tests/build/.diskwork/two.tune"; then
|
||||
result ok "TuneC writes what the hand-laid fixture does" "byte for byte"
|
||||
else
|
||||
result no "TuneC writes what the hand-laid fixture does" \
|
||||
"$(cmp "$ROOT/Tests/build/.diskwork/compiled.tune" "$ROOT/Tests/build/.diskwork/two.tune" 2>&1 | head -1)"
|
||||
fi
|
||||
|
||||
# ---- And it refuses what the player could not notice ----
|
||||
#
|
||||
# Each of these is something a tune can get wrong that the machine cannot tell you about.
|
||||
# The player 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 later; and a duration of nought is
|
||||
# counted down to 255 and held, which sounds like a hang rather than a mistake.
|
||||
refuses() {
|
||||
# refuses <what it should say> <line>...
|
||||
local want="$1"; shift
|
||||
printf '%s\n' "$@" > "$BUILD/bad.tune.txt"
|
||||
local said
|
||||
said="$("$ROOT/TuneC" -I "$ROOT/Tests/build/.diskwork" "$BUILD/bad.tune.txt" \
|
||||
"$BUILD/bad.tune" 2>&1)"
|
||||
if [ -f "$BUILD/bad.tune" ]; then rm -f "$BUILD/bad.tune"; fi
|
||||
case "$said" in
|
||||
*"$want"*) result ok "TuneC refuses $want" "and says which line" ;;
|
||||
*) result no "TuneC refuses $want" "$(echo "$said" | head -1)" ;;
|
||||
esac
|
||||
}
|
||||
refuses "would come apart" \
|
||||
"#Tick 0d125000" "#Patch A low.patch" "#Voice 0d0 A" "#Voice 0d1 A" \
|
||||
"#Sequence Four" "0d60 0d4" "#Sequence Eight" "0d60 0d8" \
|
||||
"#Order 0d0" "Four" "#Order 0d1" "Eight"
|
||||
refuses "a part and no instrument" \
|
||||
"#Tick 0d125000" "#Patch A low.patch" "#Sequence S" "0d60 0d4" "#Order 0d0" "S"
|
||||
refuses "a duration is from" \
|
||||
"#Tick 0d125000" "#Patch A low.patch" "#Voice 0d0 A" "#Sequence S" "0d60 0d0" \
|
||||
"#Order 0d0" "S"
|
||||
refuses "no sequence of that name" \
|
||||
"#Tick 0d125000" "#Patch A low.patch" "#Voice 0d0 A" "#Sequence S" "0d60 0d4" \
|
||||
"#Order 0d0" "Nope"
|
||||
|
||||
# ---- And a file that is not a tune is refused ----
|
||||
#
|
||||
# The loader reads offsets out of a file and follows what they name, so a file that is not
|
||||
|
||||
@@ -50,6 +50,7 @@ SRC_DIR_ASM = Source/Assembler
|
||||
SRC_DIR_DSK = Source/DiskTool
|
||||
SRC_DIR_LINT = Source/Linter
|
||||
SRC_DIR_PATCH = Source/Patch
|
||||
SRC_DIR_TUNE = Source/Tune
|
||||
OBJ_DIR = Object
|
||||
|
||||
# Source files
|
||||
@@ -65,6 +66,7 @@ ASM_SRCS = Assembler.c assembly.c firstPass.c Assm-util.c secondPass.c
|
||||
DSK_SRCS = SplitDisk.c
|
||||
LINT_SRCS = Linter.c
|
||||
PATCH_SRCS = SoundPatch.c
|
||||
TUNE_SRCS = TuneC.c
|
||||
|
||||
EMU_OBJS = $(EMU_SRCS:%.c=$(OBJ_DIR)/%.o)
|
||||
VOY_OBJS = $(VOY_SRCS:%.c=$(OBJ_DIR)/%.o)
|
||||
@@ -72,6 +74,7 @@ ASM_OBJS = $(ASM_SRCS:%.c=$(OBJ_DIR)/%.o)
|
||||
DSK_OBJS = $(DSK_SRCS:%.c=$(OBJ_DIR)/%.o)
|
||||
LINT_OBJS = $(LINT_SRCS:%.c=$(OBJ_DIR)/%.o) $(OBJ_DIR)/assembly.o
|
||||
PATCH_OBJS = $(PATCH_SRCS:%.c=$(OBJ_DIR)/%.o)
|
||||
TUNE_OBJS = $(TUNE_SRCS:%.c=$(OBJ_DIR)/%.o)
|
||||
|
||||
# Output binary names
|
||||
EMU_TARGET = SplitBit
|
||||
@@ -80,6 +83,7 @@ ASM_TARGET = Assembler
|
||||
DSK_TARGET = SplitDisk
|
||||
LINT_TARGET = SplitLint
|
||||
PATCH_TARGET = SoundPatch
|
||||
TUNE_TARGET = TuneC
|
||||
|
||||
# ---- Whether this machine can build Voyager ----
|
||||
#
|
||||
@@ -212,7 +216,7 @@ SCRATCH_BLOCKS = 2048
|
||||
# linter, the whole test suite - has to build on a machine with no graphics library at all,
|
||||
# because a project about a small understandable CPU should not need OpenGL to run its
|
||||
# tests. Where Raylib is missing, 'make' says so once and builds everything else.
|
||||
TOOLS = $(EMU_TARGET) $(ASM_TARGET) $(DSK_TARGET) $(LINT_TARGET) $(PATCH_TARGET)
|
||||
TOOLS = $(EMU_TARGET) $(ASM_TARGET) $(DSK_TARGET) $(LINT_TARGET) $(PATCH_TARGET) $(TUNE_TARGET)
|
||||
|
||||
# ---- What 'make' builds ----
|
||||
#
|
||||
@@ -305,6 +309,17 @@ $(PATCH_TARGET): $(PATCH_OBJS)
|
||||
$(CC) $(CFLAGS) -o $(PATCH_TARGET) $(PATCH_OBJS) -lm
|
||||
|
||||
# Compile patch tool source files to object files
|
||||
# ---- TuneC, which turns a written tune into the bytes the player reads ----
|
||||
#
|
||||
# It needs nothing: patches reach it as bytes from SoundPatch, which stays the only thing that
|
||||
# understands what a soundThing patch means.
|
||||
$(TUNE_TARGET): $(TUNE_OBJS)
|
||||
$(CC) $(CFLAGS) -o $(TUNE_TARGET) $(TUNE_OBJS)
|
||||
|
||||
$(OBJ_DIR)/%.o: $(SRC_DIR_TUNE)/%.c
|
||||
@mkdir -p $(OBJ_DIR)
|
||||
$(CC) $(CFLAGS) -MMD -MP -c $< -o $@
|
||||
|
||||
$(OBJ_DIR)/%.o: $(SRC_DIR_PATCH)/%.c
|
||||
@mkdir -p $(OBJ_DIR)
|
||||
$(CC) $(CFLAGS) $(POSIXFLAGS) $(DEPFLAGS) -c $< -o $@
|
||||
@@ -329,7 +344,7 @@ $(OBJ_DIR)/%.o: $(SRC_DIR_ASM)/%.c
|
||||
# disagreed with everything else about how big the struct was, and smashed its stack on every
|
||||
# run. A clean build hid it, which is why 'make sanitize' would never have found it either.
|
||||
# Tests/voyager.sh did, by failing all 115 tests that start the machine.
|
||||
-include $(EMU_OBJS:.o=.d) $(VOY_OBJS:.o=.d) $(ASM_OBJS:.o=.d) $(DSK_OBJS:.o=.d) $(LINT_OBJS:.o=.d) $(PATCH_OBJS:.o=.d)
|
||||
-include $(EMU_OBJS:.o=.d) $(VOY_OBJS:.o=.d) $(ASM_OBJS:.o=.d) $(DSK_OBJS:.o=.d) $(LINT_OBJS:.o=.d) $(PATCH_OBJS:.o=.d) $(TUNE_OBJS:.o=.d)
|
||||
|
||||
# ---- The strict build the README promises ----
|
||||
#
|
||||
@@ -443,7 +458,7 @@ clean:
|
||||
rm -rf Tests/build
|
||||
rm -rf $(PROG_BUILD)
|
||||
rm -f $(SRC_DIR_EMU)/rom.c
|
||||
rm -f $(EMU_TARGET) $(ASM_TARGET) $(DSK_TARGET) $(LINT_TARGET) $(VOY_TARGET) $(PATCH_TARGET)
|
||||
rm -f $(EMU_TARGET) $(ASM_TARGET) $(DSK_TARGET) $(LINT_TARGET) $(VOY_TARGET) $(PATCH_TARGET) $(TUNE_TARGET)
|
||||
|
||||
# Install compiled binaries
|
||||
install: $(EMU_TARGET) $(ASM_TARGET) $(DSK_TARGET) $(LINT_TARGET)
|
||||
|
||||
Reference in New Issue
Block a user