1
0
Fork 0
mirror of https://github.com/zeldaret/oot.git synced 2024-11-25 09:45:02 +00:00

[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
This commit is contained in:
Tharo 2024-08-15 01:54:31 +01:00 committed by GitHub
parent c8ec6042e1
commit d3b9ba17da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1795 additions and 15 deletions

View file

@ -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 $(<D)/aifc/$(<F:.wav=.aifc) $@ && echo "$(<F) OK") || (mkdir -p NONMATCHINGS/$(<D) && cp $(<D)/aifc/$(<F:.wav=.aifc) NONMATCHINGS/$(<D)/$(<F:.wav=.aifc))
endif
# then assemble the samplebanks...
.PRECIOUS: $(BUILD_DIR)/assets/audio/samplebanks/%.xml
$(BUILD_DIR)/assets/audio/samplebanks/%.xml: assets/audio/samplebanks/%.xml
cat $< | $(BUILD_DIR_REPLACE) > $@
$(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 "$(<F) OK"
endif
-include $(DEP_FILES)
# Print target for debugging

View file

@ -71,12 +71,13 @@ The build process has the following package requirements:
* python3-pip
* python3-venv
* libpng-dev
* libxml2-dev
Under Debian / Ubuntu (which we recommend using), you can install them with the following commands:
```bash
sudo apt-get update
sudo apt-get install git build-essential binutils-mips-linux-gnu python3 python3-pip python3-venv libpng-dev
sudo apt-get install git build-essential binutils-mips-linux-gnu python3 python3-pip python3-venv libpng-dev libxml2-dev
```
If you are using GCC as the compiler for Ocarina of Time, you will also need:

View file

@ -13,12 +13,13 @@ For macOS, use Homebrew to install the following dependencies:
* libpng
* bash
* clang-format
* libxml2
You can install them with the following commands:
```bash
brew update
brew install coreutils make python3 libpng bash clang-format
brew install coreutils make python3 libpng bash clang-format libxml2
```
(The repository expects Homebrew-installed programs to be either linked correctly in `$PATH` etc. or in their default locations.)

8
spec
View file

@ -147,7 +147,13 @@ endseg
beginseg
name "Audiotable"
include "$(BUILD_DIR)/baserom/Audiotable.o"
address 0
include "$(BUILD_DIR)/assets/audio/samplebanks/SampleBank_0.o"
include "$(BUILD_DIR)/assets/audio/samplebanks/SampleBank_2.o"
include "$(BUILD_DIR)/assets/audio/samplebanks/SampleBank_3.o"
include "$(BUILD_DIR)/assets/audio/samplebanks/SampleBank_4.o"
include "$(BUILD_DIR)/assets/audio/samplebanks/SampleBank_5.o"
include "$(BUILD_DIR)/assets/audio/samplebanks/SampleBank_6.o"
endseg
#if OOT_NTSC

29
tools/audio/.clang-format Normal file
View file

@ -0,0 +1,29 @@
IndentWidth: 4
Language: Cpp
UseTab: Never
ColumnLimit: 120
PointerAlignment: Right
BreakBeforeBraces: Linux
AlwaysBreakAfterReturnType: TopLevel
AlignArrayOfStructures: Left
SpaceAfterCStyleCast: false
SpaceBeforeParens: ControlStatementsExceptControlMacros
Cpp11BracedListStyle: false
IndentCaseLabels: true
BinPackArguments: true
BinPackParameters: true
AlignAfterOpenBracket: Align
AlignOperands: true
BreakBeforeTernaryOperators: true
BreakBeforeBinaryOperators: None
AllowShortBlocksOnASingleLine: true
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: false
AllowShortEnumsOnASingleLine: false
AlignEscapedNewlines: Left
AlignTrailingComments: true
SortIncludes: false
AlignConsecutiveMacros: Consecutive
ForEachMacros: ['LL_FOREACH']

3
tools/audio/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
__pycache__/
sbc

View file

@ -1,14 +1,44 @@
PROGRAMS := sbc
ifeq ($(shell which xml2-config),)
$(error xml2-config not found. Did you install libxml2-dev?)
endif
CLANG_FORMAT := clang-format-14
FORMAT_ARGS := -i -style=file
CC := gcc
CFLAGS := -Wall -Wextra -pedantic
OPTFLAGS := -Og -g3
XML_CFLAGS := $(shell xml2-config --cflags)
XML_LDFLAGS := $(shell xml2-config --libs)
.PHONY: all clean distclean format
all:
all: $(PROGRAMS)
$(MAKE) -C sampleconv
clean:
$(RM) $(PROGRAMS)
$(MAKE) -C sampleconv clean
distclean: clean
$(MAKE) -C sampleconv distclean
format:
$(CLANG_FORMAT) $(FORMAT_ARGS) $(shell find . -maxdepth 1 -type f -name "*.[ch]")
$(MAKE) -C sampleconv format
sbc_SOURCES := samplebank_compiler.c samplebank.c aifc.c xml.c util.c
sbc_CFLAGS := $(XML_CFLAGS)
sbc_LDFLAGS := $(XML_LDFLAGS)
define COMPILE =
$(1): $($1_SOURCES)
$(CC) $(CFLAGS) $($1_CFLAGS) $$^ $($1_LDFLAGS) -o $$@
endef
$(foreach p,$(PROGRAMS),$(eval $(call COMPILE,$(p))))

552
tools/audio/aifc.c Normal file
View file

@ -0,0 +1,552 @@
/**
* 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 <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#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);
}

75
tools/audio/aifc.h Normal file
View file

@ -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 <stdbool.h>
#include <stddef.h>
#include <stdint.h>
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

124
tools/audio/samplebank.c Normal file
View file

@ -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 <assert.h>
#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:
// <SampleBank Name="" Index="" Medium="" CachePolicy="" BufferBug="">
// <Pointer Index=""/>
// <Sample Name="" Path=""/>
// <Blob Name="" Path=""/>
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 <SampleBank>");
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;
}

36
tools/audio/samplebank.h Normal file
View file

@ -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

View file

@ -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 <assert.h>
#include <stdio.h>
#include <string.h>
#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 <out.d>] <in.xml> <out.s>\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;
}

144
tools/audio/util.c Normal file
View file

@ -0,0 +1,144 @@
/* SPDX-FileCopyrightText: Copyright (C) 2024 ZeldaRET */
/* SPDX-License-Identifier: CC0-1.0 */
#define _GNU_SOURCE
#include <ctype.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#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;
}

72
tools/audio/util.h Normal file
View file

@ -0,0 +1,72 @@
/* SPDX-FileCopyrightText: Copyright (C) 2024 ZeldaRET */
/* SPDX-License-Identifier: CC0-1.0 */
#ifndef UTIL_H_
#define UTIL_H_
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
// Endian
#if defined(__linux__) || defined(__CYGWIN__)
#include <endian.h>
#elif defined(__APPLE__)
#include <libkern/OSByteOrder.h>
#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

393
tools/audio/xml.c Normal file
View file

@ -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 <assert.h>
#include <ctype.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#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);
}

61
tools/audio/xml.h Normal file
View file

@ -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 <stddef.h>
#include <stdbool.h>
#include <stdint.h>
#include <libxml/parser.h>
#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