From d3b9ba17da552430ffb5a5a766b8489bd4156f39 Mon Sep 17 00:00:00 2001 From: Tharo <17233964+Thar0@users.noreply.github.com> Date: Thu, 15 Aug 2024 01:54:31 +0100 Subject: [PATCH] [Audio 4/?] Build Samplebanks, match Audiotable (#2032) * [Audio 4/?] Build Samplebanks, match Audiotable * Fix some makefile formatting * Add missing scope in MARK chunk handling * Add comment to generate asm file when buffer bug data is emitted, remove duplicate CC4 definition * Adjust comment * SBCFLAGS * Remove unnecessary comments on notes_lut * Split build directories creation command into several to avoid it becoming too long * objcopy -j -> --only-section * Fix mkdir warning when extracted/VERSION/assets doesn't exist --- Makefile | 72 +++- README.md | 3 +- docs/BUILDING_MACOS.md | 3 +- spec | 8 +- tools/audio/.clang-format | 29 ++ tools/audio/.gitignore | 3 + tools/audio/Makefile | 32 +- tools/audio/aifc.c | 552 ++++++++++++++++++++++++++++++ tools/audio/aifc.h | 75 ++++ tools/audio/samplebank.c | 124 +++++++ tools/audio/samplebank.h | 36 ++ tools/audio/samplebank_compiler.c | 203 +++++++++++ tools/audio/util.c | 144 ++++++++ tools/audio/util.h | 72 ++++ tools/audio/xml.c | 393 +++++++++++++++++++++ tools/audio/xml.h | 61 ++++ 16 files changed, 1795 insertions(+), 15 deletions(-) create mode 100644 tools/audio/.clang-format create mode 100644 tools/audio/.gitignore create mode 100644 tools/audio/aifc.c create mode 100644 tools/audio/aifc.h create mode 100644 tools/audio/samplebank.c create mode 100644 tools/audio/samplebank.h create mode 100644 tools/audio/samplebank_compiler.c create mode 100644 tools/audio/util.c create mode 100644 tools/audio/util.h create mode 100644 tools/audio/xml.c create mode 100644 tools/audio/xml.h diff --git a/Makefile b/Makefile index f2ba949a4b..e0026e2a5f 100644 --- a/Makefile +++ b/Makefile @@ -229,13 +229,16 @@ ZAPD := tools/ZAPD/ZAPD.out FADO := tools/fado/fado.elf PYTHON ?= $(VENV)/bin/python3 -# Command to replace path variables in the spec file. We can't use the C -# preprocessor for this because it won't substitute inside string literals. -SPEC_REPLACE_VARS := sed -e 's|$$(BUILD_DIR)|$(BUILD_DIR)|g' +# Command to replace $(BUILD_DIR) in some files with the build path. +# We can't use the C preprocessor for this because it won't substitute inside string literals. +BUILD_DIR_REPLACE := sed -e 's|$$(BUILD_DIR)|$(BUILD_DIR)|g' # Audio tools AUDIO_EXTRACT := $(PYTHON) tools/audio_extraction.py SAMPLECONV := tools/audio/sampleconv/sampleconv +SBC := tools/audio/sbc + +SBCFLAGS := --matching CFLAGS += $(CPP_DEFINES) CPPFLAGS += $(CPP_DEFINES) @@ -303,8 +306,10 @@ endif ifneq ($(wildcard $(EXTRACTED_DIR)/assets/audio),) SAMPLE_EXTRACT_DIRS := $(shell find $(EXTRACTED_DIR)/assets/audio/samples -type d) + SAMPLEBANK_EXTRACT_DIRS := $(shell find $(EXTRACTED_DIR)/assets/audio/samplebanks -type d) else SAMPLE_EXTRACT_DIRS := + SAMPLEBANK_EXTRACT_DIRS := endif ifneq ($(wildcard assets/audio/samples),) @@ -313,10 +318,22 @@ else SAMPLE_DIRS := endif +ifneq ($(wildcard assets/audio/samplebanks),) + SAMPLEBANK_DIRS := $(shell find assets/audio/samplebanks -type d) +else + SAMPLEBANK_DIRS := +endif + SAMPLE_FILES := $(foreach dir,$(SAMPLE_DIRS),$(wildcard $(dir)/*.wav)) SAMPLE_EXTRACT_FILES := $(foreach dir,$(SAMPLE_EXTRACT_DIRS),$(wildcard $(dir)/*.wav)) AIFC_FILES := $(foreach f,$(SAMPLE_FILES),$(BUILD_DIR)/$(f:.wav=.aifc)) $(foreach f,$(SAMPLE_EXTRACT_FILES:.wav=.aifc),$(f:$(EXTRACTED_DIR)/%=$(BUILD_DIR)/%)) +SAMPLEBANK_XMLS := $(foreach dir,$(SAMPLEBANK_DIRS),$(wildcard $(dir)/*.xml)) +SAMPLEBANK_EXTRACT_XMLS := $(foreach dir,$(SAMPLEBANK_EXTRACT_DIRS),$(wildcard $(dir)/*.xml)) +SAMPLEBANK_BUILD_XMLS := $(foreach f,$(SAMPLEBANK_XMLS),$(BUILD_DIR)/$f) $(foreach f,$(SAMPLEBANK_EXTRACT_XMLS),$(f:$(EXTRACTED_DIR)/%=$(BUILD_DIR)/%)) +SAMPLEBANK_O_FILES := $(foreach f,$(SAMPLEBANK_BUILD_XMLS),$(f:.xml=.o)) +SAMPLEBANK_DEP_FILES := $(foreach f,$(SAMPLEBANK_O_FILES),$(f:.o=.d)) + # create extracted directories $(shell mkdir -p $(EXTRACTED_DIR) $(EXTRACTED_DIR)/assets $(EXTRACTED_DIR)/text) @@ -345,7 +362,7 @@ O_FILES := $(foreach f,$(S_FILES:.s=.o),$(BUILD_DIR)/$f) \ $(foreach f,$(ASSET_C_FILES_COMMITTED:.c=.o),$(BUILD_DIR)/$f) \ $(foreach f,$(BASEROM_BIN_FILES),$(BUILD_DIR)/baserom/$(notdir $f).o) -OVL_RELOC_FILES := $(shell $(CPP) $(CPPFLAGS) $(SPEC) | $(SPEC_REPLACE_VARS) | grep -o '[^"]*_reloc.o' ) +OVL_RELOC_FILES := $(shell $(CPP) $(CPPFLAGS) $(SPEC) | $(BUILD_DIR_REPLACE) | grep -o '[^"]*_reloc.o' ) # Automatic dependency files # (Only asm_processor dependencies and reloc dependencies are handled for now) @@ -363,15 +380,21 @@ TEXTURE_FILES_OUT := $(foreach f,$(TEXTURE_FILES_PNG_EXTRACTED:.png=.inc.c),$(f: # create build directories $(shell mkdir -p $(BUILD_DIR)/baserom \ - $(BUILD_DIR)/assets/text \ - $(foreach dir, \ + $(BUILD_DIR)/assets/text) +$(shell mkdir -p $(foreach dir, \ $(SRC_DIRS) \ $(UNDECOMPILED_DATA_DIRS) \ + $(SAMPLE_DIRS) \ + $(SAMPLEBANK_DIRS) \ $(ASSET_BIN_DIRS_COMMITTED), \ - $(BUILD_DIR)/$(dir)) \ - $(foreach dir, \ + $(BUILD_DIR)/$(dir))) +ifneq ($(wildcard $(EXTRACTED_DIR)/assets),) +$(shell mkdir -p $(foreach dir, \ + $(SAMPLE_EXTRACT_DIRS) \ + $(SAMPLEBANK_EXTRACT_DIRS) \ $(ASSET_BIN_DIRS_EXTRACTED), \ $(dir:$(EXTRACTED_DIR)/%=$(BUILD_DIR)/%))) +endif ifeq ($(COMPILER),ido) $(BUILD_DIR)/src/boot/stackcheck.o: OPTFLAGS := -O2 @@ -543,7 +566,8 @@ $(ROMC): $(ROM) $(ELF) $(BUILD_DIR)/compress_ranges.txt $(PYTHON) tools/compress.py --in $(ROM) --out $@ --dmadata-start `./tools/dmadata_start.sh $(NM) $(ELF)` --compress `cat $(BUILD_DIR)/compress_ranges.txt` --threads $(N_THREADS) $(PYTHON) -m ipl3checksum sum --cic 6105 --update $@ -$(ELF): $(TEXTURE_FILES_OUT) $(ASSET_FILES_OUT) $(O_FILES) $(OVL_RELOC_FILES) $(LDSCRIPT) $(BUILD_DIR)/undefined_syms.txt +$(ELF): $(TEXTURE_FILES_OUT) $(ASSET_FILES_OUT) $(O_FILES) $(OVL_RELOC_FILES) $(LDSCRIPT) $(BUILD_DIR)/undefined_syms.txt \ + $(SAMPLEBANK_O_FILES) $(LD) -T $(LDSCRIPT) -T $(BUILD_DIR)/undefined_syms.txt --no-check-sections --accept-unknown-input-arch --emit-relocs -Map $(MAP) -o $@ ## Order-only prerequisites @@ -559,7 +583,7 @@ $(O_FILES): | asset_files .PHONY: o_files asset_files $(BUILD_DIR)/$(SPEC): $(SPEC) - $(CPP) $(CPPFLAGS) $< | $(SPEC_REPLACE_VARS) > $@ + $(CPP) $(CPPFLAGS) $< | $(BUILD_DIR_REPLACE) > $@ $(LDSCRIPT): $(BUILD_DIR)/$(SPEC) $(MKLDSCRIPT) $< $@ @@ -596,7 +620,7 @@ ifneq ($(COMPILER),gcc) else $(CC) -c $(CFLAGS) $(MIPS_VERSION) $(OPTFLAGS) -o $@ $< endif - $(OBJCOPY) -O binary -j.rodata $@ $@.bin + $(OBJCOPY) -O binary --only-section .rodata $@ $@.bin $(BUILD_DIR)/assets/%.o: assets/%.c $(CC) -c $(CFLAGS) $(MIPS_VERSION) $(OPTFLAGS) -o $@ $< @@ -677,6 +701,9 @@ AUDIO_BUILD_DEBUG ?= 0 # first build samples... +.PRECIOUS: $(BUILD_DIR)/assets/audio/samples/%.aifc +.PRECIOUS: $(BUILD_DIR)/assets/audio/samples/%.half.aifc + $(BUILD_DIR)/assets/audio/samples/%.half.aifc: assets/audio/samples/%.half.wav $(SAMPLECONV) vadpcm-half $< $@ @@ -695,6 +722,29 @@ ifeq ($(AUDIO_BUILD_DEBUG),1) @(cmp $( $@ + +$(BUILD_DIR)/assets/audio/samplebanks/%.xml: $(EXTRACTED_DIR)/assets/audio/samplebanks/%.xml + cat $< | $(BUILD_DIR_REPLACE) > $@ + +.PRECIOUS: $(BUILD_DIR)/assets/audio/samplebanks/%.s +$(BUILD_DIR)/assets/audio/samplebanks/%.s: $(BUILD_DIR)/assets/audio/samplebanks/%.xml | $(AIFC_FILES) + $(SBC) $(SBCFLAGS) --makedepend $(@:.s=.d) $< $@ + +-include $(SAMPLEBANK_DEP_FILES) + +$(BUILD_DIR)/assets/audio/samplebanks/%.o: $(BUILD_DIR)/assets/audio/samplebanks/%.s + $(AS) $(ASFLAGS) $< -o $@ +ifeq ($(AUDIO_BUILD_DEBUG),1) + $(OBJCOPY) -O binary --only-section .rodata $@ $(@:.o=.bin) + @cmp $(@:.o=.bin) $(patsubst $(BUILD_DIR)/assets/audio/samplebanks/%,$(EXTRACTED_DIR)/baserom_audiotest/audiotable_files/%,$(@:.o=.bin)) && echo "$( +#include +#include +#include +#include +#include + +#include "aifc.h" +#include "util.h" + +#define CC4_CHECK(buf, str) \ + ((buf)[0] == (str)[0] && (buf)[1] == (str)[1] && (buf)[2] == (str)[2] && (buf)[3] == (str)[3]) + +#define CC4(c1, c2, c3, c4) (((c1) << 24) | ((c2) << 16) | ((c3) << 8) | (c4)) + +#define FREAD(file, data, size) \ + do { \ + if (fread((data), (size), 1, (file)) != 1) { \ + error("[%s:%d] Could not read %lu bytes from file", __FILE__, __LINE__, (size_t)(size)); \ + } \ + } while (0) + +#define VADPCM_VER ((int16_t)1) + +#if 0 +#define DEBUGF(fmt, ...) printf(fmt, ##__VA_ARGS__) +#else +#define DEBUGF(fmt, ...) (void)0 +#endif + +typedef struct { + int16_t numChannels; + uint16_t numFramesH; + uint16_t numFramesL; + int16_t sampleSize; + uint8_t sampleRate[10]; // 80-bit float + // followed by compression type + compression name pstring +} aiff_COMM; + +typedef struct { + uint16_t nMarkers; +} aiff_MARK; + +typedef struct { + uint16_t MarkerID; + uint16_t positionH; + uint16_t positionL; +} Marker; + +typedef enum { + LOOP_PLAYMODE_NONE = 0, + LOOP_PLAYMODE_FWD = 1, + LOOP_PLAYMODE_FWD_BWD = 2 +} aiff_loop_playmode; + +typedef struct { + int16_t playMode; // aiff_loop_playmode + // Marker IDs + int16_t beginLoop; + int16_t endLoop; +} Loop; + +typedef struct { + int8_t baseNote; + int8_t detune; + int8_t lowNote; + int8_t highNote; + int8_t lowVelocity; + int8_t highVelocity; + int16_t gain; + Loop sustainLoop; + Loop releaseLoop; +} aiff_INST; + +typedef struct { + int32_t offset; + int32_t blockSize; +} aiff_SSND; + +static_assert(sizeof(double) == sizeof(uint64_t), "Double is assumed to be 64-bit"); + +#define F64_GET_SGN(bits) (((bits) >> 63) & 1) // 1-bit +#define F64_GET_EXP(bits) ((((bits) >> 52) & 0x7FF) - 0x3FF) // 15-bit +#define F64_GET_MANT_H(bits) (((bits) >> 32) & 0xFFFFF) // 20-bit +#define F64_GET_MANT_L(bits) ((bits)&0xFFFFFFFF) // 32-bit + +static UNUSED void +f64_to_f80(double f64, uint8_t *f80) +{ + union { + uint32_t w[3]; + uint8_t b[12]; + } f80tmp; + + // get f64 bits + + uint64_t f64_bits = *(uint64_t *)&f64; + + int f64_sgn = F64_GET_SGN(f64_bits); + int f64_exponent = F64_GET_EXP(f64_bits); + uint32_t f64_mantissa_hi = F64_GET_MANT_H(f64_bits); + uint32_t f64_mantissa_lo = F64_GET_MANT_L(f64_bits); + + // build f80 words + + f80tmp.w[0] = (f64_sgn << 15) | (f64_exponent + 0x3FFF); + f80tmp.w[1] = (1 << 31) | (f64_mantissa_hi << 11) | (f64_mantissa_lo >> 21); + f80tmp.w[2] = f64_mantissa_lo << 11; + + // byteswap to BE + + f80tmp.w[0] = htobe32(f80tmp.w[0]); + f80tmp.w[1] = htobe32(f80tmp.w[1]); + f80tmp.w[2] = htobe32(f80tmp.w[2]); + + // write bytes + + for (size_t i = 0; i < 10; i++) + f80[i] = f80tmp.b[i + 2]; +} + +static void +f80_to_f64(double *f64, uint8_t *f80) +{ + union { + uint32_t w[3]; + uint8_t b[12]; + } f80tmp; + + // read bytes + + f80tmp.b[0] = f80tmp.b[1] = 0; + for (size_t i = 0; i < 10; i++) + f80tmp.b[i + 2] = f80[i]; + + // byteswap from BE + + f80tmp.w[0] = be32toh(f80tmp.w[0]); + f80tmp.w[1] = be32toh(f80tmp.w[1]); + f80tmp.w[2] = be32toh(f80tmp.w[2]); + + // get f64 parts + + int f64_sgn = (f80tmp.w[0] >> 15) & 1; + int f64_exponent = (f80tmp.w[0] & 0x7FFF) - 0x3FFF; + uint32_t f64_mantissa_hi = (f80tmp.w[1] >> 11) & 0xFFFFF; + uint32_t f64_mantissa_lo = ((f80tmp.w[1] & 0x7FF) << 21) | (f80tmp.w[2] >> 11); + + // build bitwise f64 + + uint64_t f64_bits = ((uint64_t)f64_sgn << 63) | ((((uint64_t)f64_exponent + 0x3FF) & 0x7FF) << 52) | + ((uint64_t)f64_mantissa_hi << 32) | ((uint64_t)f64_mantissa_lo); + + // write double + + *f64 = *(double *)&f64_bits; +} + +static void +read_pstring(FILE *f, char *out) +{ + unsigned char len; + + // read string length + FREAD(f, &len, sizeof(len)); + + // read string and null-terminate it + FREAD(f, out, len); + out[len] = '\0'; + + // pad to 2-byte boundary + if (!(len & 1)) + FREAD(f, &len, 1); +} + +static char * +read_pstring_alloc(FILE *f) +{ + unsigned char len; + + // read string length + FREAD(f, &len, sizeof(len)); + + // alloc + char *out = malloc(len + 1); + + // read string and null-terminate it + FREAD(f, out, len); + out[len] = '\0'; + + // pad to 2-byte boundary + if (!(len & 1)) + FREAD(f, &len, 1); + + return out; +} + +void +aifc_read(aifc_data *af, const char *path, uint8_t *match_buf, size_t *match_buf_pos) +{ + FILE *in; + bool has_comm = false; + bool has_ssnd = false; + + memset(af, 0, sizeof(aifc_data)); + + DEBUGF("[aifc] path [%s]\n", path); + + if (path == NULL) + return; + + in = fopen(path, "rb"); + if (in == NULL) + error("Failed to open \"%s\" for reading", path); + + char form[4]; + uint32_t size; + char aifc[4]; + + FREAD(in, form, 4); + FREAD(in, &size, 4); + size = be32toh(size); + FREAD(in, aifc, 4); + + DEBUGF("total size = 0x%X\n", size); + + if (!CC4_CHECK(form, "FORM") || !CC4_CHECK(aifc, "AIFC")) + error("Not an aifc file?"); + + af->path = path; + + while (true) { + char cc4[4]; + uint32_t chunk_size; + + long start = ftell(in); + if (start > 8 + size) { + error("Overran file"); + } + if (start == 8 + size) { + break; + } + + FREAD(in, cc4, 4); + FREAD(in, &chunk_size, 4); + chunk_size = be32toh(chunk_size); + + chunk_size++; + chunk_size &= ~1; + + DEBUGF("%c%c%c%c\n", cc4[0], cc4[1], cc4[2], cc4[3]); + + switch (CC4(cc4[0], cc4[1], cc4[2], cc4[3])) { + case CC4('C', 'O', 'M', 'M'): { + aiff_COMM comm; + FREAD(in, &comm, sizeof(comm)); + comm.numChannels = be16toh(comm.numChannels); + comm.numFramesH = be16toh(comm.numFramesH); + comm.numFramesL = be16toh(comm.numFramesL); + comm.sampleSize = be16toh(comm.sampleSize); + + assert(comm.numChannels == 1); // mono + assert(comm.sampleSize == 16); // 16-bit samples + + af->num_channels = comm.numChannels; + af->sample_size = comm.sampleSize; + af->num_frames = (comm.numFramesH << 16) | comm.numFramesL; + f80_to_f64(&af->sample_rate, comm.sampleRate); + + uint32_t comp_type = CC4('N', 'O', 'N', 'E'); + if (chunk_size > sizeof(aiff_COMM)) { + uint32_t compressionType; + FREAD(in, &compressionType, sizeof(compressionType)); + comp_type = be32toh(compressionType); + } + af->compression_type = comp_type; + + af->compression_name = NULL; + if (chunk_size > sizeof(aiff_COMM) + 4) { + af->compression_name = read_pstring_alloc(in); + } + + DEBUGF(" numChannels %d\n" + " numFrames %u\n" + " sampleSize %d\n" + " sampleRate %f\n" + " compressionType %c%c%c%c (%s)\n", + af->num_channels, af->num_frames, af->sample_size, af->sample_rate, af->compression_type >> 24, + af->compression_type >> 16, af->compression_type >> 8, af->compression_type, + af->compression_name); + + has_comm = true; + } break; + + case CC4('I', 'N', 'S', 'T'): { + aiff_INST inst; + FREAD(in, &inst, sizeof(inst)); + inst.gain = be16toh(inst.gain); + inst.sustainLoop.playMode = be16toh(inst.sustainLoop.playMode); + inst.sustainLoop.beginLoop = be16toh(inst.sustainLoop.beginLoop); + inst.sustainLoop.endLoop = be16toh(inst.sustainLoop.endLoop); + inst.releaseLoop.playMode = be16toh(inst.releaseLoop.playMode); + inst.releaseLoop.beginLoop = be16toh(inst.releaseLoop.beginLoop); + inst.releaseLoop.endLoop = be16toh(inst.releaseLoop.endLoop); + + // basenote + + DEBUGF(" baseNote = %d (%d)\n" + " detune = %d\n" + " lowNote = %d\n" + " highNote = %d\n" + " lowVelocity = %d\n" + " highVelocity= %d\n" + " gain = %d\n" + " sustainLoop = %d [%d:%d]\n" + " releaseLoop = %d [%d:%d]\n", + inst.baseNote, NOTE_MIDI_TO_Z64(inst.baseNote), inst.detune, inst.lowNote, inst.highNote, + inst.lowVelocity, inst.highVelocity, inst.gain, inst.sustainLoop.playMode, + inst.sustainLoop.beginLoop, inst.sustainLoop.endLoop, inst.releaseLoop.playMode, + inst.releaseLoop.beginLoop, inst.releaseLoop.endLoop); + + af->basenote = inst.baseNote; + af->detune = inst.detune; + af->has_inst = true; + } break; + + case CC4('M', 'A', 'R', 'K'): { + aiff_MARK mark; + FREAD(in, &mark, sizeof(mark)); + mark.nMarkers = be16toh(mark.nMarkers); + + af->num_markers = mark.nMarkers; + af->markers = malloc(mark.nMarkers * sizeof(aifc_marker)); + + for (size_t i = 0; i < mark.nMarkers; i++) { + Marker marker; + FREAD(in, &marker, sizeof(marker)); + marker.MarkerID = be16toh(marker.MarkerID); + marker.positionH = be16toh(marker.positionH); + marker.positionL = be16toh(marker.positionL); + + (*af->markers)[i].id = marker.MarkerID; + (*af->markers)[i].pos = (marker.positionH << 16) | marker.positionL; + (*af->markers)[i].label = read_pstring_alloc(in); + + DEBUGF(" MARKER: %d @ %u [%s]\n", (*af->markers)[i].id, (*af->markers)[i].pos, + (*af->markers)[i].label); + } + } break; + + case CC4('A', 'P', 'P', 'L'): { + char subcc4[4]; + + FREAD(in, subcc4, 4); + + DEBUGF(" %c%c%c%c\n", subcc4[0], subcc4[1], subcc4[2], subcc4[3]); + + switch (CC4(subcc4[0], subcc4[1], subcc4[2], subcc4[3])) { + case CC4('s', 't', 'o', 'c'): { + char chunk_name[257]; + read_pstring(in, chunk_name); + + DEBUGF(" %s\n", chunk_name); + + if (strequ(chunk_name, "VADPCMCODES")) { + int16_t version; + uint16_t order; + uint16_t npredictors; + + FREAD(in, &version, sizeof(version)); + version = be16toh(version); + FREAD(in, &order, sizeof(order)); + order = be16toh(order); + FREAD(in, &npredictors, sizeof(npredictors)); + npredictors = be16toh(npredictors); + + if (version != VADPCM_VER) + error("Non-identical codebook chunk versions"); + + size_t book_size = 8 * order * npredictors; + + af->book.order = order; + af->book.npredictors = npredictors; + af->book_state = malloc(book_size * sizeof(int16_t)); + FREAD(in, af->book_state, book_size * sizeof(int16_t)); + + for (size_t i = 0; i < book_size; i++) + (*af->book_state)[i] = be16toh((*af->book_state)[i]); + + af->has_book = true; + + // DEBUG + + DEBUGF(" order = %d\n" + " npredictors = %d\n", + af->book.order, af->book.npredictors); + + for (size_t i = 0; i < book_size; i++) { + if (i % 8 == 0) + DEBUGF("\n "); + DEBUGF("%04X ", (uint16_t)(*af->book_state)[i]); + } + DEBUGF("\n"); + } else if (strequ(chunk_name, "VADPCMLOOPS")) { + int16_t version; + int16_t nloops; + + FREAD(in, &version, sizeof(version)); + version = be16toh(version); + FREAD(in, &nloops, sizeof(nloops)); + nloops = be16toh(nloops); + + if (version != VADPCM_VER) + error("Non-identical loop chunk versions"); + + if (nloops != 1) + error("Only one loop is supported, got %d", nloops); + + FREAD(in, &af->loop, sizeof(ALADPCMloop)); + af->loop.start = be32toh(af->loop.start); + af->loop.end = be32toh(af->loop.end); + af->loop.count = be32toh(af->loop.count); + for (size_t i = 0; i < ARRAY_COUNT(af->loop.state); i++) + af->loop.state[i] = be16toh(af->loop.state[i]); + + af->has_loop = true; + + // DEBUG + + DEBUGF(" start = %d\n" + " end = %d\n" + " count = %d\n", + af->loop.start, af->loop.end, af->loop.count); + + for (size_t i = 0; i < ARRAY_COUNT(af->loop.state); i++) { + if (i % 8 == 0) + DEBUGF("\n "); + DEBUGF("%04X ", (uint16_t)af->loop.state[i]); + } + DEBUGF("\n"); + } else { + warning("Skipping unknown APPL::stoc subchunk: \"%s\"", chunk_name); + } + } break; + + default: + warning("Skipping unknown APPL subchunk: \"%c%c%c%c\"", subcc4[0], subcc4[1], subcc4[2], + subcc4[3]); + break; + } + } break; + + case CC4('S', 'S', 'N', 'D'): { + aiff_SSND ssnd; + FREAD(in, &ssnd, sizeof(ssnd)); + ssnd.offset = be32toh(ssnd.offset); + ssnd.blockSize = be32toh(ssnd.blockSize); + + assert(ssnd.offset == 0); + assert(ssnd.blockSize == 0); + + af->ssnd_offset = ftell(in); + // TODO use numFrames instead? + af->ssnd_size = chunk_size - sizeof(ssnd); + + // Skip reading the rest of the chunk + fseek(in, af->ssnd_size, SEEK_CUR); + + DEBUGF(" offset = 0x%lX size = 0x%lX\n", af->ssnd_offset, af->ssnd_size); + + has_ssnd = true; + } break; + + default: // skip it + break; + } + + long read_size = ftell(in) - start - 8; + + if (read_size > chunk_size) + error("overran chunk: %lu vs %u\n", read_size, chunk_size); + else if (read_size < chunk_size) + warning("did not read entire %.*s chunk: %lu vs %u", 4, cc4, read_size, chunk_size); + + fseek(in, start + 8 + chunk_size, SEEK_SET); + } + + if (!has_comm) + error("aiff/aifc has no COMM chunk"); + if (!has_ssnd) + error("aiff/aifc has no SSND chunk"); + + // replicate buffer bug in original tool + if (match_buf != NULL && match_buf_pos != NULL) { + size_t buf_pos = ALIGN16(*match_buf_pos) % BUG_BUF_SIZE; + size_t rem = af->ssnd_size; + long seek_offset = 0; + + if (rem > BUG_BUF_SIZE) { + // The sample is so large that it will cover the buffer more than once, let's only read as much as we + // need to. + + // Advance to the buffer position to read only the final data into + buf_pos = (buf_pos + rem - BUG_BUF_SIZE) % BUG_BUF_SIZE; + // We need to seek to the actual data in the file that would be read at this point + seek_offset = rem - BUG_BUF_SIZE; + // The remaining data to read is just 1 buffer's worth of data + rem = BUG_BUF_SIZE; + } + + fseek(in, af->ssnd_offset + seek_offset, SEEK_SET); + + if (rem > BUG_BUF_SIZE - buf_pos) { + // rem will circle around in the buffer + + // Fill up to the end of the buffer + FREAD(in, &match_buf[buf_pos], BUG_BUF_SIZE - buf_pos); + rem -= BUG_BUF_SIZE - buf_pos; + // Return to the start of the buffer + buf_pos = 0; + } + // rem fits in the buffer without circling back, fill buffer + FREAD(in, &match_buf[buf_pos], rem); + + *match_buf_pos = (buf_pos + rem) % BUG_BUF_SIZE; + } + + fclose(in); +} + +void +aifc_dispose(aifc_data *af) +{ + free(af->book_state); + af->has_book = false; + + af->has_loop = false; + + free(af->compression_name); + + for (size_t i = 0; i < af->num_markers; i++) + free((*af->markers)[i].label); + free(af->markers); +} diff --git a/tools/audio/aifc.h b/tools/audio/aifc.h new file mode 100644 index 0000000000..3e9920fd09 --- /dev/null +++ b/tools/audio/aifc.h @@ -0,0 +1,75 @@ +/** + * SPDX-FileCopyrightText: Copyright (C) 2024 ZeldaRET + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +#ifndef AIFC_H_ +#define AIFC_H_ + +#include +#include +#include + +typedef struct { + int32_t order; + int32_t npredictors; +} ALADPCMbookhead; + +typedef int16_t ALADPCMbookstate[]; + +typedef struct { + uint32_t start; + uint32_t end; + uint32_t count; + int16_t state[16]; +} ALADPCMloop; + +typedef struct { + int16_t id; + uint32_t pos; + char *label; +} aifc_marker; + +typedef struct { + const char *path; // for debugging + // COMM + uint32_t num_frames; + int16_t num_channels; + int16_t sample_size; + double sample_rate; + uint32_t compression_type; + char *compression_name; + // SSND + long ssnd_offset; + size_t ssnd_size; + // INST + bool has_inst; + int8_t basenote; + int8_t detune; + // MARK + size_t num_markers; + aifc_marker (*markers)[]; + // APPL::stoc::VADPCMCODES + bool has_book; + ALADPCMbookhead book; + ALADPCMbookstate *book_state; + // APPL::stoc::VADPCMLOOPS + bool has_loop; + ALADPCMloop loop; +} aifc_data; + +#define BUG_BUF_SIZE 0x10000 + +void +aifc_read(aifc_data *af, const char *path, uint8_t *match_buf, size_t *match_buf_pos); + +void +aifc_dispose(aifc_data *af); + +// Subtract 21, if negative wrap into [0, 128) +#define NOTE_MIDI_TO_Z64(b) (((b)-21 < 0) ? ((b)-21 + 128) : ((b)-21)) + +#endif diff --git a/tools/audio/samplebank.c b/tools/audio/samplebank.c new file mode 100644 index 0000000000..8eda227f27 --- /dev/null +++ b/tools/audio/samplebank.c @@ -0,0 +1,124 @@ +/** + * SPDX-FileCopyrightText: Copyright (C) 2024 ZeldaRET + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +#include + +#include "xml.h" +#include "samplebank.h" +#include "util.h" + +const char * +samplebank_path_forname(samplebank *sb, const char *name) +{ + assert(name != NULL); + + for (size_t i = 0; i < sb->num_samples; i++) { + if (strequ(sb->sample_names[i], name)) + return sb->sample_paths[i]; + } + return NULL; +} + +typedef struct { + const char *name; + const char *path; +} samplebank_xml_entry; + +void +read_samplebank_xml(samplebank *sb, xmlDocPtr doc) +{ + // XML Example: + // + // + // + // + + static const xml_attr_spec header_spec = { + {"Name", false, xml_parse_c_identifier, offsetof(samplebank, name) }, + { "Index", false, xml_parse_int, offsetof(samplebank, index) }, + { "Medium", false, xml_parse_c_identifier, offsetof(samplebank, medium) }, + { "CachePolicy", false, xml_parse_c_identifier, offsetof(samplebank, cache_policy)}, + { "BufferBug", true, xml_parse_bool, offsetof(samplebank, buffer_bug) }, + }; + static const xml_attr_spec entry_spec = { + {"Name", false, xml_parse_c_identifier, offsetof(samplebank_xml_entry, name)}, + { "Path", false, xml_parse_string, offsetof(samplebank_xml_entry, path)}, + }; + + xmlNodePtr root = xmlDocGetRootElement(doc); + + if (!strequ(XMLSTR_TO_STR(root->name), "SampleBank")) + error("Root node must be "); + + sb->buffer_bug = false; + xml_parse_node_by_spec(sb, root, header_spec, ARRAY_COUNT(header_spec)); + + if (root->children == NULL) + error("Missing samples list"); + + size_t entries_cap = 8; + size_t entries_len = 0; + sb->sample_names = malloc(entries_cap * sizeof(const char *)); + sb->sample_paths = malloc(entries_cap * sizeof(const char *)); + sb->is_sample = malloc(entries_cap * sizeof(bool)); + + size_t pointers_cap = 4; + size_t pointers_len = 0; + sb->pointer_indices = malloc(pointers_cap * sizeof(int)); + + LL_FOREACH(xmlNodePtr, node, root->children) { + if (node->type != XML_ELEMENT_NODE) + continue; + + if (node->children != NULL) { + xmlNodePtr first_child = node->children; + const char *child_name = XMLSTR_TO_STR(first_child->name); + error("Unexpected child node(s) (first is %s) in samples list (line %d)", child_name, first_child->line); + } + + const char *node_name = XMLSTR_TO_STR(node->name); + bool is_sample; + + if (strequ(node_name, "Sample")) { + is_sample = true; + } else if (strequ(node_name, "Blob")) { + is_sample = false; + } else if (strequ(node_name, "Pointer")) { + // pointer entry + int ptr_index; + xml_get_single_property(&ptr_index, node, "Index", xml_parse_int); + + if (pointers_len == pointers_cap) { + pointers_cap *= 2; + sb->pointer_indices = realloc(sb->pointer_indices, pointers_cap * sizeof(int)); + } + sb->pointer_indices[pointers_len++] = ptr_index; + continue; + } else { + error("Unexpected element node %s in samples list (line %d)", node_name, node->line); + } + + samplebank_xml_entry ent; + xml_parse_node_by_spec(&ent, node, entry_spec, ARRAY_COUNT(entry_spec)); + + if (entries_len == entries_cap) { + entries_cap *= 2; + sb->sample_names = realloc(sb->sample_names, entries_cap * sizeof(const char *)); + sb->sample_paths = realloc(sb->sample_paths, entries_cap * sizeof(const char *)); + sb->is_sample = realloc(sb->is_sample, entries_cap * sizeof(bool)); + } + + sb->sample_names[entries_len] = ent.name; + sb->sample_paths[entries_len] = ent.path; + sb->is_sample[entries_len] = is_sample; + entries_len++; + } + + sb->num_samples = entries_len; + sb->num_pointers = pointers_len; +} diff --git a/tools/audio/samplebank.h b/tools/audio/samplebank.h new file mode 100644 index 0000000000..c436253fc6 --- /dev/null +++ b/tools/audio/samplebank.h @@ -0,0 +1,36 @@ +/** + * SPDX-FileCopyrightText: Copyright (C) 2024 ZeldaRET + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +#ifndef SAMPLEBANK_H_ +#define SAMPLEBANK_H_ + +#include "xml.h" + +typedef struct { + const char *name; + int index; + const char *medium; + const char *cache_policy; + bool buffer_bug; + + size_t num_samples; + const char **sample_paths; + const char **sample_names; + bool *is_sample; + + size_t num_pointers; + int *pointer_indices; +} samplebank; + +const char * +samplebank_path_forname(samplebank *sb, const char *name); + +void +read_samplebank_xml(samplebank *sb, xmlDocPtr doc); + +#endif diff --git a/tools/audio/samplebank_compiler.c b/tools/audio/samplebank_compiler.c new file mode 100644 index 0000000000..c53515dc1f --- /dev/null +++ b/tools/audio/samplebank_compiler.c @@ -0,0 +1,203 @@ +/** + * SPDX-FileCopyrightText: Copyright (C) 2024 ZeldaRET + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +#include +#include +#include + +#include "xml.h" +#include "aifc.h" +#include "samplebank.h" +#include "util.h" + +NORETURN static void +usage(const char *progname) +{ + fprintf(stderr, "Usage: %s [--matching] [--makedepend ] \n", progname); + exit(EXIT_FAILURE); +} + +int +main(int argc, char **argv) +{ + static uint8_t match_buf[BUG_BUF_SIZE]; + const char *filename = NULL; + xmlDocPtr document; + const char *outfilename = NULL; + const char *mdfilename = NULL; + FILE *mdfile; + FILE *outf; + samplebank sb; + uint8_t *match_buf_ptr; + size_t match_buf_pos; + bool matching = false; + + // parse args + +#define arg_error(fmt, ...) \ + do { \ + fprintf(stderr, fmt "\n", ##__VA_ARGS__); \ + usage(argv[0]); \ + } while (0) + + int argn = 0; + for (int i = 1; i < argc; i++) { + if (argv[i][0] == '-') { + // Optional args + + if (strequ(argv[i], "--matching")) { + if (matching) + arg_error("Received --matching option twice"); + + matching = true; + continue; + } + if (strequ(argv[i], "--makedepend")) { + if (mdfilename != NULL) + arg_error("Received --makedepend option twice"); + if (i + 1 == argc) + arg_error("--makedepend missing required argument"); + + mdfilename = argv[++i]; + continue; + } + arg_error("Unknown option \"%s\"", argv[i]); + } else { + // Required args + + switch (argn) { + case 0: + filename = argv[i]; + break; + case 1: + outfilename = argv[i]; + break; + default: + arg_error("Unknown positional argument \"%s\"", argv[i]); + break; + } + argn++; + } + } + if (argn != 2) + arg_error("Not enough positional arguments"); + +#undef arg_error + + // open xml + document = xmlReadFile(filename, NULL, XML_PARSE_NONET); + if (document == NULL) + return EXIT_FAILURE; + + // parse xml + read_samplebank_xml(&sb, document); + + // open output asm file + outf = fopen(outfilename, "w"); + if (outf == NULL) + error("Unable to open output file [%s] for writing", outfilename); + + // open output dep file if applicable + if (mdfilename != NULL) { + mdfile = fopen(mdfilename, "w"); + if (mdfile == NULL) + error("Unable to open dependency file [%s] for writing", mdfilename); + + fprintf(mdfile, "%s: \\\n %s", outfilename, filename); + } + + // write output + + fprintf(outf, + // clang-format off + ".rdata" "\n" + ".balign 16" "\n" + "\n" + ".global %s_Start" "\n" + "%s_Start:" "\n" + "$start:" "\n", + // clang-format on + sb.name, sb.name); + + // original tool appears to have a buffer clearing bug involving a buffer sized BUG_BUF_SIZE + match_buf_ptr = (matching) ? match_buf : NULL; + match_buf_pos = 0; + + for (size_t i = 0; i < sb.num_samples; i++) { + const char *name = sb.sample_names[i]; + const char *path = sb.sample_paths[i]; + bool is_sample = sb.is_sample[i]; + + if (mdfilename != NULL) + fprintf(mdfile, " \\\n %s", path); + + if (!is_sample) { + // blob + fprintf(outf, + // clang-format off + "\n" + "# BLOB %s" "\n" + "\n" + ".incbin \"%s\"" "\n" + "\n" + ".balign 16" "\n" + "\n", + // clang-format on + name, path); + continue; + } + + // aifc sample + fprintf(outf, + // clang-format off + "\n" + "# SAMPLE %lu" "\n" + "\n" + ".global %s_%s_Abs" "\n" + "%s_%s_Abs:" "\n" + ".global %s_%s_Off" "\n" + ".set %s_%s_Off, . - $start" "\n" + "\n", + // clang-format on + i, sb.name, name, sb.name, name, sb.name, name, sb.name, name); + + aifc_data aifc; + aifc_read(&aifc, path, match_buf_ptr, &match_buf_pos); + + fprintf(outf, ".incbin \"%s\", 0x%lX, 0x%lX\n", path, aifc.ssnd_offset, aifc.ssnd_size); + + if (matching && sb.buffer_bug && i == sb.num_samples - 1) { + // emplace garbage + size_t end = ALIGN16(match_buf_pos); + + fprintf(outf, "\n# Garbage data from buffer bug\n"); + for (; match_buf_pos < end; match_buf_pos++) + fprintf(outf, ".byte 0x%02X\n", match_buf[match_buf_pos]); + } else { + fputs("\n.balign 16\n", outf); + } + + aifc_dispose(&aifc); + } + + if (mdfilename != NULL) { + fputs("\n", mdfile); + fclose(mdfile); + } + + fprintf(outf, + // clang-format off + ".global %s_Size" "\n" + ".set %s_Size, . - $start" "\n", + // clang-format on + sb.name, sb.name); + + fclose(outf); + xmlFreeDoc(document); + return EXIT_SUCCESS; +} diff --git a/tools/audio/util.c b/tools/audio/util.c new file mode 100644 index 0000000000..8043f95775 --- /dev/null +++ b/tools/audio/util.c @@ -0,0 +1,144 @@ +/* SPDX-FileCopyrightText: Copyright (C) 2024 ZeldaRET */ +/* SPDX-License-Identifier: CC0-1.0 */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include + +#include "util.h" + +// TODO ideally we should be collecting all errors and displaying them all before exiting + +NORETURN void +error(const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + fprintf(stderr, "\x1b[91m" + "Error: " + "\x1b[97m"); + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\x1b[0m" + "\n"); + va_end(ap); + + exit(EXIT_FAILURE); +} + +void +warning(const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + fprintf(stderr, "\x1b[95m" + "Warning: " + "\x1b[97m"); + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\x1b[0m" + "\n"); + va_end(ap); +} + +void * +util_read_whole_file(const char *filename, size_t *size_out) +{ + FILE *file = fopen(filename, "rb"); + void *buffer = NULL; + size_t size; + + if (file == NULL) + error("failed to open file '%s' for reading: %s", filename, strerror(errno)); + + // get size + fseek(file, 0, SEEK_END); + size = ftell(file); + + // if the file is empty, return NULL buffer and 0 size + if (size != 0) { + // allocate buffer + buffer = malloc(size + 1); + if (buffer == NULL) + error("could not allocate buffer for file '%s'", filename); + + // read file + fseek(file, 0, SEEK_SET); + if (fread(buffer, size, 1, file) != 1) + error("error reading from file '%s': %s", filename, strerror(errno)); + + // null-terminate the buffer (in case of text files) + ((char *)buffer)[size] = '\0'; + } + + fclose(file); + + if (size_out != NULL) + *size_out = size; + return buffer; +} + +void +util_write_whole_file(const char *filename, const void *data, size_t size) +{ + FILE *file = fopen(filename, "wb"); + + if (file == NULL) + error("failed to open file '%s' for writing: %s", filename, strerror(errno)); + + if (fwrite(data, size, 1, file) != 1) + error("error writing to file '%s': %s", filename, strerror(errno)); + + fclose(file); +} + +bool +str_is_c_identifier(const char *str) +{ + // A C language identifier must: + // - ONLY contain [_, abc..xyz, ABC..XYZ, 0..9] (we do not support unicode or extensions like $) + // - NOT be a keyword + // - NOT start with a digit [0..9] + + static const char *const c_kwds[] = { + "auto", "break", "case", "char", "const", "continue", "default", "do", + "double", "else", "enum", "extern", "float", "for", "goto", "if", + "inline", "int", "long", "register", "restrict", "return", "short", "signed", + "sizeof", "static", "struct", "switch", "typedef", "union", "unsigned", "void", + "volatile", "while", + + "_Alignas", "_Alignof", "_Atomic", "_Bool", "_Complex", "_Generic", "_Imaginary", "_Noreturn", + "_Static_assert", "_Thread_local", + }; + + if (str == NULL) { + return false; + } + if (isdigit(str[0])) { + // Starts with a digit, fail + return false; + } + + size_t len = strlen(str); + for (size_t i = 0; i < len; i++) { + char c = str[i]; + + bool alpha = isalpha(c); + bool digit = isdigit(c); + bool uscore = c == '_'; + + if (!(alpha || digit || uscore)) { + // Contains bad character, fail + return false; + } + } + + for (size_t i = 0; i < ARRAY_COUNT(c_kwds); i++) { + if (strequ(str, c_kwds[i])) { + // Matched a C keyword, fail + return false; + } + } + return true; +} diff --git a/tools/audio/util.h b/tools/audio/util.h new file mode 100644 index 0000000000..69a877f602 --- /dev/null +++ b/tools/audio/util.h @@ -0,0 +1,72 @@ +/* SPDX-FileCopyrightText: Copyright (C) 2024 ZeldaRET */ +/* SPDX-License-Identifier: CC0-1.0 */ +#ifndef UTIL_H_ +#define UTIL_H_ + +#include +#include +#include + +// Endian + +#if defined(__linux__) || defined(__CYGWIN__) +#include +#elif defined(__APPLE__) +#include + +#define htobe16(x) OSSwapHostToBigInt16(x) +#define htole16(x) OSSwapHostToLittleInt16(x) +#define be16toh(x) OSSwapBigToHostInt16(x) +#define le16toh(x) OSSwapLittleToHostInt16(x) + +#define htobe32(x) OSSwapHostToBigInt32(x) +#define htole32(x) OSSwapHostToLittleInt32(x) +#define be32toh(x) OSSwapBigToHostInt32(x) +#define le32toh(x) OSSwapLittleToHostInt32(x) + +#define htobe64(x) OSSwapHostToBigInt64(x) +#define htole64(x) OSSwapHostToLittleInt64(x) +#define be64toh(x) OSSwapBigToHostInt64(x) +#define le64toh(x) OSSwapLittleToHostInt64(x) +#else +#error "Endian conversion unsupported, add it" +#endif + +// Attribute macros + +#define ALWAYS_INLINE inline __attribute__((always_inline)) + +#define NORETURN __attribute__((noreturn)) +#define UNUSED __attribute__((unused)) + +// Helper macros + +#define strequ(s1, s2) ((__builtin_constant_p(s2) ? strncmp(s1, s2, sizeof(s2) - 1) : strcmp(s1, s2)) == 0) + +#define str_endswith(str, len, endswith) \ + ((len) > (sizeof(endswith) - 1) && strequ(&(str)[(len) - sizeof(endswith) + 1], (endswith))) + +#define LL_FOREACH(type, x, base) for (type(x) = (base); (x) != NULL; (x) = (x)->next) + +#define ARRAY_COUNT(arr) (sizeof(arr) / sizeof((arr)[0])) + +#define ALIGN16(x) (((x) + 0xF) & ~0xF) + +#define BOOL_STR(b) ((b) ? "true" : "false") + +// util.c functions + +__attribute__((format(printf, 1, 2))) NORETURN void +error(const char *fmt, ...); +__attribute__((format(printf, 1, 2))) void +warning(const char *fmt, ...); + +void * +util_read_whole_file(const char *filename, size_t *size_out); +void +util_write_whole_file(const char *filename, const void *data, size_t size); + +bool +str_is_c_identifier(const char *str); + +#endif diff --git a/tools/audio/xml.c b/tools/audio/xml.c new file mode 100644 index 0000000000..3df2c60a80 --- /dev/null +++ b/tools/audio/xml.c @@ -0,0 +1,393 @@ +/** + * SPDX-FileCopyrightText: Copyright (C) 2024 ZeldaRET + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +#include +#include +#include +#include +#include + +#include "xml.h" +#include "util.h" + +#define copy_out(out, v) memcpy((out), &(v), sizeof(v)); + +/** + * Parse a string as an integer. + * + * The expected value matches case-insensitive regex `^\s*[-+]?(0x[0-9A-F]+|[0-9]+)\s*$`. + * The value may be base 10, or base 16 with a (case-insensitive) 0x prefix. + * Leading and trailing whitespace is ignored. + */ +static int +xml_str_to_int(const char *value) +{ + if (value == NULL || value[0] == '\0') + goto err; + + bool neg = false; + int res; + + size_t value_len = strlen(value); + size_t start; + + // consume initial whitespace + for (start = 0; start < value_len; start++) { + if (!isspace(value[start])) + break; + } + // if we consumed the whole string, it was bad + if (start == value_len) + goto err; + + // handle sign character if present + if (value[start] == '+' || value[start] == '-') { + neg = value[start] == '-'; + start++; + } + + int base; + + // get absolute value in either base 10 or 16 + if (start + 2 < value_len && value[start + 0] == '0' && tolower(value[start + 1]) == 'x') { + start += 2; + base = 16; + } else { + base = 10; + } + + char *str_end; + res = strtol(&value[start], &str_end, base); + size_t end = str_end - value; + if (start == end) + goto err; + + // consume trailing whitespace + while (value[end] != '\0') { + if (!isspace(value[end])) + goto err; + end++; + } + + assert(end == value_len); + + // apply sign + return neg ? -res : res; +err: + error("bad int value %s", value); +} + +void +xml_parse_int(const char *value, void *out) +{ + int v = xml_str_to_int(value); + + copy_out(out, v); +} + +void +xml_parse_uint(const char *value, void *out) +{ + int v = xml_str_to_int(value); + if (v < 0) + error("Value should be unsigned"); + + copy_out(out, v); +} + +void +xml_parse_s16(const char *value, void *out) +{ + int v = xml_str_to_int(value); + if (v < INT16_MIN || v > INT16_MAX) + error("Value %d out of range for s16", v); + int16_t vs16 = v; + + copy_out(out, vs16); +} + +void +xml_parse_u8(const char *value, void *out) +{ + int v = xml_str_to_int(value); + if (v < 0 || v > UINT8_MAX) + error("Value %d out of range for u8", v); + uint8_t vu8 = v; + + copy_out(out, vu8); +} + +void +xml_parse_s8(const char *value, void *out) +{ + int v = xml_str_to_int(value); + if (v < INT8_MIN || v > INT8_MAX) + error("Value %d out of range for s8", v); + int8_t vs8 = v; + + copy_out(out, vs8); +} + +/** + * Parse a note number name to its s8 [0;127] value. + * For example "PITCH_EF4" -> 42. + */ +void +xml_parse_note_number(const char *value, void *out) +{ + size_t value_len = strlen(value); + int8_t vs8; + char c; + + if (value_len == 0) + goto err; + + // consume initial whitespace + size_t start; + for (start = 0; start < value_len; start++) { + if (!isspace(value[start])) + break; + } + // if we consumed the whole string, it was bad + if (start == value_len) + goto err; + + c = toupper(value[start]); + + if (c >= 'A' && c <= 'G') { + start++; + + // got a note number + static const int8_t notes_lut[] = { + /* A */ 12, + /* B */ 14, + /* C */ 3, + /* D */ 5, + /* E */ 7, + /* F */ 8, + /* G */ 10, + }; + char cm = toupper(value[start]); + int mod = 0; + int v; + + if (value_len > start && (cm == 'F' || cm == 'S')) { + // handle flat/sharp modifier + mod = (cm == 'S') ? 1 : -1; + start++; + } + + if (start == value_len) + goto err; + + bool neg = false; + int res; + + // if value starts with NEG (ignoring case) + if (start + 3 <= value_len && toupper(value[start + 0]) == 'N' && toupper(value[start + 1]) == 'E' && + toupper(value[start + 2]) == 'G') { + neg = true; + start += 3; + } + + int base = 10; + + char *str_end; + res = strtol(&value[start], &str_end, base); + size_t end = str_end - value; + if (start == end) + goto err; + + // consume trailing whitespace + while (value[end] != '\0') { + if (!isspace(value[end])) + goto err; + end++; + } + + assert(end == value_len); + + // apply sign + v = neg ? -res : res; + + if (v < -1 || v > 10) + error("Value %d out of range for note number", v); + + vs8 = (v - 1) * 12 + notes_lut[c - 'A'] + mod; + if (vs8 < 0) + vs8 += 128; + } else { // got a raw value + vs8 = xml_str_to_int(&value[start]); + } + + if (vs8 < 0) + goto err; + + copy_out(out, vs8); + return; +err: + error("Invalid note %s", value); +} + +void +xml_parse_string(const char *value, void *out) +{ + // copies only the pointer to the string + copy_out(out, value); +} + +void +xml_parse_c_identifier(const char *value, void *out) +{ + if (!str_is_c_identifier(value)) + error("Input %s is not a valid C Language identifier", value); + + // copies only the pointer to the string + copy_out(out, value); +} + +void +xml_parse_bool(const char *value, void *out) +{ + bool v; + + // TODO make case-insensitive + if (strequ(value, "true")) + v = true; + else if (strequ(value, "false")) + v = false; + else + error("Invalid value %s for bool", value); + + copy_out(out, v); +} + +void +xml_parse_float(const char *value, void *out) +{ + char *end; + float v = strtof(value, &end); + + if (value == end) + error("Invalid value %s for float", value); + if (v < 0.0f) + error("Only positive floats are allowed"); + + copy_out(out, v); +} + +void +xml_parse_double(const char *value, void *out) +{ + char *end; + double v = strtod(value, &end); + + if (value == end) + error("Invalid value %s for double", value); + if (v < 0.0) + error("Only positive doubles are allowed"); + + copy_out(out, v); +} + +void +xml_get_single_property(void *out, const xmlNodePtr node, const char *name, xml_parser_func parser) +{ + xmlAttrPtr attr = node->properties; + + if (attr == NULL || attr->next != NULL) + error("Expected only property %s on line %d", name, node->line); + + const char *prop_name = XMLSTR_TO_STR(attr->name); + + if (!strequ(prop_name, name)) + error("Unexpected attribute on line %d: got: \"%s\", expected: \"%s\"", node->line, prop_name, name); + + xmlChar *xvalue = xmlNodeListGetString(node->doc, attr->children, 1); + const char *value = XMLSTR_TO_STR(xvalue); + + parser(value, (uint8_t *)out); + + if (parser != xml_parse_string && parser != xml_parse_c_identifier) + free(xvalue); +} + +void +xml_parse_node_by_spec(void *out, const xmlNodePtr node, const xml_attr_spec spec, size_t spec_length) +{ + bool *got = alloca(spec_length * sizeof(bool)); + memset(got, false, spec_length * sizeof(bool)); + + LL_FOREACH(xmlAttrPtr, attr, node->properties) { + const char *name = XMLSTR_TO_STR(attr->name); + bool found = false; + + for (size_t i = 0; i < spec_length; i++) { + if (strequ(name, spec[i].name)) { + // strictly speaking this pointer needs to be freed but we may want to save the string itself + // so we just don't free it and let it clean up when the program terminates + xmlChar *xvalue = xmlNodeListGetString(node->doc, attr->children, 1); + const char *value = XMLSTR_TO_STR(xvalue); + + spec[i].parser_func(value, (uint8_t *)out + spec[i].offset); + + if (spec[i].parser_func != xml_parse_string && spec[i].parser_func != xml_parse_c_identifier) + free(xvalue); // free when we don't need the string in the future, TODO strdup when we do need it? + + got[i] = true; + found = true; + break; + } + } + + if (!found) + error("Unrecognized attribute %s on line %d", name, node->line); + } + + for (size_t i = 0; i < spec_length; i++) { + if (!spec[i].optional && !got[i]) + error("Expected a %s attribute on line %d", spec[i].name, node->line); + } +} + +static void +xml_print_rec(xmlNodePtr base, int *pIndent) +{ + LL_FOREACH(xmlNodePtr, node, base) { + if (node->type != XML_ELEMENT_NODE) + continue; + + fprintf(stdout, "%*cChild is <%s> (%i)\n", *pIndent, ' ', node->name, node->type); + *pIndent += 4; + + LL_FOREACH(xmlAttrPtr, attr, node->properties) { + xmlChar *value = xmlNodeListGetString(node->doc, attr->children, 1); + fprintf(stdout, "%*c- Property <%s> \"%s\"\n", *pIndent, ' ', attr->name, XMLSTR_TO_STR(value)); + free(value); + } + + xml_print_rec(node->children, pIndent); + *pIndent -= 4; + } +} + +void +xml_print_tree(xmlDocPtr document) +{ + xmlNodePtr root = xmlDocGetRootElement(document); + int indent = 4; + fprintf(stdout, "Root is <%s> (%i)\n", root->name, root->type); + + LL_FOREACH(xmlAttrPtr, attr, root->properties) { + xmlChar *value = xmlNodeListGetString(root->doc, attr->children, 1); + fprintf(stdout, "%*c- Property <%s> \"%s\"\n", indent, ' ', attr->name, XMLSTR_TO_STR(value)); + free(value); + } + + xml_print_rec(root->children, &indent); +} diff --git a/tools/audio/xml.h b/tools/audio/xml.h new file mode 100644 index 0000000000..dabfbcfefe --- /dev/null +++ b/tools/audio/xml.h @@ -0,0 +1,61 @@ +/** + * SPDX-FileCopyrightText: Copyright (C) 2024 ZeldaRET + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +#ifndef XML_H +#define XML_H + +#include +#include +#include + +#include + +#define XMLSTR_TO_STR(s) ((const char *)(s)) + +typedef void (*xml_parser_func)(const char *, void *); + +typedef struct { + const char *name; + bool optional; + xml_parser_func parser_func; + size_t offset; +} xml_attr_spec[]; + +void +xml_parse_int(const char *value, void *out); +void +xml_parse_uint(const char *value, void *out); +void +xml_parse_s16(const char *value, void *out); +void +xml_parse_u8(const char *value, void *out); +void +xml_parse_s8(const char *value, void *out); +void +xml_parse_note_number(const char *value, void *out); +void +xml_parse_string(const char *value, void *out); +void +xml_parse_c_identifier(const char *value, void *out); +void +xml_parse_bool(const char *value, void *out); +void +xml_parse_float(const char *value, void *out); +void +xml_parse_double(const char *value, void *out); + +void +xml_get_single_property(void *out, const xmlNodePtr node, const char *name, xml_parser_func parser); + +void +xml_parse_node_by_spec(void *out, const xmlNodePtr node, const xml_attr_spec spec, size_t spec_length); + +void +xml_print_tree(xmlDocPtr document); + +#endif