709 lines
23 KiB
C
709 lines
23 KiB
C
// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
|
// See LICENSE.md for license information.
|
|
// SPDX-License-Identifier: MIT
|
|
//
|
|
// songbake.c - bake JoeyLib tracker text notation into a JYM1 event stream.
|
|
//
|
|
// One authored .song file plays on every JoeyLib port through the
|
|
// jlMusicPlay sequencer (src/core/music.c), which drives the jlAudioVoice
|
|
// 3-voice tone layer and the jlAudioNoise percussion generator. See music.c
|
|
// for the JYM1 stream layout this tool emits.
|
|
//
|
|
// Input format (one row per line, ';' starts a comment):
|
|
//
|
|
// tickms 110 directive: ms per row. Required before the first
|
|
// row; later occurrences emit a tempo change event at
|
|
// that point in the song.
|
|
// loop directive: the NEXT row is the loop-back target
|
|
// repeat 4 directive: repeat the rows down to 'endrepeat'
|
|
// endrepeat 4 times (repeats may nest; 'loop' inside a repeat
|
|
// block is rejected)
|
|
// C-2:2 A-4:5 --- row: 3 tone cells, or 4 cells with noise last
|
|
// C-2:2 --- --- N4:6
|
|
//
|
|
// Tone cell forms:
|
|
// C-4:3 note-on, note name + octave, ':' + AGI attenuation 0..14 (0 =
|
|
// loudest). ':atten' may be omitted (defaults to 0). Sharps use
|
|
// '#': C#4:3. Note range C-0..B-7.
|
|
// @440:3 note-on at an exact frequency in Hz (1..65535) -- for
|
|
// chip-accurate transcription work where equal-temperament note
|
|
// names would round the pitch.
|
|
// --- hold: no event for this voice this row
|
|
// off silence this voice
|
|
//
|
|
// Noise cell forms (4th column):
|
|
// N4:3 noise-on, pitch 0..31 (0 = brightest hiss, 31 = lowest rumble),
|
|
// ':' + attenuation as above
|
|
// --- hold
|
|
// off silence the noise voice
|
|
//
|
|
// On the Atari ST the noise generator shares channel C's volume with tone
|
|
// voice 2; songbake warns when both are audible at once.
|
|
//
|
|
// A row lasts one tick. Rows with no events compress into wait bytes.
|
|
//
|
|
// Usage:
|
|
// songbake song.song song.jym binary stream
|
|
// songbake -c gName song.song song.h C header (uint8_t array)
|
|
|
|
#include <ctype.h>
|
|
#include <math.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "toolBuf.h"
|
|
#include "toolFile.h"
|
|
|
|
#define TOOL_NAME "songbake"
|
|
#define USAGE_EXIT 2
|
|
#define OP_END 0x00u
|
|
#define OP_NOTE 0x10u
|
|
#define OP_OFF 0x20u
|
|
#define OP_TEMPO 0x40u
|
|
#define OP_WAIT 0x80u
|
|
#define WAIT_MAX 0x7Fu
|
|
#define LOOP_NONE 0xFFFFu
|
|
#define TONE_VOICES 3u
|
|
#define NOISE_VOICE 3u
|
|
#define NOISE_PITCH_MAX 31u
|
|
#define ATTEN_MAX 14u
|
|
#define TICK_MS_MAX 1000u
|
|
#define REPEAT_MAX 256u
|
|
#define FREQ_MAX 0xFFFFu
|
|
#define JYM_MAGIC "JYM1"
|
|
#define JYM_MAGIC_BYTES 4u
|
|
// Equal temperament: A-4 (MIDI note 69) is 440 Hz, 12 notes an octave, and
|
|
// octave 0 starts at MIDI note 12.
|
|
#define A4_HZ 440.0
|
|
#define A4_MIDI 69
|
|
#define OCTAVE_NOTES 12
|
|
#define OCTAVE_MAX 7
|
|
// A row's cells, and the most of a line's words kept (enough to tell a row
|
|
// of too many cells by its count).
|
|
#define CELLS_MAX 5u
|
|
// The ST overlap warning names this many lines at most.
|
|
#define OVERLAP_SHOWN 8u
|
|
// The C header's array: this many bytes a line.
|
|
#define C_BYTES_PER_LINE 12u
|
|
#define ASCII_MAX 0x7Fu
|
|
// A number's digits saturate here: anything this big is out of every range.
|
|
#define NUMBER_CAP 0x7FFFFFFFu
|
|
|
|
// One line of the source that is not blank: its number, its words (the
|
|
// first CELLS_MAX), and how many words it has.
|
|
typedef struct LineT {
|
|
uint32_t lineNum;
|
|
uint32_t cellCount;
|
|
char *cells[CELLS_MAX];
|
|
} LineT;
|
|
|
|
// A row of the song once repeats are unrolled: its line, and whether it
|
|
// came from inside a repeat block.
|
|
typedef struct RowT {
|
|
const LineT *line;
|
|
bool inRepeat;
|
|
} RowT;
|
|
|
|
typedef struct RowListT {
|
|
RowT *rows;
|
|
size_t count;
|
|
size_t cap;
|
|
} RowListT;
|
|
|
|
// An open repeat block: its count and the rows in it so far.
|
|
typedef struct BlockT {
|
|
uint32_t count;
|
|
RowListT rows;
|
|
} BlockT;
|
|
|
|
// A parsed cell: held, off, or on with a value (Hz or noise pitch) and an
|
|
// attenuation.
|
|
typedef enum CellKindE {
|
|
CELL_HOLD = 0,
|
|
CELL_OFF,
|
|
CELL_ON
|
|
} CellKindE;
|
|
|
|
typedef struct CellT {
|
|
CellKindE kind;
|
|
uint32_t value;
|
|
uint32_t atten;
|
|
} CellT;
|
|
|
|
static void bake(const LineT *lines, size_t lineCount, ToolBufT *blob);
|
|
static bool digits(const char *s, size_t len, uint32_t *value);
|
|
static void expandRepeats(const LineT *lines, size_t lineCount, RowListT *out);
|
|
static void fail(uint32_t lineNum, const char *fmt, ...) __attribute__((noreturn, format(printf, 2, 3)));
|
|
static void flushWait(ToolBufT *events, uint32_t *pendingWait);
|
|
static bool isWord(const char *cell, const char *word);
|
|
static size_t linesRead(char *text, size_t len, LineT **lines);
|
|
static uint32_t noteToFreq(const char *cell, size_t len, uint32_t lineNum);
|
|
static void overlapNote(uint32_t *shown, uint32_t *shownCount, uint32_t lineNum);
|
|
static CellT parseNoiseCell(const char *cell, uint32_t lineNum);
|
|
static CellT parseToneCell(const char *cell, uint32_t lineNum);
|
|
static void rowAdd(RowListT *list, const LineT *line, bool inRepeat);
|
|
static size_t splitAtten(const char *cell, uint32_t lineNum, uint32_t *atten);
|
|
static void usage(void);
|
|
static void writeCHeader(const char *path, const char *name, const ToolBufT *blob, const char *srcName);
|
|
|
|
static const char *const kNoteNames[OCTAVE_NOTES] = {
|
|
"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"
|
|
};
|
|
|
|
|
|
// The song's lines into its JYM1 stream: the header, then the events.
|
|
static void bake(const LineT *lines, size_t lineCount, ToolBufT *blob) {
|
|
RowListT rows;
|
|
ToolBufT events;
|
|
ToolBufT rowEvents;
|
|
CellT cell;
|
|
const LineT *line;
|
|
uint32_t tickMs;
|
|
uint32_t loopOfs;
|
|
uint32_t pendingWait;
|
|
uint32_t ms;
|
|
uint32_t voice;
|
|
uint32_t shown[OVERLAP_SHOWN];
|
|
uint32_t shownCount;
|
|
uint32_t i;
|
|
bool haveTick;
|
|
bool loopPending;
|
|
bool sawRow;
|
|
bool voice2On;
|
|
bool noiseOn;
|
|
size_t r;
|
|
|
|
memset(&rows, 0, sizeof(rows));
|
|
memset(&events, 0, sizeof(events));
|
|
memset(&rowEvents, 0, sizeof(rowEvents));
|
|
expandRepeats(lines, lineCount, &rows);
|
|
tickMs = 0u;
|
|
haveTick = false;
|
|
loopOfs = LOOP_NONE;
|
|
loopPending = false;
|
|
pendingWait = 0u;
|
|
sawRow = false;
|
|
voice2On = false;
|
|
noiseOn = false;
|
|
shownCount = 0u;
|
|
for (r = 0; r < rows.count; r++) {
|
|
line = rows.rows[r].line;
|
|
if (isWord(line->cells[0], "tickms")) {
|
|
if (line->cellCount != 2u || !digits(line->cells[1], strlen(line->cells[1]), &ms)) {
|
|
fail(line->lineNum, "usage: tickms <1..%u>", TICK_MS_MAX);
|
|
}
|
|
if (ms < 1u || ms > TICK_MS_MAX) {
|
|
fail(line->lineNum, "tickms out of range (1..%u)", TICK_MS_MAX);
|
|
}
|
|
if (!haveTick) {
|
|
tickMs = ms;
|
|
haveTick = true;
|
|
} else {
|
|
// Mid-song tempo change: flush waits owed at the old tempo,
|
|
// then emit the change event.
|
|
flushWait(&events, &pendingWait);
|
|
toolBufByte(&events, OP_TEMPO);
|
|
toolBufLe16(&events, ms);
|
|
}
|
|
continue;
|
|
}
|
|
if (isWord(line->cells[0], "loop")) {
|
|
if (line->cellCount != 1u) {
|
|
fail(line->lineNum, "'loop' takes no arguments");
|
|
}
|
|
if (rows.rows[r].inRepeat) {
|
|
fail(line->lineNum, "'loop' inside a repeat block is ambiguous");
|
|
}
|
|
if (loopOfs != LOOP_NONE || loopPending) {
|
|
fail(line->lineNum, "second 'loop' directive");
|
|
}
|
|
loopPending = true;
|
|
continue;
|
|
}
|
|
if (line->cellCount != TONE_VOICES && line->cellCount != TONE_VOICES + 1u) {
|
|
fail(line->lineNum, "want %u or %u voice cells, got %u", TONE_VOICES, TONE_VOICES + 1u, line->cellCount);
|
|
}
|
|
rowEvents.len = 0u;
|
|
for (voice = 0; voice < TONE_VOICES; voice++) {
|
|
cell = parseToneCell(line->cells[voice], line->lineNum);
|
|
if (cell.kind == CELL_HOLD) {
|
|
continue;
|
|
}
|
|
if (cell.kind == CELL_OFF) {
|
|
toolBufByte(&rowEvents, OP_OFF + voice);
|
|
} else {
|
|
toolBufByte(&rowEvents, OP_NOTE + voice);
|
|
toolBufLe16(&rowEvents, cell.value);
|
|
toolBufByte(&rowEvents, cell.atten);
|
|
}
|
|
if (voice == TONE_VOICES - 1u) {
|
|
voice2On = cell.kind == CELL_ON;
|
|
}
|
|
}
|
|
if (line->cellCount == TONE_VOICES + 1u) {
|
|
cell = parseNoiseCell(line->cells[NOISE_VOICE], line->lineNum);
|
|
if (cell.kind == CELL_OFF) {
|
|
toolBufByte(&rowEvents, OP_OFF + NOISE_VOICE);
|
|
noiseOn = false;
|
|
} else if (cell.kind == CELL_ON) {
|
|
toolBufByte(&rowEvents, OP_NOTE + NOISE_VOICE);
|
|
toolBufLe16(&rowEvents, cell.value);
|
|
toolBufByte(&rowEvents, cell.atten);
|
|
noiseOn = true;
|
|
}
|
|
}
|
|
if (voice2On && noiseOn) {
|
|
overlapNote(shown, &shownCount, line->lineNum);
|
|
}
|
|
// The loop target is the first byte this row contributes; flush
|
|
// queued waits first so the jump replays them from the row itself,
|
|
// not from silence belonging to earlier rows.
|
|
if (loopPending || rowEvents.len > 0u) {
|
|
flushWait(&events, &pendingWait);
|
|
}
|
|
if (loopPending) {
|
|
loopOfs = (uint32_t)events.len;
|
|
loopPending = false;
|
|
}
|
|
toolBufAdd(&events, rowEvents.data, rowEvents.len);
|
|
pendingWait++;
|
|
sawRow = true;
|
|
}
|
|
if (!haveTick) {
|
|
fail(0u, "missing 'tickms' directive");
|
|
}
|
|
if (!sawRow) {
|
|
fail(0u, "song has no rows");
|
|
}
|
|
if (loopPending) {
|
|
fail(0u, "'loop' directive with no row after it");
|
|
}
|
|
flushWait(&events, &pendingWait);
|
|
toolBufByte(&events, OP_END);
|
|
if (events.len >= LOOP_NONE) {
|
|
fail(0u, "event stream exceeds 64 KB");
|
|
}
|
|
if (shownCount > 0u) {
|
|
fprintf(stderr, "%s: warning: tone voice 2 and noise are audible together (lines ", TOOL_NAME);
|
|
for (i = 0; i < shownCount; i++) {
|
|
fprintf(stderr, "%s%u", i == 0u ? "" : ", ", shown[i]);
|
|
}
|
|
fprintf(stderr, "); on the Atari ST they share channel C's volume\n");
|
|
}
|
|
toolBufAdd(blob, JYM_MAGIC, JYM_MAGIC_BYTES);
|
|
toolBufLe16(blob, tickMs);
|
|
toolBufLe16(blob, loopOfs);
|
|
toolBufAdd(blob, events.data, events.len);
|
|
toolBufFree(&rowEvents);
|
|
toolBufFree(&events);
|
|
free(rows.rows);
|
|
}
|
|
|
|
|
|
// len bytes of s as a decimal number (saturating at NUMBER_CAP); false
|
|
// unless all of them, at least one, are digits.
|
|
static bool digits(const char *s, size_t len, uint32_t *value) {
|
|
size_t i;
|
|
|
|
if (len == 0u) {
|
|
return false;
|
|
}
|
|
*value = 0u;
|
|
for (i = 0; i < len; i++) {
|
|
if (!isdigit((unsigned char)s[i])) {
|
|
return false;
|
|
}
|
|
if (*value < NUMBER_CAP / 10u) {
|
|
*value = *value * 10u + (uint32_t)(s[i] - '0');
|
|
} else {
|
|
*value = NUMBER_CAP;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// Unroll repeat/endrepeat blocks (nesting allowed), keeping each row's line
|
|
// for error reporting.
|
|
static void expandRepeats(const LineT *lines, size_t lineCount, RowListT *out) {
|
|
BlockT *stack;
|
|
RowListT *dest;
|
|
uint32_t count;
|
|
uint32_t n;
|
|
size_t depth;
|
|
size_t i;
|
|
size_t r;
|
|
|
|
stack = (BlockT *)calloc(lineCount + 1u, sizeof(BlockT));
|
|
if (stack == NULL) {
|
|
toolFail("out of memory");
|
|
}
|
|
depth = 0u;
|
|
for (i = 0; i < lineCount; i++) {
|
|
if (isWord(lines[i].cells[0], "repeat")) {
|
|
if (lines[i].cellCount != 2u || !digits(lines[i].cells[1], strlen(lines[i].cells[1]), &count)) {
|
|
fail(lines[i].lineNum, "usage: repeat <count>");
|
|
}
|
|
if (count < 1u || count > REPEAT_MAX) {
|
|
fail(lines[i].lineNum, "repeat count out of range (1..%u)", REPEAT_MAX);
|
|
}
|
|
memset(&stack[depth], 0, sizeof(BlockT));
|
|
stack[depth].count = count;
|
|
depth++;
|
|
continue;
|
|
}
|
|
if (isWord(lines[i].cells[0], "endrepeat")) {
|
|
if (depth == 0u) {
|
|
fail(lines[i].lineNum, "endrepeat without repeat");
|
|
}
|
|
depth--;
|
|
dest = depth > 0u ? &stack[depth - 1u].rows : out;
|
|
for (n = 0; n < stack[depth].count; n++) {
|
|
for (r = 0; r < stack[depth].rows.count; r++) {
|
|
rowAdd(dest, stack[depth].rows.rows[r].line, stack[depth].rows.rows[r].inRepeat);
|
|
}
|
|
}
|
|
free(stack[depth].rows.rows);
|
|
continue;
|
|
}
|
|
rowAdd(depth > 0u ? &stack[depth - 1u].rows : out, &lines[i], depth > 0u);
|
|
}
|
|
if (depth > 0u) {
|
|
fail(0u, "repeat without endrepeat");
|
|
}
|
|
free(stack);
|
|
}
|
|
|
|
|
|
// "songbake: line N: message" (no line for a whole-song error, lineNum 0)
|
|
// to stderr, and exit 1.
|
|
static void fail(uint32_t lineNum, const char *fmt, ...) {
|
|
va_list args;
|
|
|
|
fprintf(stderr, "%s: ", TOOL_NAME);
|
|
if (lineNum != 0u) {
|
|
fprintf(stderr, "line %u: ", lineNum);
|
|
}
|
|
va_start(args, fmt);
|
|
vfprintf(stderr, fmt, args);
|
|
va_end(args);
|
|
fputc('\n', stderr);
|
|
exit(1);
|
|
}
|
|
|
|
|
|
// The rows waited so far as wait bytes, WAIT_MAX at most each.
|
|
static void flushWait(ToolBufT *events, uint32_t *pendingWait) {
|
|
uint32_t chunk;
|
|
|
|
while (*pendingWait > 0u) {
|
|
chunk = *pendingWait < WAIT_MAX ? *pendingWait : WAIT_MAX;
|
|
toolBufByte(events, OP_WAIT | chunk);
|
|
*pendingWait -= chunk;
|
|
}
|
|
}
|
|
|
|
|
|
static bool isWord(const char *cell, const char *word) {
|
|
return strcmp(cell, word) == 0;
|
|
}
|
|
|
|
|
|
// The text's lines that are not blank once their comment (from ';') is
|
|
// gone, split into words in place; how many. Lines end at "\n", "\r\n", or
|
|
// "\r", as Python's text files read them.
|
|
static size_t linesRead(char *text, size_t len, LineT **lines) {
|
|
LineT *out;
|
|
size_t count;
|
|
size_t at;
|
|
size_t end;
|
|
size_t next;
|
|
size_t i;
|
|
uint32_t lineNum;
|
|
char *p;
|
|
char *comment;
|
|
LineT *line;
|
|
|
|
out = (LineT *)malloc((len + 1u) * sizeof(LineT));
|
|
if (out == NULL) {
|
|
toolFail("out of memory");
|
|
}
|
|
count = 0u;
|
|
lineNum = 0u;
|
|
for (at = 0; at < len; at = next) {
|
|
for (end = at; end < len && text[end] != '\n' && text[end] != '\r'; end++) {
|
|
}
|
|
next = end + ((end + 1u < len && text[end] == '\r' && text[end + 1u] == '\n') ? 2u : 1u);
|
|
text[end] = '\0';
|
|
lineNum++;
|
|
comment = strchr(text + at, ';');
|
|
if (comment != NULL) {
|
|
*comment = '\0';
|
|
}
|
|
line = &out[count];
|
|
line->lineNum = lineNum;
|
|
line->cellCount = 0u;
|
|
for (p = text + at; *p != '\0';) {
|
|
while (isspace((unsigned char)*p)) {
|
|
p++;
|
|
}
|
|
if (*p == '\0') {
|
|
break;
|
|
}
|
|
if (line->cellCount < CELLS_MAX) {
|
|
line->cells[line->cellCount] = p;
|
|
}
|
|
line->cellCount++;
|
|
while (*p != '\0' && !isspace((unsigned char)*p)) {
|
|
p++;
|
|
}
|
|
if (*p != '\0') {
|
|
*p++ = '\0';
|
|
}
|
|
}
|
|
for (i = line->cellCount; i < CELLS_MAX; i++) {
|
|
line->cells[i] = "";
|
|
}
|
|
if (line->cellCount > 0u) {
|
|
count++;
|
|
}
|
|
}
|
|
*lines = out;
|
|
return count;
|
|
}
|
|
|
|
|
|
// 'C-4' / 'C#4' (len bytes of cell) as its equal-temperament Hz, A-4 = 440.
|
|
static uint32_t noteToFreq(const char *cell, size_t len, uint32_t lineNum) {
|
|
size_t nameLen;
|
|
uint32_t note;
|
|
int octave;
|
|
int midi;
|
|
double freq;
|
|
|
|
nameLen = len > 0u ? len - 1u : 0u;
|
|
while (nameLen > 0u && cell[nameLen - 1u] == '-') {
|
|
nameLen--;
|
|
}
|
|
for (note = 0; note < OCTAVE_NOTES; note++) {
|
|
if (strlen(kNoteNames[note]) == nameLen && strncmp(cell, kNoteNames[note], nameLen) == 0) {
|
|
break;
|
|
}
|
|
}
|
|
if (len == 0u || note == OCTAVE_NOTES || !isdigit((unsigned char)cell[len - 1u])) {
|
|
fail(lineNum, "bad note '%.*s' (want e.g. C-4, C#4, A-2)", (int)len, cell);
|
|
}
|
|
octave = cell[len - 1u] - '0';
|
|
if (octave > OCTAVE_MAX) {
|
|
fail(lineNum, "octave out of range in '%.*s' (0..%d)", (int)len, cell, OCTAVE_MAX);
|
|
}
|
|
midi = (octave + 1) * OCTAVE_NOTES + (int)note;
|
|
freq = nearbyint(A4_HZ * pow(2.0, (double)(midi - A4_MIDI) / (double)OCTAVE_NOTES));
|
|
if (freq < 1.0 || freq > (double)FREQ_MAX) {
|
|
fail(lineNum, "'%.*s' is out of the 16-bit Hz range", (int)len, cell);
|
|
}
|
|
return (uint32_t)freq;
|
|
}
|
|
|
|
|
|
// lineNum among the first OVERLAP_SHOWN distinct lines, smallest first.
|
|
static void overlapNote(uint32_t *shown, uint32_t *shownCount, uint32_t lineNum) {
|
|
uint32_t i;
|
|
uint32_t j;
|
|
|
|
for (i = 0; i < *shownCount && shown[i] < lineNum; i++) {
|
|
}
|
|
if ((i < *shownCount && shown[i] == lineNum) || i == OVERLAP_SHOWN) {
|
|
return;
|
|
}
|
|
if (*shownCount < OVERLAP_SHOWN) {
|
|
(*shownCount)++;
|
|
}
|
|
for (j = *shownCount - 1u; j > i; j--) {
|
|
shown[j] = shown[j - 1u];
|
|
}
|
|
shown[i] = lineNum;
|
|
}
|
|
|
|
|
|
// A noise cell: held, off, or a pitch (0..31) at an attenuation.
|
|
static CellT parseNoiseCell(const char *cell, uint32_t lineNum) {
|
|
CellT out;
|
|
size_t bodyLen;
|
|
|
|
memset(&out, 0, sizeof(out));
|
|
if (isWord(cell, "---")) {
|
|
return out;
|
|
}
|
|
if (isWord(cell, "off")) {
|
|
out.kind = CELL_OFF;
|
|
return out;
|
|
}
|
|
bodyLen = splitAtten(cell, lineNum, &out.atten);
|
|
if (bodyLen == 0u || cell[0] != 'N' || !digits(cell + 1, bodyLen - 1u, &out.value)) {
|
|
fail(lineNum, "bad noise cell '%s' (want e.g. N4:3, N31)", cell);
|
|
}
|
|
if (out.value > NOISE_PITCH_MAX) {
|
|
fail(lineNum, "noise pitch out of range in '%s' (0..%u)", cell, NOISE_PITCH_MAX);
|
|
}
|
|
out.kind = CELL_ON;
|
|
return out;
|
|
}
|
|
|
|
|
|
// A tone cell: held, off, or a frequency (a note, or @Hz) at an
|
|
// attenuation.
|
|
static CellT parseToneCell(const char *cell, uint32_t lineNum) {
|
|
CellT out;
|
|
size_t bodyLen;
|
|
|
|
memset(&out, 0, sizeof(out));
|
|
if (isWord(cell, "---")) {
|
|
return out;
|
|
}
|
|
if (isWord(cell, "off")) {
|
|
out.kind = CELL_OFF;
|
|
return out;
|
|
}
|
|
bodyLen = splitAtten(cell, lineNum, &out.atten);
|
|
if (bodyLen > 0u && cell[0] == '@') {
|
|
if (!digits(cell + 1, bodyLen - 1u, &out.value)) {
|
|
fail(lineNum, "bad frequency cell '%s' (want e.g. @440:3)", cell);
|
|
}
|
|
if (out.value < 1u || out.value > FREQ_MAX) {
|
|
fail(lineNum, "frequency out of range in '%s' (1..%u)", cell, FREQ_MAX);
|
|
}
|
|
} else {
|
|
out.value = noteToFreq(cell, bodyLen, lineNum);
|
|
}
|
|
out.kind = CELL_ON;
|
|
return out;
|
|
}
|
|
|
|
|
|
static void rowAdd(RowListT *list, const LineT *line, bool inRepeat) {
|
|
RowT *rows;
|
|
size_t cap;
|
|
|
|
if (list->count == list->cap) {
|
|
cap = list->cap == 0u ? 64u : list->cap * 2u;
|
|
rows = (RowT *)realloc(list->rows, cap * sizeof(RowT));
|
|
if (rows == NULL) {
|
|
toolFail("out of memory");
|
|
}
|
|
list->rows = rows;
|
|
list->cap = cap;
|
|
}
|
|
list->rows[list->count].line = line;
|
|
list->rows[list->count].inRepeat = inRepeat;
|
|
list->count++;
|
|
}
|
|
|
|
|
|
// A cell's attenuation, after its last ':' (0 when it has none): the
|
|
// length of the cell before it.
|
|
static size_t splitAtten(const char *cell, uint32_t lineNum, uint32_t *atten) {
|
|
const char *colon;
|
|
|
|
colon = strrchr(cell, ':');
|
|
*atten = 0u;
|
|
if (colon == NULL) {
|
|
return strlen(cell);
|
|
}
|
|
if (!digits(colon + 1, strlen(colon + 1), atten) || *atten > ATTEN_MAX) {
|
|
fail(lineNum, "bad attenuation in '%s' (0..%u)", cell, ATTEN_MAX);
|
|
}
|
|
return (size_t)(colon - cell);
|
|
}
|
|
|
|
|
|
static void usage(void) {
|
|
fprintf(stderr, "usage: %s [-c NAME] input.song output\n", TOOL_NAME);
|
|
exit(USAGE_EXIT);
|
|
}
|
|
|
|
|
|
// The stream as a C header: a static uint8_t array named name.
|
|
static void writeCHeader(const char *path, const char *name, const ToolBufT *blob, const char *srcName) {
|
|
FILE *f;
|
|
size_t i;
|
|
bool ok;
|
|
|
|
f = fopen(path, "w");
|
|
if (f == NULL) {
|
|
toolFail("cannot write %s", path);
|
|
}
|
|
fprintf(f, "// Generated by tools/songbake from %s.\n", srcName);
|
|
fprintf(f, "// Do not hand-edit; edit the .song source and rebake:\n");
|
|
fprintf(f, "// build/tools/songbake -c %s %s <this file>\n\n", name, srcName);
|
|
fprintf(f, "#ifndef JOEYLIB_BAKED_");
|
|
for (i = 0; name[i] != '\0'; i++) {
|
|
fputc(toupper((unsigned char)name[i]), f);
|
|
}
|
|
fprintf(f, "_H\n#define JOEYLIB_BAKED_");
|
|
for (i = 0; name[i] != '\0'; i++) {
|
|
fputc(toupper((unsigned char)name[i]), f);
|
|
}
|
|
fprintf(f, "_H\n\n#include <stdint.h>\n\n");
|
|
fprintf(f, "static const uint8_t %s[%u] = {\n", name, (unsigned)blob->len);
|
|
for (i = 0; i < blob->len; i++) {
|
|
fprintf(f, "%s0x%02X%s", i % C_BYTES_PER_LINE == 0u ? " " : " ", blob->data[i], (i % C_BYTES_PER_LINE == C_BYTES_PER_LINE - 1u || i == blob->len - 1u) ? ",\n" : ",");
|
|
}
|
|
fprintf(f, "};\n\n#endif\n");
|
|
ok = !ferror(f);
|
|
ok = fclose(f) == 0 && ok;
|
|
if (!ok) {
|
|
toolFail("cannot write %s", path);
|
|
}
|
|
}
|
|
|
|
|
|
int main(int argc, char **argv) {
|
|
const char *cName;
|
|
char *text;
|
|
uint8_t *data;
|
|
size_t len;
|
|
size_t i;
|
|
size_t lineCount;
|
|
int a;
|
|
LineT *lines;
|
|
ToolBufT blob;
|
|
|
|
toolFileName(TOOL_NAME);
|
|
cName = NULL;
|
|
a = 1;
|
|
if (argc > 1 && strcmp(argv[1], "-c") == 0) {
|
|
if (argc < 3) {
|
|
fail(0u, "-c needs a symbol name");
|
|
}
|
|
cName = argv[2];
|
|
a = 3;
|
|
}
|
|
if (argc - a != 2) {
|
|
usage();
|
|
}
|
|
if (!toolReadFile(argv[a], &data, &len)) {
|
|
toolFail("cannot read %s", argv[a]);
|
|
}
|
|
text = (char *)data;
|
|
for (i = 0; i < len; i++) {
|
|
if ((unsigned char)text[i] > ASCII_MAX) {
|
|
toolFail("%s is not ASCII (byte %zu)", argv[a], i);
|
|
}
|
|
}
|
|
lineCount = linesRead(text, len, &lines);
|
|
memset(&blob, 0, sizeof(blob));
|
|
bake(lines, lineCount, &blob);
|
|
if (cName != NULL) {
|
|
writeCHeader(argv[a + 1], cName, &blob, argv[a]);
|
|
} else if (!toolWriteFile(argv[a + 1], blob.data, blob.len)) {
|
|
toolFail("cannot write %s", argv[a + 1]);
|
|
}
|
|
fprintf(stderr, "%s: %s: %u bytes\n", TOOL_NAME, argv[a + 1], (unsigned)blob.len);
|
|
toolBufFree(&blob);
|
|
free(lines);
|
|
free(data);
|
|
return 0;
|
|
}
|