From aa97586659dbd24174258e0437ab9411a9747d3d Mon Sep 17 00:00:00 2001 From: Tharo <17233964+Thar0@users.noreply.github.com> Date: Wed, 28 Aug 2024 02:09:59 +0100 Subject: [PATCH] [Audio 6/?] Build Soundfonts and the Soundfont Table (#2056) * [Audio 6/?] Build Soundfonts and the Soundfont Table * Improve lots of error messages * First suggested changes * Make audio build debugging more friendly Co-authored-by: Dragorn421 * Some fixes from MM review * Make soundfont_table.h generation depend on the samplebank xmls since they are read, report from which soundfont the invalid pointer indirect warning originates from --------- Co-authored-by: Dragorn421 --- Makefile | 77 +- baseroms/gc-eu-mq-dbg/config.yml | 4 - baseroms/gc-eu-mq/config.yml | 4 - baseroms/gc-eu/config.yml | 4 - baseroms/gc-jp-ce/config.yml | 4 - baseroms/gc-jp-mq/config.yml | 4 - baseroms/gc-jp/config.yml | 4 - baseroms/gc-us-mq/config.yml | 4 - baseroms/gc-us/config.yml | 4 - baseroms/ntsc-1.2/config.yml | 4 - data/audio_tables.rodata.s | 3 - include/attributes.h | 9 +- include/audio/soundfont_file.h | 46 + include/variables.h | 2 +- include/z64audio.h | 24 +- linker_scripts/soundfont.ld | 19 + spec | 43 +- src/audio/lib/heap.c | 6 +- src/audio/lib/load.c | 2 +- src/audio/lib/seqplayer.c | 2 +- src/audio/lib/synthesis.c | 8 +- src/audio/lib/thread.c | 2 +- src/audio/tables/soundfont_table.c | 50 + src/code/graph.c | 2 +- src/code/speed_meter.c | 2 +- src/code/z_collision_check.c | 2 - src/code/z_play.c | 3 +- src/overlays/actors/ovl_Fishing/z_fishing.c | 2 +- tools/audio/.gitignore | 2 + tools/audio/Makefile | 10 +- tools/audio/aifc.c | 7 +- tools/audio/aifc.h | 5 + tools/audio/audio_tablegen.c | 144 +- tools/audio/elf32.h | 235 +++ tools/audio/sampleconv/src/codec/vadpcm.c | 4 +- tools/audio/sampleconv/src/container/aiff.c | 4 +- tools/audio/sfpatch.c | 57 + tools/audio/soundfont.c | 70 + tools/audio/soundfont.h | 170 ++ tools/audio/soundfont_compiler.c | 1814 +++++++++++++++++++ 40 files changed, 2775 insertions(+), 87 deletions(-) create mode 100644 include/audio/soundfont_file.h create mode 100644 linker_scripts/soundfont.ld create mode 100644 src/audio/tables/soundfont_table.c create mode 100644 tools/audio/elf32.h create mode 100644 tools/audio/sfpatch.c create mode 100644 tools/audio/soundfont.c create mode 100644 tools/audio/soundfont.h create mode 100644 tools/audio/soundfont_compiler.c diff --git a/Makefile b/Makefile index d9291e0c52..33c7cf21c7 100644 --- a/Makefile +++ b/Makefile @@ -237,9 +237,12 @@ BUILD_DIR_REPLACE := sed -e 's|$$(BUILD_DIR)|$(BUILD_DIR)|g' AUDIO_EXTRACT := $(PYTHON) tools/audio_extraction.py SAMPLECONV := tools/audio/sampleconv/sampleconv SBC := tools/audio/sbc +SFC := tools/audio/sfc +SFPATCH := tools/audio/sfpatch ATBLGEN := tools/audio/atblgen SBCFLAGS := --matching +SFCFLAGS := --matching CFLAGS += $(CPP_DEFINES) CPPFLAGS += $(CPP_DEFINES) @@ -267,7 +270,7 @@ else # Suppress warnings for wrong number of macro arguments (to fake variadic # macros) and Microsoft extensions such as anonymous structs (which the # compiler does support but warns for their usage). - CFLAGS += -G 0 -non_shared -fullwarn -verbose -Xcpluscomm $(INC) -Wab,-r4300_mul -woff 516,609,649,838,712 + CFLAGS += -G 0 -non_shared -fullwarn -verbose -Xcpluscomm $(INC) -Wab,-r4300_mul -woff 516,609,649,838,712,807 MIPS_VERSION := -mips2 endif @@ -308,9 +311,11 @@ 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) + SOUNDFONT_EXTRACT_DIRS := $(shell find $(EXTRACTED_DIR)/assets/audio/soundfonts -type d) else SAMPLE_EXTRACT_DIRS := SAMPLEBANK_EXTRACT_DIRS := + SOUNDFONT_EXTRACT_DIRS := endif ifneq ($(wildcard assets/audio/samples),) @@ -325,6 +330,12 @@ else SAMPLEBANK_DIRS := endif +ifneq ($(wildcard assets/audio/soundfonts),) + SOUNDFONT_DIRS := $(shell find assets/audio/soundfonts -type d) +else + SOUNDFONT_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)/%)) @@ -335,6 +346,13 @@ SAMPLEBANK_BUILD_XMLS := $(foreach f,$(SAMPLEBANK_XMLS),$(BUILD_DIR)/$f) $(for SAMPLEBANK_O_FILES := $(foreach f,$(SAMPLEBANK_BUILD_XMLS),$(f:.xml=.o)) SAMPLEBANK_DEP_FILES := $(foreach f,$(SAMPLEBANK_O_FILES),$(f:.o=.d)) +SOUNDFONT_XMLS := $(foreach dir,$(SOUNDFONT_DIRS),$(wildcard $(dir)/*.xml)) +SOUNDFONT_EXTRACT_XMLS := $(foreach dir,$(SOUNDFONT_EXTRACT_DIRS),$(wildcard $(dir)/*.xml)) +SOUNDFONT_BUILD_XMLS := $(foreach f,$(SOUNDFONT_XMLS),$(BUILD_DIR)/$f) $(foreach f,$(SOUNDFONT_EXTRACT_XMLS),$(f:$(EXTRACTED_DIR)/%=$(BUILD_DIR)/%)) +SOUNDFONT_O_FILES := $(foreach f,$(SOUNDFONT_BUILD_XMLS),$(f:.xml=.o)) +SOUNDFONT_HEADERS := $(foreach f,$(SOUNDFONT_BUILD_XMLS),$(f:.xml=.h)) +SOUNDFONT_DEP_FILES := $(foreach f,$(SOUNDFONT_O_FILES),$(f:.o=.d)) + # create extracted directories $(shell mkdir -p $(EXTRACTED_DIR) $(EXTRACTED_DIR)/assets $(EXTRACTED_DIR)/text) @@ -388,12 +406,14 @@ $(shell mkdir -p $(foreach dir, \ $(UNDECOMPILED_DATA_DIRS) \ $(SAMPLE_DIRS) \ $(SAMPLEBANK_DIRS) \ + $(SOUNDFONT_DIRS) \ $(ASSET_BIN_DIRS_COMMITTED), \ $(BUILD_DIR)/$(dir))) ifneq ($(wildcard $(EXTRACTED_DIR)/assets),) $(shell mkdir -p $(foreach dir, \ $(SAMPLE_EXTRACT_DIRS) \ $(SAMPLEBANK_EXTRACT_DIRS) \ + $(SOUNDFONT_EXTRACT_DIRS) \ $(ASSET_BIN_DIRS_EXTRACTED), \ $(dir:$(EXTRACTED_DIR)/%=$(BUILD_DIR)/%))) endif @@ -565,7 +585,8 @@ $(ROMC): $(ROM) $(ELF) $(BUILD_DIR)/compress_ranges.txt $(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 \ - $(SAMPLEBANK_O_FILES) + $(SAMPLEBANK_O_FILES) $(SOUNDFONT_O_FILES) \ + $(BUILD_DIR)/assets/audio/audiobank_padding.o $(LD) -T $(LDSCRIPT) -T $(BUILD_DIR)/undefined_syms.txt --no-check-sections --accept-unknown-input-arch --emit-relocs -Map $(MAP) -o $@ ## Order-only prerequisites @@ -700,6 +721,10 @@ $(BUILD_DIR)/assets/%.jpg.inc.c: $(EXTRACTED_DIR)/assets/%.jpg # Audio AUDIO_BUILD_DEBUG ?= 0 +ifeq ($(AUDIO_BUILD_DEBUG),1) + # for debugging only, make soundfonts depend on samplebanks so they can be linked against + $(BUILD_DIR)/assets/audio/soundfonts/%.o: $(SAMPLEBANK_O_FILES) +endif # first build samples... @@ -747,11 +772,47 @@ ifeq ($(AUDIO_BUILD_DEBUG),1) @cmp $(@:.o=.bin) $(patsubst $(BUILD_DIR)/assets/audio/samplebanks/%,$(EXTRACTED_DIR)/baserom_audiotest/audiotable_files/%,$(@:.o=.bin)) && echo "$( $@ + +$(BUILD_DIR)/assets/audio/soundfonts/%.xml: $(EXTRACTED_DIR)/assets/audio/soundfonts/%.xml + cat $< | $(BUILD_DIR_REPLACE) > $@ + +.PRECIOUS: $(BUILD_DIR)/assets/audio/soundfonts/%.c $(BUILD_DIR)/assets/audio/soundfonts/%.h $(BUILD_DIR)/assets/audio/soundfonts/%.name +$(BUILD_DIR)/assets/audio/soundfonts/%.c $(BUILD_DIR)/assets/audio/soundfonts/%.h $(BUILD_DIR)/assets/audio/soundfonts/%.name: $(BUILD_DIR)/assets/audio/soundfonts/%.xml | $(SAMPLEBANK_BUILD_XMLS) $(AIFC_FILES) +# This rule can be triggered for either the .c or .h file, so $@ may refer to either the .c or .h file. A simple +# substitution $(@:.c=.h) will fail ~50% of the time with -j. Instead, don't assume anything about the suffix of $@. + $(SFC) $(SFCFLAGS) --makedepend $(basename $@).d $< $(basename $@).c $(basename $@).h $(basename $@).name + +-include $(SOUNDFONT_DEP_FILES) + +$(BUILD_DIR)/assets/audio/soundfonts/%.o: $(BUILD_DIR)/assets/audio/soundfonts/%.c $(BUILD_DIR)/assets/audio/soundfonts/%.name +# compile c to unlinked object + $(CC) -c $(CFLAGS) $(MIPS_VERSION) $(OPTFLAGS) -I include/audio -o $(@:.o=.tmp) $< +# partial link + $(LD) -r -T linker_scripts/soundfont.ld $(@:.o=.tmp) -o $(@:.o=.tmp2) +# patch defined symbols to be ABS symbols so that they remain file-relative offsets forever + $(SFPATCH) $(@:.o=.tmp2) $(@:.o=.tmp2) +# write start and size symbols afterwards, filename != symbolic name so source symbolic name from the .name file written by sfc + $(OBJCOPY) --add-symbol $$(cat $(<:.c=.name))_Start=.rodata:0,global --redefine-sym __LEN__=$$(cat $(<:.c=.name))_Size $(@:.o=.tmp2) $@ +# cleanup temp files + @$(RM) $(@:.o=.tmp) $(@:.o=.tmp2) +ifeq ($(AUDIO_BUILD_DEBUG),1) + $(LD) $(foreach f,$(SAMPLEBANK_O_FILES),-R $f) -T linker_scripts/soundfont.ld $@ -o $(@:.o=.elf) + $(OBJCOPY) -O binary -j.rodata $(@:.o=.elf) $(@:.o=.bin) + @(cmp $(@:.o=.bin) $(patsubst $(BUILD_DIR)/assets/audio/soundfonts/%,$(EXTRACTED_DIR)/baserom_audiotest/audiobank_files/%,$(@:.o=.bin)) && echo "$( rodata $(BUILD_DIR)/src/audio/tables/samplebank_table.o: src/audio/tables/samplebank_table.c $(BUILD_DIR)/assets/audio/samplebank_table.h @@ -762,6 +823,18 @@ endif $(LD) -r -T linker_scripts/data_with_rodata.ld $(@:.o=.tmp) -o $@ @$(RM) $(@:.o=.tmp) +$(BUILD_DIR)/src/audio/tables/soundfont_table.o: src/audio/tables/soundfont_table.c $(BUILD_DIR)/assets/audio/soundfont_table.h $(SOUNDFONT_HEADERS) +ifneq ($(RUN_CC_CHECK),0) + $(CC_CHECK) $< +endif + $(CC) -c $(CFLAGS) $(MIPS_VERSION) $(OPTFLAGS) -o $(@:.o=.tmp) $< + $(LD) -r -T linker_scripts/data_with_rodata.ld $(@:.o=.tmp) -o $@ + @$(RM) $(@:.o=.tmp) + +# Extra audiobank padding that doesn't belong to any soundfont file +$(BUILD_DIR)/assets/audio/audiobank_padding.o: + echo ".section .rodata; .fill 0x20" | $(AS) $(ASFLAGS) -o $@ + -include $(DEP_FILES) # Print target for debugging diff --git a/baseroms/gc-eu-mq-dbg/config.yml b/baseroms/gc-eu-mq-dbg/config.yml index c9841df4c2..204affcc25 100644 --- a/baseroms/gc-eu-mq-dbg/config.yml +++ b/baseroms/gc-eu-mq-dbg/config.yml @@ -29,10 +29,6 @@ incbins: segment: code vram: 0x8012A7C0 size: 0x400 - - name: gSoundFontTable - segment: code - vram: 0x801550D0 - size: 0x270 - name: gSequenceFontTable segment: code vram: 0x80155340 diff --git a/baseroms/gc-eu-mq/config.yml b/baseroms/gc-eu-mq/config.yml index 6a44327d54..ffdef5fecc 100644 --- a/baseroms/gc-eu-mq/config.yml +++ b/baseroms/gc-eu-mq/config.yml @@ -21,10 +21,6 @@ incbins: segment: code vram: 0x800E3A10 size: 0xAF0 - - name: gSoundFontTable - segment: code - vram: 0x80110470 - size: 0x270 - name: gSequenceFontTable segment: code vram: 0x801106E0 diff --git a/baseroms/gc-eu/config.yml b/baseroms/gc-eu/config.yml index a17207c192..48f3913a4b 100644 --- a/baseroms/gc-eu/config.yml +++ b/baseroms/gc-eu/config.yml @@ -21,10 +21,6 @@ incbins: segment: code vram: 0x800E3A30 size: 0xAF0 - - name: gSoundFontTable - segment: code - vram: 0x80110490 - size: 0x270 - name: gSequenceFontTable segment: code vram: 0x80110700 diff --git a/baseroms/gc-jp-ce/config.yml b/baseroms/gc-jp-ce/config.yml index 853c09a025..b67ab356c5 100644 --- a/baseroms/gc-jp-ce/config.yml +++ b/baseroms/gc-jp-ce/config.yml @@ -21,10 +21,6 @@ incbins: segment: code vram: 0x800E60B0 size: 0xAF0 - - name: gSoundFontTable - segment: code - vram: 0x80112C80 - size: 0x270 - name: gSequenceFontTable segment: code vram: 0x80112EF0 diff --git a/baseroms/gc-jp-mq/config.yml b/baseroms/gc-jp-mq/config.yml index e3b01e7ede..53b0a71bdb 100644 --- a/baseroms/gc-jp-mq/config.yml +++ b/baseroms/gc-jp-mq/config.yml @@ -21,10 +21,6 @@ incbins: segment: code vram: 0x800E60B0 size: 0xAF0 - - name: gSoundFontTable - segment: code - vram: 0x80112C80 - size: 0x270 - name: gSequenceFontTable segment: code vram: 0x80112EF0 diff --git a/baseroms/gc-jp/config.yml b/baseroms/gc-jp/config.yml index 4479cd93be..b817960b1b 100644 --- a/baseroms/gc-jp/config.yml +++ b/baseroms/gc-jp/config.yml @@ -21,10 +21,6 @@ incbins: segment: code vram: 0x800E60D0 size: 0xAF0 - - name: gSoundFontTable - segment: code - vram: 0x80112CA0 - size: 0x270 - name: gSequenceFontTable segment: code vram: 0x80112F10 diff --git a/baseroms/gc-us-mq/config.yml b/baseroms/gc-us-mq/config.yml index ec6f1f1be4..da23eec425 100644 --- a/baseroms/gc-us-mq/config.yml +++ b/baseroms/gc-us-mq/config.yml @@ -21,10 +21,6 @@ incbins: segment: code vram: 0x800E6090 size: 0xAF0 - - name: gSoundFontTable - segment: code - vram: 0x80112C60 - size: 0x270 - name: gSequenceFontTable segment: code vram: 0x80112ED0 diff --git a/baseroms/gc-us/config.yml b/baseroms/gc-us/config.yml index 7612a6970d..ae0adc79f6 100644 --- a/baseroms/gc-us/config.yml +++ b/baseroms/gc-us/config.yml @@ -21,10 +21,6 @@ incbins: segment: code vram: 0x800E60B0 size: 0xAF0 - - name: gSoundFontTable - segment: code - vram: 0x80112C80 - size: 0x270 - name: gSequenceFontTable segment: code vram: 0x80112EF0 diff --git a/baseroms/ntsc-1.2/config.yml b/baseroms/ntsc-1.2/config.yml index 4455c86879..fef7e941c6 100644 --- a/baseroms/ntsc-1.2/config.yml +++ b/baseroms/ntsc-1.2/config.yml @@ -24,10 +24,6 @@ incbins: segment: code vram: 0x800E5E70 size: 0xAF0 - - name: gSoundFontTable - segment: code - vram: 0x80113DF0 - size: 0x270 - name: gSequenceFontTable segment: code vram: 0x80114060 diff --git a/data/audio_tables.rodata.s b/data/audio_tables.rodata.s index 88f9cee98a..9014311c01 100644 --- a/data/audio_tables.rodata.s +++ b/data/audio_tables.rodata.s @@ -9,9 +9,6 @@ .balign 16 -glabel gSoundFontTable - .incbin "incbin/gSoundFontTable" - glabel gSequenceFontTable .incbin "incbin/gSequenceFontTable" diff --git a/include/attributes.h b/include/attributes.h index ce1365fde3..0bee9c40ca 100644 --- a/include/attributes.h +++ b/include/attributes.h @@ -5,9 +5,10 @@ #define __attribute__(x) #endif -#define UNUSED __attribute__((unused)) -#define FALLTHROUGH __attribute__((fallthrough)) -#define NORETURN __attribute__((noreturn)) -#define NO_REORDER __attribute__((no_reorder)) +#define UNUSED __attribute__((unused)) +#define FALLTHROUGH __attribute__((fallthrough)) +#define NORETURN __attribute__((noreturn)) +#define NO_REORDER __attribute__((no_reorder)) +#define SECTION_DATA __attribute__((section(".data"))) #endif diff --git a/include/audio/soundfont_file.h b/include/audio/soundfont_file.h new file mode 100644 index 0000000000..3f320a6e56 --- /dev/null +++ b/include/audio/soundfont_file.h @@ -0,0 +1,46 @@ +#ifndef SOUNDFONT_FILE_H +#define SOUNDFONT_FILE_H + +#include "libc/stdbool.h" +#include "alignment.h" +#include "attributes.h" +#include "z64audio.h" + +// Envelope definitions + +#define ENVELOPE_POINT(delay, target) { (delay), (target) } +#define ENVELOPE_DISABLE() { ADSR_DISABLE, 0 } +#define ENVELOPE_HANG() { ADSR_HANG, 0 } +#define ENVELOPE_GOTO(index) { ADSR_GOTO, (index) } +#define ENVELOPE_RESTART() { ADSR_RESTART, 0 } + +// Instrument definitions + +#define INSTR_SAMPLE_NONE { NULL, 0.0f } +#define INSTR_SAMPLE_LO_NONE 0 +#define INSTR_SAMPLE_HI_NONE 127 + +// Explicit padding is sometimes required where soundfont data was padded to 0x10 bytes (possibly due to source file +// splits in the original soundfonts?) +// It's less convenient for us to emit multiple files per soundfont, so instead we fill in the padding manually. + +#ifndef GLUE +#define GLUE(a,b) a##b +#endif +#ifndef GLUE2 +#define GLUE2(a,b) GLUE(a,b) +#endif + +#ifdef __sgi +// For IDO, we have to add explicit padding arrays +#define SF_PAD4() static u8 GLUE2(_pad, __LINE__) [] = { 0,0,0,0 } +#define SF_PAD8() static u8 GLUE2(_pad, __LINE__) [] = { 0,0,0,0,0,0,0,0 } +#define SF_PADC() static u8 GLUE2(_pad, __LINE__) [] = { 0,0,0,0,0,0,0,0,0,0,0,0 } +#else +// For other compilers, the soundfont compiler (sfc) emits alignment attributes that handle this automatically +#define SF_PAD4() +#define SF_PAD8() +#define SF_PADC() +#endif + +#endif diff --git a/include/variables.h b/include/variables.h index c7d16e3ae2..87ac90bd91 100644 --- a/include/variables.h +++ b/include/variables.h @@ -168,7 +168,7 @@ extern s32 __osPfsLastChannel; extern const TempoData gTempoData; extern const AudioHeapInitSizes gAudioHeapInitSizes; extern s16 gOcarinaSongItemMap[]; -extern u8 gSoundFontTable[]; +extern AudioTable gSoundFontTable; extern u8 gSequenceFontTable[]; extern u8 gSequenceTable[]; extern AudioTable gSampleBankTable; diff --git a/include/z64audio.h b/include/z64audio.h index 6c2a34edf0..a86c805d0e 100644 --- a/include/z64audio.h +++ b/include/z64audio.h @@ -177,18 +177,32 @@ typedef struct EnvelopePoint { /* 0x2 */ s16 arg; } EnvelopePoint; // size = 0x4 -typedef struct AdpcmLoop { +typedef struct AdpcmLoopHeader { /* 0x00 */ u32 start; - /* 0x04 */ u32 end; - /* 0x08 */ u32 count; + /* 0x04 */ u32 end; // s16 sample position where the loop ends + /* 0x08 */ u32 count; // The number of times the loop is played before the sound completes. Setting count to -1 indicates that the loop should play indefinitely. /* 0x0C */ char unk_0C[0x4]; +} AdpcmLoopHeader; // size = 0x10 + +typedef struct AdpcmLoop { + /* 0x00 */ AdpcmLoopHeader header; /* 0x10 */ s16 predictorState[16]; // only exists if count != 0. 8-byte aligned } AdpcmLoop; // size = 0x30 (or 0x10) -typedef struct AdpcmBook { +typedef struct AdpcmBookHeader { /* 0x00 */ s32 order; /* 0x04 */ s32 numPredictors; - /* 0x08 */ s16 book[1]; // size 8 * order * numPredictors. 8-byte aligned +} AdpcmBookHeader; // size = 0x8 + +/** + * The procedure used to design the codeBook is based on an adaptive clustering algorithm. + * The size of the codeBook is (8 * order * numPredictors) and is 8-byte aligned + */ +typedef s16 AdpcmBookData[]; + +typedef struct AdpcmBook { + /* 0x00 */ AdpcmBookHeader header; + /* 0x08 */ AdpcmBookData book; // size 8 * order * numPredictors. 8-byte aligned } AdpcmBook; // size >= 0x8 typedef struct Sample { diff --git a/linker_scripts/soundfont.ld b/linker_scripts/soundfont.ld new file mode 100644 index 0000000000..d914a7de31 --- /dev/null +++ b/linker_scripts/soundfont.ld @@ -0,0 +1,19 @@ +OUTPUT_ARCH (mips) + +/* Soundfont Linker Script, maps data into rodata and adds a file length symbol */ + +SECTIONS { + + .rodata : + { + *(.data*) + *(.rodata*) + . = ALIGN(16); + __LEN__ = . - ADDR(.rodata); + } + + /DISCARD/ : + { + *(*); + } +} diff --git a/spec b/spec index fd724b9c61..434f3281c8 100644 --- a/spec +++ b/spec @@ -141,8 +141,46 @@ endseg beginseg name "Audiobank" - address 0x10 // fake RAM address to avoid map lookup inaccuracies - include "$(BUILD_DIR)/baserom/Audiobank.o" + address 0 + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_0.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_1.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_2.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_3.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_4.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_5.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_6.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_7.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_8.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_9.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_10.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_11.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_12.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_13.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_14.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_15.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_16.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_17.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_18.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_19.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_20.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_21.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_22.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_23.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_24.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_25.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_26.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_27.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_28.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_29.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_30.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_31.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_32.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_33.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_34.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_35.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_36.o" + include "$(BUILD_DIR)/assets/audio/soundfonts/Soundfont_37.o" + include "$(BUILD_DIR)/assets/audio/audiobank_padding.o" endseg beginseg @@ -650,6 +688,7 @@ beginseg // combined object before the final link. include "$(BUILD_DIR)/src/code/z_message_z_game_over.o" include "$(BUILD_DIR)/src/code/z_construct.o" + include "$(BUILD_DIR)/src/audio/tables/soundfont_table.o" include "$(BUILD_DIR)/data/audio_tables.rodata.o" include "$(BUILD_DIR)/src/audio/tables/samplebank_table.o" include "$(BUILD_DIR)/data/rsp.text.o" diff --git a/src/audio/lib/heap.c b/src/audio/lib/heap.c index e68ef6eec9..a29e0d9c9f 100644 --- a/src/audio/lib/heap.c +++ b/src/audio/lib/heap.c @@ -1002,9 +1002,9 @@ void AudioHeap_Init(void) { reverb->sample.medium = MEDIUM_RAM; reverb->sample.size = reverb->windowSize * SAMPLE_SIZE; reverb->sample.sampleAddr = (u8*)reverb->leftRingBuf; - reverb->loop.start = 0; - reverb->loop.count = 1; - reverb->loop.end = reverb->windowSize; + reverb->loop.header.start = 0; + reverb->loop.header.count = 1; + reverb->loop.header.end = reverb->windowSize; if (reverb->downsampleRate != 1) { reverb->unk_0E = 0x8000 / reverb->downsampleRate; diff --git a/src/audio/lib/load.c b/src/audio/lib/load.c index 807bab3171..be87f5c457 100644 --- a/src/audio/lib/load.c +++ b/src/audio/lib/load.c @@ -1214,7 +1214,7 @@ void AudioLoad_Init(void* heap, u32 heapSize) { // Set audio tables pointers gAudioCtx.sequenceTable = (AudioTable*)gSequenceTable; - gAudioCtx.soundFontTable = (AudioTable*)gSoundFontTable; + gAudioCtx.soundFontTable = &gSoundFontTable; gAudioCtx.sampleBankTable = &gSampleBankTable; gAudioCtx.sequenceFontTable = gSequenceFontTable; diff --git a/src/audio/lib/seqplayer.c b/src/audio/lib/seqplayer.c index 4caddea4e4..11866bf615 100644 --- a/src/audio/lib/seqplayer.c +++ b/src/audio/lib/seqplayer.c @@ -946,7 +946,7 @@ s32 AudioSeq_SeqLayerProcessScriptStep4(SequenceLayer* layer, s32 cmd) { if (layer->delay == 0) { if (layer->tunedSample != NULL) { - time = layer->tunedSample->sample->loop->end; + time = layer->tunedSample->sample->loop->header.end; } else { time = 0.0f; } diff --git a/src/audio/lib/synthesis.c b/src/audio/lib/synthesis.c index b93494de4a..2a84f0cde5 100644 --- a/src/audio/lib/synthesis.c +++ b/src/audio/lib/synthesis.c @@ -796,7 +796,7 @@ Acmd* AudioSynth_ProcessNote(s32 noteIndex, NoteSubEu* noteSubEu, NoteSynthesisS } else { sample = noteSubEu->tunedSample->sample; loopInfo = sample->loop; - loopEndPos = loopInfo->end; + loopEndPos = loopInfo->header.end; sampleAddr = (u32)sample->sampleAddr; resampledTempLen = 0; @@ -829,7 +829,7 @@ Acmd* AudioSynth_ProcessNote(s32 noteIndex, NoteSubEu* noteSubEu, NoteSynthesisS if (1) {} if (1) {} if (1) {} - nEntries = SAMPLES_PER_FRAME * sample->book->order * sample->book->numPredictors; + nEntries = SAMPLES_PER_FRAME * sample->book->header.order * sample->book->header.numPredictors; aLoadADPCM(cmd++, nEntries, gAudioCtx.curLoadedBook); } } @@ -861,7 +861,7 @@ Acmd* AudioSynth_ProcessNote(s32 noteIndex, NoteSubEu* noteSubEu, NoteSynthesisS nSamplesInFirstFrame = nSamplesUntilLoopEnd; } nFramesToDecode = (nSamplesToDecode + SAMPLES_PER_FRAME - 1) / SAMPLES_PER_FRAME; - if (loopInfo->count != 0) { + if (loopInfo->header.count != 0) { // Loop around and restart restart = true; } else { @@ -1019,7 +1019,7 @@ Acmd* AudioSynth_ProcessNote(s32 noteIndex, NoteSubEu* noteSubEu, NoteSynthesisS } else { if (restart) { synthState->restart = true; - synthState->samplePosInt = loopInfo->start; + synthState->samplePosInt = loopInfo->header.start; } else { synthState->samplePosInt += nSamplesToProcess; } diff --git a/src/audio/lib/thread.c b/src/audio/lib/thread.c index 22afc33fdf..d1809c2d01 100644 --- a/src/audio/lib/thread.c +++ b/src/audio/lib/thread.c @@ -795,7 +795,7 @@ s32 func_800E6590(s32 seqPlayerIndex, s32 channelIndex, s32 layerIndex) { if (tunedSample == NULL) { return 0; } - loopEnd = tunedSample->sample->loop->end; + loopEnd = tunedSample->sample->loop->header.end; samplePos = note->synthesisState.samplePosInt; return loopEnd - samplePos; } diff --git a/src/audio/tables/soundfont_table.c b/src/audio/tables/soundfont_table.c new file mode 100644 index 0000000000..65d1fc9a52 --- /dev/null +++ b/src/audio/tables/soundfont_table.c @@ -0,0 +1,50 @@ +#include "attributes.h" +#include "z64audio.h" + +// Symbol definition + +extern AudioTable gSoundFontTable; +#pragma weak gSoundFontTable = sSoundFontTableHeader + +// Externs for table + +#define DEFINE_SOUNDFONT(name, medium, cachePolicy, sampleBankNormal, sampleBankDD, nInstruments, nDrums, nSfx) \ + extern u8 name##_Start[]; \ + extern u8 name##_Size[]; + +#include "assets/audio/soundfont_table.h" + +#undef DEFINE_SOUNDFONT + +// Table header + +NO_REORDER AudioTableHeader sSoundFontTableHeader = { +// The table contains the number of soundfonts, count them with the preprocessor +#define DEFINE_SOUNDFONT(name, medium, cachePolicy, sampleBankNormal, sampleBankDD, nInstruments, nDrums, nSfx) 1 + + +#include "assets/audio/soundfont_table.h" + 0, + +#undef DEFINE_SOUNDFONT + + 0, + 0x00000000, + { 0, 0, 0, 0, 0, 0, 0, 0 }, +}; + +// Table body + +NO_REORDER AudioTableEntry sSoundFontTableEntries[] = { +#define DEFINE_SOUNDFONT(name, medium, cachePolicy, sampleBankNormal, sampleBankDD, nInstruments, nDrums, nSfx) \ + { (u32)name##_Start, \ + (u32)name##_Size, \ + (medium), \ + (cachePolicy), \ + ((sampleBankNormal) << 8) | (sampleBankDD), \ + ((nInstruments) << 8) | (nDrums), \ + (nSfx) }, + +#include "assets/audio/soundfont_table.h" + +#undef DEFINE_SOUNDFONT +}; diff --git a/src/code/graph.c b/src/code/graph.c index 747416399b..f6cf66b12c 100644 --- a/src/code/graph.c +++ b/src/code/graph.c @@ -4,7 +4,7 @@ #define GFXPOOL_HEAD_MAGIC 0x1234 #define GFXPOOL_TAIL_MAGIC 0x5678 -#pragma increment_block_number "gc-eu:128 gc-eu-mq:128" +#pragma increment_block_number "gc-eu:128 gc-eu-mq:128 gc-jp:128 gc-jp-ce:128 gc-jp-mq:128 gc-us:128 gc-us-mq:128" /** * The time at which the previous `Graph_Update` ended. diff --git a/src/code/speed_meter.c b/src/code/speed_meter.c index 13e2622c61..903cd0fc76 100644 --- a/src/code/speed_meter.c +++ b/src/code/speed_meter.c @@ -1,4 +1,4 @@ -#pragma increment_block_number "gc-eu:128 gc-eu-mq:128" +#pragma increment_block_number "gc-eu:128 gc-eu-mq:128 gc-jp:128 gc-jp-ce:128 gc-jp-mq:128 gc-us:128 gc-us-mq:128" #include "global.h" #include "terminal.h" diff --git a/src/code/z_collision_check.c b/src/code/z_collision_check.c index a475a09121..1680d9066b 100644 --- a/src/code/z_collision_check.c +++ b/src/code/z_collision_check.c @@ -12,8 +12,6 @@ typedef s32 (*ColChkLineFunc)(PlayState*, CollisionCheckContext*, Collider*, Vec #define SAC_ENABLE (1 << 0) -#pragma increment_block_number "gc-eu:64 gc-eu-mq:64 gc-jp:64 gc-jp-ce:64 gc-jp-mq:64 gc-us:64 gc-us-mq:64" - #if OOT_DEBUG /** * Draws a red triangle with vertices vA, vB, and vC. diff --git a/src/code/z_play.c b/src/code/z_play.c index d9b89db124..ee39e07b74 100644 --- a/src/code/z_play.c +++ b/src/code/z_play.c @@ -1,3 +1,4 @@ + #include "global.h" #include "quake.h" #include "terminal.h" @@ -5,8 +6,6 @@ #include "z64frame_advance.h" -#pragma increment_block_number "gc-eu:252 gc-eu-mq:252 gc-jp:0 gc-jp-ce:0 gc-jp-mq:0 gc-us:0 gc-us-mq:0" - TransitionTile gTransitionTile; s32 gTransitionTileState; VisMono gPlayVisMono; diff --git a/src/overlays/actors/ovl_Fishing/z_fishing.c b/src/overlays/actors/ovl_Fishing/z_fishing.c index 4bb27c2a63..834f1aed79 100644 --- a/src/overlays/actors/ovl_Fishing/z_fishing.c +++ b/src/overlays/actors/ovl_Fishing/z_fishing.c @@ -14,7 +14,7 @@ #include "cic6105.h" #endif -#pragma increment_block_number "gc-eu:171 gc-eu-mq:171 gc-jp:173 gc-jp-ce:173 gc-jp-mq:173 gc-us:173 gc-us-mq:173" +#pragma increment_block_number "gc-eu:164 gc-eu-mq:164 gc-jp:166 gc-jp-ce:166 gc-jp-mq:166 gc-us:166 gc-us-mq:166" #define FLAGS ACTOR_FLAG_4 diff --git a/tools/audio/.gitignore b/tools/audio/.gitignore index 8a45b347ef..f0d3c612fd 100644 --- a/tools/audio/.gitignore +++ b/tools/audio/.gitignore @@ -1,4 +1,6 @@ __pycache__/ atblgen +sfpatch sbc +sfc diff --git a/tools/audio/Makefile b/tools/audio/Makefile index 9b859a84c3..e93bb158c1 100644 --- a/tools/audio/Makefile +++ b/tools/audio/Makefile @@ -1,4 +1,4 @@ -PROGRAMS := atblgen sbc +PROGRAMS := atblgen sfpatch sbc sfc ifeq ($(shell which xml2-config),) $(error xml2-config not found. Did you install libxml2-dev?) @@ -30,14 +30,18 @@ format: $(CLANG_FORMAT) $(FORMAT_ARGS) $(shell find . -maxdepth 1 -type f -name "*.[ch]") $(MAKE) -C sampleconv format -atblgen_SOURCES := audio_tablegen.c samplebank.c xml.c util.c -sbc_SOURCES := samplebank_compiler.c samplebank.c aifc.c xml.c util.c +atblgen_SOURCES := audio_tablegen.c samplebank.c soundfont.c xml.c util.c +sfpatch_SOURCES := sfpatch.c util.c +sbc_SOURCES := samplebank_compiler.c samplebank.c aifc.c xml.c util.c +sfc_SOURCES := soundfont_compiler.c samplebank.c soundfont.c aifc.c xml.c util.c atblgen_CFLAGS := $(XML_CFLAGS) sbc_CFLAGS := $(XML_CFLAGS) +sfc_CFLAGS := $(XML_CFLAGS) atblgen_LDFLAGS := $(XML_LDFLAGS) sbc_LDFLAGS := $(XML_LDFLAGS) +sfc_LDFLAGS := $(XML_LDFLAGS) define COMPILE = $(1): $($1_SOURCES) diff --git a/tools/audio/aifc.c b/tools/audio/aifc.c index 6ab7433b79..fcf83f8702 100644 --- a/tools/audio/aifc.c +++ b/tools/audio/aifc.c @@ -16,11 +16,6 @@ #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) { \ @@ -486,7 +481,7 @@ aifc_read(aifc_data *af, const char *path, uint8_t *match_buf, size_t *match_buf long read_size = ftell(in) - start - 8; if (read_size > chunk_size) - error("overran chunk: %lu vs %u\n", read_size, chunk_size); + error("overran chunk: %lu vs %u", 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); diff --git a/tools/audio/aifc.h b/tools/audio/aifc.h index 3e9920fd09..1d32293c16 100644 --- a/tools/audio/aifc.h +++ b/tools/audio/aifc.h @@ -72,4 +72,9 @@ 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)) +#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)) + #endif diff --git a/tools/audio/audio_tablegen.c b/tools/audio/audio_tablegen.c index b9fba94cc5..e7ec98c8bb 100644 --- a/tools/audio/audio_tablegen.c +++ b/tools/audio/audio_tablegen.c @@ -15,6 +15,7 @@ #include #include "samplebank.h" +#include "soundfont.h" #include "xml.h" #include "util.h" @@ -51,7 +52,7 @@ tablegen_samplebanks(const char *sb_hdr_out, const char **samplebanks_paths, int xmlDocPtr document = xmlReadFile(path, NULL, XML_PARSE_NONET); if (document == NULL) - error("Could not read xml file \"%s\"\n", path); + error("Could not read xml file \"%s\"", path); read_samplebank_xml(&samplebanks[i], document); } @@ -122,7 +123,7 @@ tablegen_samplebanks(const char *sb_hdr_out, const char **samplebanks_paths, int for (size_t i = 0; i < indices_len; i++) { if (index_info[i].index_type == INDEX_NONE) - error("Missing samplebank index %lu", i); + error("No samplebank for index %lu", i); } // Emit the table @@ -164,6 +165,131 @@ tablegen_samplebanks(const char *sb_hdr_out, const char **samplebanks_paths, int return EXIT_SUCCESS; } +/* Soundfonts */ + +static int +validate_samplebank_index(soundfont *sf, samplebank *sb, int ptr_idx) +{ + if (ptr_idx != -1) { + // Validate pointer index + bool found = false; + + for (size_t i = 0; i < sb->num_pointers; i++) { + if (ptr_idx == sb->pointer_indices[i]) { + found = true; + break; + } + } + if (!found) + warning("In Soundfont %s: Invalid pointer indirect %d for samplebank %s", sf->info.name, ptr_idx, sb->name); + + return ptr_idx; + } else { + return sb->index; + } +} + +int +tablegen_soundfonts(const char *sf_hdr_out, char **soundfonts_paths, int num_soundfont_files) +{ + soundfont *soundfonts = malloc(num_soundfont_files * sizeof(soundfont)); + int max_index = 0; + + for (int i = 0; i < num_soundfont_files; i++) { + char *path = soundfonts_paths[i]; + + if (!is_xml(path)) + error("Not an xml file? (\"%s\")", path); + + xmlDocPtr document = xmlReadFile(path, NULL, XML_PARSE_NONET); + if (document == NULL) + error("Could not read xml file \"%s\"", path); + + xmlNodePtr root = xmlDocGetRootElement(document); + if (!strequ(XMLSTR_TO_STR(root->name), "Soundfont")) + error("Root node must be "); + + soundfont *sf = &soundfonts[i]; + + // Transform the xml path into a header include path + // Assumption: replacing .xml -> .h forms a valid header include path + size_t pathlen = strlen(path); + path[pathlen - 3] = 'h'; + path[pathlen - 2] = '\0'; + + read_soundfont_info(sf, root); + + if (max_index < sf->info.index) + max_index = sf->info.index; + } + + struct soundfont_file_info { + soundfont *soundfont; + int normal_bank_index; + int dd_bank_index; + char *name; + }; + struct soundfont_file_info *finfo = calloc(max_index + 1, sizeof(struct soundfont_file_info)); + + for (int i = 0; i < num_soundfont_files; i++) { + soundfont *sf = &soundfonts[i]; + + // Resolve samplebank indices + + int normal_idx = validate_samplebank_index(sf, &sf->sb, sf->info.pointer_index); + + int dd_idx = 255; + if (sf->info.bank_path_dd != NULL) + dd_idx = validate_samplebank_index(sf, &sf->sbdd, sf->info.pointer_index_dd); + + // Add info + + if (finfo[sf->info.index].soundfont != NULL) + error("Overlapping soundfont indices, saw index %u more than once", sf->info.index); + + finfo[sf->info.index].soundfont = &soundfonts[i]; + finfo[sf->info.index].normal_bank_index = normal_idx; + finfo[sf->info.index].dd_bank_index = dd_idx; + finfo[sf->info.index].name = soundfonts_paths[i]; + } + + // Make sure there are no gaps + for (int i = 0; i < max_index + 1; i++) { + if (finfo[i].soundfont == NULL) + error("No soundfont for index %d", i); + } + + FILE *out = fopen(sf_hdr_out, "w"); + + fprintf(out, + // clang-format off + "/**" "\n" + " * DEFINE_SOUNDFONT(name, medium, cachePolicy, sampleBankNormal, " + "sampleBankDD, nInstruments, nDrums, nSfx)" "\n" + " */" "\n" + // clang-format on + ); + + for (int i = 0; i < max_index + 1; i++) { + soundfont *sf = finfo[i].soundfont; + + fprintf(out, + // clang-format off + "#include \"%s\"" "\n" + "DEFINE_SOUNDFONT(%s, %s, %s, %d, %d, SF%d_NUM_INSTRUMENTS, SF%d_NUM_DRUMS, SF%d_NUM_SFX)" "\n", + // clang-format on + finfo[i].name, sf->info.name, sf->info.medium, sf->info.cache_policy, finfo[i].normal_bank_index, + finfo[i].dd_bank_index, sf->info.index, sf->info.index, sf->info.index); + } + + fclose(out); + + free(soundfonts); + free(finfo); + + return EXIT_SUCCESS; +} + /* Common */ static int @@ -173,9 +299,10 @@ usage(const char *progname) // clang-format off "%s: Generate code tables for audio data" "\n" "Usage:" "\n" - " %s --banks " "\n", + " %s --banks " "\n" + " %s --fonts " "\n", // clang-format on - progname, progname); + progname, progname, progname); return EXIT_FAILURE; } @@ -200,6 +327,15 @@ main(int argc, char **argv) int num_samplebank_files = argc - 3; ret = tablegen_samplebanks(sb_hdr_out, samplebanks_paths, num_samplebank_files); + } else if (strequ(mode, "--fonts")) { + if (argc < 4) + return usage(progname); + + const char *sf_hdr_out = argv[2]; + char **soundfonts_paths = &argv[3]; + int num_soundfont_files = argc - 3; + + ret = tablegen_soundfonts(sf_hdr_out, soundfonts_paths, num_soundfont_files); } else { return usage(progname); } diff --git a/tools/audio/elf32.h b/tools/audio/elf32.h new file mode 100644 index 0000000000..109be5a2ed --- /dev/null +++ b/tools/audio/elf32.h @@ -0,0 +1,235 @@ +/* SPDX-FileCopyrightText: Copyright (C) 2024 ZeldaRET */ +/* SPDX-License-Identifier: CC0-1.0 */ +#ifndef ELF32_H_ +#define ELF32_H_ + +#include + +#include "util.h" + +#define elf32_read16(x) be16toh(x) +#define elf32_write16(x) htobe16(x) +#define elf32_read32(x) be32toh(x) +#define elf32_write32(x) htobe32(x) + +#ifndef ELF32_QUALIFIERS +#define ELF32_QUALIFIERS static UNUSED ALWAYS_INLINE +#endif + +#define GET_PTR(data, offset) ((void *)&((uint8_t *)(data))[(offset)]) + +#define EI_NIDENT 16 +#define EI_MAG0 0x00 +#define EI_MAG1 0x01 +#define EI_MAG2 0x02 +#define EI_MAG3 0x03 +#define EI_CLASS 0x04 +#define EI_DATA 0x05 +#define EI_VERSION 0x06 +#define EI_OSABI 0x07 +#define EI_ABIVERSION 0x08 +#define EI_PAD 0x09 + +typedef struct { + uint8_t e_ident[EI_NIDENT]; + uint16_t e_type; + uint16_t e_machine; + uint32_t e_version; + uint32_t e_entry; + uint32_t e_phoff; + uint32_t e_shoff; + uint32_t e_flags; + uint16_t e_ehsize; + uint16_t e_phentsize; + uint16_t e_phnum; + uint16_t e_shentsize; + uint16_t e_shnum; + uint16_t e_shstrndx; +} Elf32_Ehdr; + +#define ELF32_HAS_MAGIC(ehdr) \ + ((ehdr)->e_ident[EI_MAG0] == '\x7F' && (ehdr)->e_ident[EI_MAG1] == 'E' && (ehdr)->e_ident[EI_MAG2] == 'L' && \ + (ehdr)->e_ident[EI_MAG3] == 'F') + +#define ELF32_IS_32(ehdr) ((ehdr)->e_ident[EI_CLASS] == 1 /*EI_CLASS_32*/) + +#define ELF32_IS_BE(ehdr) ((ehdr)->e_ident[EI_DATA] == 2 /*EI_DATA_BE*/) + +typedef struct { + uint32_t sh_name; + uint32_t sh_type; + uint32_t sh_flags; + uint32_t sh_addr; + uint32_t sh_offset; + uint32_t sh_size; + uint32_t sh_link; + uint32_t sh_info; + uint32_t sh_addralign; + uint32_t sh_entsize; +} Elf32_Shdr; + +typedef struct { + uint32_t st_name; + uint32_t st_value; + uint32_t st_size; + uint8_t st_info; + uint8_t st_other; + uint16_t st_shndx; +} Elf32_Sym; + +// sh_type + +#define SHT_NULL 0x00000000 +#define SHT_PROGBITS 0x00000001 +#define SHT_SYMTAB 0x00000002 +#define SHT_STRTAB 0x00000003 +#define SHT_RELA 0x00000004 +#define SHT_HASH 0x00000005 +#define SHT_DYNAMIC 0x00000006 +#define SHT_NOTE 0x00000007 +#define SHT_NOBITS 0x00000008 +#define SHT_REL 0x00000009 +#define SHT_SHLIB 0x0000000A +#define SHT_DYNSYM 0x0000000B +#define SHT_INIT_ARRAY 0x0000000E +#define SHT_FINI_ARRAY 0x0000000F +#define SHT_PREINIT_ARRAY 0x00000010 +#define SHT_GROUP 0x00000011 +#define SHT_SYMTAB_SHNDX 0x00000012 +#define SHT_NUM 0x00000013 +#define SHT_LOOS 0x60000000 +// MIPS specific +#define SHT_MIPS_DEBUG 0x70000005 +#define SHT_MIPS_REGINFO 0x70000006 +#define SHT_MIPS_OPTIONS 0x7000000D + +// st_shndx + +#define SHN_UND 0x0000 +#define SHN_ABS 0xFFF1 +#define SHN_COMMON 0xFFF2 +#define SHN_LORESERVE 0xFF00 +#define SHN_XINDEX 0xFFFF + +// st_info [3:0] + +#define ST_NOTYPE 0 +#define ST_OBJECT 1 +#define ST_FUNC 2 +#define ST_SECTION 3 +#define ST_FILE 4 + +// st_info [7:4] + +#define SB_LOCAL 0 +#define SB_GLOBAL 1 +#define SB_WEAK 2 + +#define ELF32_ERR_PREFIX "[ELF32] " + +ELF32_QUALIFIERS void +validate_read(size_t offset, size_t size, size_t data_size) +{ + if (offset + size > data_size) + error(ELF32_ERR_PREFIX "Could not read %ld bytes at %08lX", size, offset); +} + +ELF32_QUALIFIERS void * +elf32_read(const char *path, size_t *data_size_out) +{ + size_t data_size; + void *data = util_read_whole_file(path, &data_size); + if (data == NULL) + error(ELF32_ERR_PREFIX "File is empty?"); + + validate_read(0, sizeof(Elf32_Ehdr), data_size); + + Elf32_Ehdr *ehdr = GET_PTR(data, 0); + + if (!ELF32_HAS_MAGIC(ehdr)) + error(ELF32_ERR_PREFIX "Not an ELF file?"); + if (!ELF32_IS_32(ehdr)) + error(ELF32_ERR_PREFIX "Not ELF32?"); + if (!ELF32_IS_BE(ehdr)) + error(ELF32_ERR_PREFIX "Not big-endian?"); + + *data_size_out = data_size; + return data; +} + +ELF32_QUALIFIERS Elf32_Shdr * +elf32_get_symtab(void *data, size_t data_size) +{ + Elf32_Ehdr *ehdr = GET_PTR(data, 0); + uint32_t e_shoff = elf32_read32(ehdr->e_shoff); + uint16_t e_shnum = elf32_read16(ehdr->e_shnum); + + Elf32_Shdr *shdr = GET_PTR(data, e_shoff); + for (size_t i = 0; i < e_shnum; i++, shdr++) { + validate_read(e_shoff + i * sizeof(Elf32_Shdr), sizeof(Elf32_Shdr), data_size); + + if (elf32_read32(shdr->sh_type) == SHT_SYMTAB) { + // there should be only one section of this type + return shdr; + } + } + return NULL; +} + +ELF32_QUALIFIERS Elf32_Shdr * +ef32_section_foridx(size_t idx, void *data, size_t data_size) +{ + Elf32_Ehdr *ehdr = GET_PTR(data, 0); + uint32_t e_shoff = elf32_read32(ehdr->e_shoff); + uint16_t e_shnum = elf32_read16(ehdr->e_shnum); + Elf32_Shdr *shdr = GET_PTR(data, e_shoff); + + if (idx >= e_shnum) + return NULL; + + validate_read(e_shoff + idx * sizeof(Elf32_Shdr), sizeof(Elf32_Shdr), data_size); + return &shdr[idx]; +} + +ELF32_QUALIFIERS Elf32_Shdr * +elf32_get_shstrtab(void *data, size_t data_size) +{ + Elf32_Ehdr *ehdr = GET_PTR(data, 0); + return ef32_section_foridx(elf32_read16(ehdr->e_shstrndx), data, data_size); +} + +ELF32_QUALIFIERS const char * +elf32_get_string(size_t offset, Elf32_Shdr *strtab, void *data, size_t data_size) +{ + uint32_t sh_offset = elf32_read32(strtab->sh_offset); + + validate_read(sh_offset + offset, 1, data_size); + return (const char *)GET_PTR(data, sh_offset + offset); +} + +ELF32_QUALIFIERS Elf32_Shdr * +elf32_section_forname(const char *name, Elf32_Shdr *shstrtab, void *data, size_t data_size) +{ + Elf32_Ehdr *ehdr = GET_PTR(data, 0); + uint32_t e_shoff = elf32_read32(ehdr->e_shoff); + uint16_t e_shnum = elf32_read16(ehdr->e_shnum); + + Elf32_Shdr *shdr = GET_PTR(data, e_shoff); + for (size_t i = 0; i < e_shnum; i++, shdr++) { + validate_read(e_shoff + i * sizeof(Elf32_Shdr), sizeof(Elf32_Shdr), data_size); + + const char *s_name = elf32_get_string(elf32_read32(shdr->sh_name), shstrtab, data, data_size); + if (strequ(s_name, name)) { + return shdr; + } + } + return NULL; +} + +ELF32_QUALIFIERS Elf32_Shdr * +elf32_get_strtab(void *data, size_t data_size) +{ + return elf32_section_forname(".strtab", elf32_get_shstrtab(data, data_size), data, data_size); +} + +#endif diff --git a/tools/audio/sampleconv/src/codec/vadpcm.c b/tools/audio/sampleconv/src/codec/vadpcm.c index a77c4948c8..8765fa02cf 100644 --- a/tools/audio/sampleconv/src/codec/vadpcm.c +++ b/tools/audio/sampleconv/src/codec/vadpcm.c @@ -1252,7 +1252,7 @@ vadpcm_dec(container_data *ctnr, UNUSED const codec_spec *codec, const enc_dec_o assert(memcmp(input, encoded, frame_size) == 0); } else { fails++; - error("FAIL [%d/%d]\n", cur_pos, nSamples); + error("FAIL [%d/%d]", cur_pos, nSamples); } // Bring the match closer to the original decode (not strictly @@ -1284,7 +1284,7 @@ vadpcm_dec(container_data *ctnr, UNUSED const codec_spec *codec, const enc_dec_o } if (fails != 0) - error("Decoding failures: %d\n", fails); + error("Decoding failures: %d", fails); // Convert VADPCM loop to regular loop, if it exists diff --git a/tools/audio/sampleconv/src/container/aiff.c b/tools/audio/sampleconv/src/container/aiff.c index d550e80055..7f824e016a 100644 --- a/tools/audio/sampleconv/src/container/aiff.c +++ b/tools/audio/sampleconv/src/container/aiff.c @@ -486,7 +486,7 @@ aiff_aifc_common_read(container_data *out, FILE *in, UNUSED bool matching, uint3 long read_size = ftell(in) - start - 8; if (read_size > chunk_size) - error("overran chunk: %lu vs %u\n", read_size, chunk_size); + error("overran chunk: %lu vs %u", 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); @@ -589,7 +589,7 @@ aiff_aifc_common_write(container_data *in, const char *path, bool aifc, bool mat { FILE *out = fopen(path, "wb"); if (out == NULL) - error("Failed to open %s for writing\n", path); + error("Failed to open %s for writing", path); const char *aifc_head = "FORM\0\0\0\0AIFC"; const char *aiff_head = "FORM\0\0\0\0AIFF"; diff --git a/tools/audio/sfpatch.c b/tools/audio/sfpatch.c new file mode 100644 index 0000000000..05029e0db0 --- /dev/null +++ b/tools/audio/sfpatch.c @@ -0,0 +1,57 @@ +/* SPDX-FileCopyrightText: Copyright (C) 2024 ZeldaRET */ +/* SPDX-License-Identifier: CC0-1.0 */ +#include +#include +#include +#include +#include +#include + +#include "elf32.h" +#include "util.h" + +/** + * Converts symbols defined in an ELF file to ABS symbols so their values remain + * unchanged after linking against them. This is used for soundfonts as references + * to symbols defined in the soundfont should remain file-relative even after the + * final link. + */ +int +main(int argc, char **argv) +{ + if (argc < 3) { + fprintf(stderr, "Usage: %s in.elf out.elf\n", argv[0]); + return EXIT_FAILURE; + } + + // read input elf file + + size_t data_size; + void *data = elf32_read(argv[1], &data_size); + + // locate symtab + + Elf32_Shdr *symtab = elf32_get_symtab(data, data_size); + if (symtab == NULL) + error("Symtab not found"); + + uint32_t sh_offset = elf32_read32(symtab->sh_offset); + uint32_t sh_size = elf32_read32(symtab->sh_size); + + // patch defined symbols to be ABS + + Elf32_Sym *sym = GET_PTR(data, sh_offset); + Elf32_Sym *sym_end = GET_PTR(data, sh_offset + sh_size); + + for (size_t i = 0; sym < sym_end; sym++, i++) { + validate_read(sh_offset + i * sizeof(Elf32_Sym), sizeof(Elf32_Sym), data_size); + + if (elf32_read16(sym->st_shndx) != SHN_UND) + sym->st_shndx = elf32_write16(SHN_ABS); + } + + // write output elf file + + util_write_whole_file(argv[2], data, data_size); + return EXIT_SUCCESS; +} diff --git a/tools/audio/soundfont.c b/tools/audio/soundfont.c new file mode 100644 index 0000000000..d664fdd238 --- /dev/null +++ b/tools/audio/soundfont.c @@ -0,0 +1,70 @@ +/** + * 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 "soundfont.h" +#include "xml.h" +#include "util.h" + +envelope_data * +sf_get_envelope(soundfont *sf, const char *name) +{ + LL_FOREACH(envelope_data *, env, sf->envelopes) { + if (env->points != NULL && strequ(name, env->name)) + return env; + } + return NULL; +} + +sample_data * +sample_data_forname(soundfont *sf, const char *name) +{ + LL_FOREACH(sample_data *, sample, sf->samples) { + if (strequ(sample->name, name)) + return sample; + } + return NULL; +} + +void +read_soundfont_info(soundfont *sf, xmlNodePtr node) +{ + static const xml_attr_spec spec = { + {"Name", false, xml_parse_c_identifier, offsetof(soundfont, info.name) }, + { "Index", false, xml_parse_int, offsetof(soundfont, info.index) }, + { "Medium", false, xml_parse_c_identifier, offsetof(soundfont, info.medium) }, + { "CachePolicy", false, xml_parse_c_identifier, offsetof(soundfont, info.cache_policy) }, + { "SampleBank", false, xml_parse_string, offsetof(soundfont, info.bank_path) }, + { "Indirect", true, xml_parse_int, offsetof(soundfont, info.pointer_index) }, + { "SampleBankDD", true, xml_parse_string, offsetof(soundfont, info.bank_path_dd) }, + { "IndirectDD", true, xml_parse_int, offsetof(soundfont, info.pointer_index_dd) }, + { "LoopsHaveFrames", true, xml_parse_bool, offsetof(soundfont, info.loops_have_frames)}, + { "NumInstruments", true, xml_parse_uint, offsetof(soundfont, info.num_instruments) }, + { "PadToSize", true, xml_parse_uint, offsetof(soundfont, info.pad_to_size) }, + }; + sf->info.num_instruments = 0; + sf->info.num_drums = 0; + sf->info.num_effects = 0; + sf->info.bank_path_dd = NULL; + sf->info.pointer_index = -1; + sf->info.pointer_index_dd = -1; + sf->info.loops_have_frames = false; + sf->info.pad_to_size = 0; + xml_parse_node_by_spec(sf, node, spec, ARRAY_COUNT(spec)); + + xmlDocPtr sb_doc = xmlReadFile(sf->info.bank_path, NULL, XML_PARSE_NONET); + if (sb_doc == NULL) + error("Failed to read sample bank xml file \"%s\"", sf->info.bank_path); + read_samplebank_xml(&sf->sb, sb_doc); + + if (sf->info.bank_path_dd != NULL) { + xmlDocPtr sbdd_doc = xmlReadFile(sf->info.bank_path_dd, NULL, XML_PARSE_NONET); + if (sbdd_doc == NULL) + error("Failed to read sample bank xml file \"%s\"", sf->info.bank_path); + read_samplebank_xml(&sf->sbdd, sbdd_doc); + } +} diff --git a/tools/audio/soundfont.h b/tools/audio/soundfont.h new file mode 100644 index 0000000000..eeed712de2 --- /dev/null +++ b/tools/audio/soundfont.h @@ -0,0 +1,170 @@ +/** + * 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 SOUNDFONT_H_ +#define SOUNDFONT_H_ + +#include + +#include "aifc.h" +#include "samplebank.h" + +// special delay values +#define ADSR_DISABLE 0 +#define ADSR_HANG -1 +#define ADSR_GOTO -2 +#define ADSR_RESTART -3 + +#define INSTR_LO_NONE 0 +#define INSTR_HI_NONE 127 + +typedef struct sample_data { + struct sample_data *next; + + const char *name; + double sample_rate; + int8_t base_note; + bool is_dd; + bool cached; + aifc_data aifc; +} sample_data; + +typedef struct { + int16_t delay; + int16_t arg; +} envelope_point; + +typedef struct envelope_data { + struct envelope_data *next; + + const char *name; + uint8_t release; + envelope_point *points; + size_t n_points; + bool used; +} envelope_data; + +typedef struct instr_data { + struct instr_data *next; + + unsigned int program_number; + const char *name; + const char *envelope_name; + + // for matching only + bool unused; + + // these are provided as-is for unused (name == NULL) otherwise they are read from the aifc file + double sample_rate_mid; + double sample_rate_lo; + double sample_rate_hi; + int8_t base_note_mid; + int8_t base_note_lo; + int8_t base_note_hi; + + envelope_data *envelope; + uint16_t release; + const char *sample_name_low; + const char *sample_name_mid; + const char *sample_name_high; + int8_t sample_low_end; + int8_t sample_high_start; + sample_data *sample_low; + sample_data *sample_mid; + sample_data *sample_high; + float sample_low_tuning; + float sample_mid_tuning; + float sample_high_tuning; +} instr_data; + +typedef struct drum_data { + struct drum_data *next; + + const char *name; + const char *sample_name; + const char *envelope_name; + envelope_data *envelope; + uint16_t release; + int8_t note; + int8_t note_start; + int8_t note_end; + int pan; + sample_data *sample; + double sample_rate; + int8_t base_note; +} drum_data; + +typedef struct sfx_data { + struct sfx_data *next; + + const char *name; + const char *sample_name; + sample_data *sample; + double sample_rate; + int8_t base_note; + float tuning; +} sfx_data; + +typedef struct { + bool matching; + + struct { + const char *name; + const char *symbol; + int index; + const char *medium; + const char *cache_policy; + const char *bank_path; + int pointer_index; + const char *bank_path_dd; + int pointer_index_dd; + unsigned int pad_to_size; + unsigned int num_instruments; // or the maximum program number (+1), since this also includes empty slots + // between instruments + unsigned int num_drums; + unsigned int num_effects; + bool loops_have_frames; + } info; + + uint32_t program_number_bitset[4]; + + envelope_data *envelopes; + envelope_data *envelope_last; + + samplebank sb; + samplebank sbdd; + + sample_data *samples; + sample_data *sample_last; + + instr_data *instruments; + instr_data *instrument_last; + + drum_data *drums; + drum_data *drums_last; + + sfx_data *sfx; + sfx_data *sfx_last; + + uint8_t *match_padding; + size_t match_padding_num; +} soundfont; + +#define NOTE_UNSET (INT8_MIN) +#define RELEASE_UNSET (UINT16_MAX) + +envelope_data * +sf_get_envelope(soundfont *sf, const char *name); + +sample_data * +sample_data_forname(soundfont *sf, const char *name); + +void +read_soundfont_info(soundfont *sf, xmlNodePtr node); + +#endif diff --git a/tools/audio/soundfont_compiler.c b/tools/audio/soundfont_compiler.c new file mode 100644 index 0000000000..efa326507f --- /dev/null +++ b/tools/audio/soundfont_compiler.c @@ -0,0 +1,1814 @@ +/** + * 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 +#include + +#include "xml.h" +#include "aifc.h" +#include "samplebank.h" +#include "soundfont.h" +#include "util.h" + +static_assert(sizeof(float) == sizeof(uint32_t), "Float is assumed to be 32-bit"); + +static float +i2f(uint32_t i) +{ + union { + float f; + uint32_t i; + } fi; + + fi.i = i; + return fi.f; +} + +static uint32_t +f2i(float f) +{ + union { + float f; + uint32_t i; + } fi; + + fi.f = f; + return fi.i; +} + +static int +midinote_to_z64note(int note) +{ + // Converts from MIDI note number (middle C = 60) to Z64 note number (middle C = 39) + int z64note = note - 21; + if (z64note < 0) // % 128 + z64note += 128; + return z64note; +} + +/** + * Calculate the tuning value from a given samplerate and basenote. + * + * Uses a lookup table (gPitchFrequencies from the audio driver source) to compute the result of `2^(basenote / 12)` + * (with appropriate shifting such that the index for C4 results in 1.0) + */ +static float +calc_tuning(float sample_rate, int basenote) +{ + static const float playback_sample_rate = 32000.0f; // Target samplerate in-game is 32KHz + static const float pitch_frequencies[] = { + // gPitchFrequencies in audio driver source + /* 0x00 */ 0.105112f, // PITCH_A0 + /* 0x01 */ 0.111362f, // PITCH_BF0 + /* 0x02 */ 0.117984f, // PITCH_B0 + /* 0x03 */ 0.125f, // PITCH_C1 + /* 0x04 */ 0.132433f, // PITCH_DF1 + /* 0x05 */ 0.140308f, // PITCH_D1 + /* 0x06 */ 0.148651f, // PITCH_EF1 + /* 0x07 */ 0.15749f, // PITCH_E1 + /* 0x08 */ 0.166855f, // PITCH_F1 + /* 0x09 */ 0.176777f, // PITCH_GF1 + /* 0x0A */ 0.187288f, // PITCH_G1 + /* 0x0B */ 0.198425f, // PITCH_AF1 + /* 0x0C */ 0.210224f, // PITCH_A1 + /* 0x0D */ 0.222725f, // PITCH_BF1 + /* 0x0E */ 0.235969f, // PITCH_B1 + /* 0x0F */ 0.25f, // PITCH_C2 + /* 0x10 */ 0.264866f, // PITCH_DF2 + /* 0x11 */ 0.280616f, // PITCH_D2 + /* 0x12 */ 0.297302f, // PITCH_EF2 + /* 0x13 */ 0.31498f, // PITCH_E2 + /* 0x14 */ 0.33371f, // PITCH_F2 + /* 0x15 */ 0.353553f, // PITCH_GF2 + /* 0x16 */ 0.374577f, // PITCH_G2 + /* 0x17 */ 0.39685f, // PITCH_AF2 + /* 0x18 */ 0.420448f, // PITCH_A2 + /* 0x19 */ 0.445449f, // PITCH_BF2 + /* 0x1A */ 0.471937f, // PITCH_B2 + /* 0x1B */ 0.5f, // PITCH_C3 + /* 0x1C */ 0.529732f, // PITCH_DF3 + /* 0x1D */ 0.561231f, // PITCH_D3 + /* 0x1E */ 0.594604f, // PITCH_EF3 + /* 0x1F */ 0.629961f, // PITCH_E3 + /* 0x20 */ 0.66742f, // PITCH_F3 + /* 0x21 */ 0.707107f, // PITCH_GF3 + /* 0x22 */ 0.749154f, // PITCH_G3 + /* 0x23 */ 0.793701f, // PITCH_AF3 + /* 0x24 */ 0.840897f, // PITCH_A3 + /* 0x25 */ 0.890899f, // PITCH_BF3 + /* 0x26 */ 0.943875f, // PITCH_B3 + /* 0x27 */ 1.0f, // PITCH_C4 (Middle C) + /* 0x28 */ 1.059463f, // PITCH_DF4 + /* 0x29 */ 1.122462f, // PITCH_D4 + /* 0x2A */ 1.189207f, // PITCH_EF4 + /* 0x2B */ 1.259921f, // PITCH_E4 + /* 0x2C */ 1.33484f, // PITCH_F4 + /* 0x2D */ 1.414214f, // PITCH_GF4 + /* 0x2E */ 1.498307f, // PITCH_G4 + /* 0x2F */ 1.587401f, // PITCH_AF4 + /* 0x30 */ 1.681793f, // PITCH_A4 + /* 0x31 */ 1.781798f, // PITCH_BF4 + /* 0x32 */ 1.887749f, // PITCH_B4 + /* 0x33 */ 2.0f, // PITCH_C5 + /* 0x34 */ 2.118926f, // PITCH_DF5 + /* 0x35 */ 2.244924f, // PITCH_D5 + /* 0x36 */ 2.378414f, // PITCH_EF5 + /* 0x37 */ 2.519842f, // PITCH_E5 + /* 0x38 */ 2.66968f, // PITCH_F5 + /* 0x39 */ 2.828428f, // PITCH_GF5 + /* 0x3A */ 2.996615f, // PITCH_G5 + /* 0x3B */ 3.174803f, // PITCH_AF5 + /* 0x3C */ 3.363586f, // PITCH_A5 + /* 0x3D */ 3.563596f, // PITCH_BF5 + /* 0x3E */ 3.775498f, // PITCH_B5 + /* 0x3F */ 4.0f, // PITCH_C6 + /* 0x40 */ 4.237853f, // PITCH_DF6 + /* 0x41 */ 4.489849f, // PITCH_D6 + /* 0x42 */ 4.756829f, // PITCH_EF6 + /* 0x43 */ 5.039685f, // PITCH_E6 + /* 0x44 */ 5.33936f, // PITCH_F6 + /* 0x45 */ 5.656855f, // PITCH_GF6 + /* 0x46 */ 5.993229f, // PITCH_G6 + /* 0x47 */ 6.349606f, // PITCH_AF6 + /* 0x48 */ 6.727173f, // PITCH_A6 + /* 0x49 */ 7.127192f, // PITCH_BF6 + /* 0x4A */ 7.550996f, // PITCH_B6 + /* 0x4B */ 8.0f, // PITCH_C7 + /* 0x4C */ 8.475705f, // PITCH_DF7 + /* 0x4D */ 8.979697f, // PITCH_D7 + /* 0x4E */ 9.513658f, // PITCH_EF7 + /* 0x4F */ 10.07937f, // PITCH_E7 + /* 0x50 */ 10.6787205f, // PITCH_F7 + /* 0x51 */ 11.31371f, // PITCH_GF7 + /* 0x52 */ 11.986459f, // PITCH_G7 + /* 0x53 */ 12.699211f, // PITCH_AF7 + /* 0x54 */ 13.454346f, // PITCH_A7 + /* 0x55 */ 14.254383f, // PITCH_BF7 + /* 0x56 */ 15.101993f, // PITCH_B7 + /* 0x57 */ 16.0f, // PITCH_C8 + /* 0x58 */ 16.95141f, // PITCH_DF8 + /* 0x59 */ 17.959395f, // PITCH_D8 + /* 0x5A */ 19.027315f, // PITCH_EF8 + /* 0x5B */ 20.15874f, // PITCH_E8 + /* 0x5C */ 21.35744f, // PITCH_F8 + /* 0x5D */ 22.62742f, // PITCH_GF8 + /* 0x5E */ 23.972918f, // PITCH_G8 + /* 0x5F */ 25.398422f, // PITCH_AF8 + /* 0x60 */ 26.908691f, // PITCH_A8 + /* 0x61 */ 28.508766f, // PITCH_BF8 + /* 0x62 */ 30.203985f, // PITCH_B8 + /* 0x63 */ 32.0f, // PITCH_C9 + /* 0x64 */ 33.90282f, // PITCH_DF9 + /* 0x65 */ 35.91879f, // PITCH_D9 + /* 0x66 */ 38.05463f, // PITCH_EF9 + /* 0x67 */ 40.31748f, // PITCH_E9 + /* 0x68 */ 42.71488f, // PITCH_F9 + /* 0x69 */ 45.25484f, // PITCH_GF9 + /* 0x6A */ 47.945835f, // PITCH_G9 + /* 0x6B */ 50.796845f, // PITCH_AF9 + /* 0x6C */ 53.817383f, // PITCH_A9 + /* 0x6D */ 57.017532f, // PITCH_BF9 + /* 0x6E */ 60.40797f, // PITCH_B9 + /* 0x6F */ 64.0f, // PITCH_C10 + /* 0x70 */ 67.80564f, // PITCH_DF10 + /* 0x71 */ 71.83758f, // PITCH_D10 + /* 0x72 */ 76.10926f, // PITCH_EF10 + /* 0x73 */ 80.63496f, // PITCH_E10 + /* 0x74 */ 85.42976f, // PITCH_F10 + /* 0x75 */ 0.055681f, // PITCH_BFNEG1 + /* 0x76 */ 0.058992f, // PITCH_BNEG1 + /* 0x77 */ 0.0625f, // PITCH_C0 + /* 0x78 */ 0.066216f, // PITCH_DF0 + /* 0x79 */ 0.070154f, // PITCH_D0 + /* 0x7A */ 0.074325f, // PITCH_EF0 + /* 0x7B */ 0.078745f, // PITCH_E0 + /* 0x7C */ 0.083427f, // PITCH_F0 + /* 0x7D */ 0.088388f, // PITCH_GF0 + /* 0x7E */ 0.093644f, // PITCH_G0 + /* 0x7F */ 0.099213f, // PITCH_AF0 + }; + + return (sample_rate / playback_sample_rate) * pitch_frequencies[basenote]; +} + +void +read_envelopes_info(soundfont *sf, xmlNodePtr envelopes) +{ + static const xml_attr_spec spec_env = { + {"Name", false, xml_parse_c_identifier, offsetof(envelope_data, name) }, + { "Release", false, xml_parse_u8, offsetof(envelope_data, release)}, + }; + static const xml_attr_spec spec_env_pt = { + {"Delay", false, xml_parse_s16, offsetof(envelope_point, delay)}, + { "Arg", false, xml_parse_s16, offsetof(envelope_point, arg) }, + }; + static const xml_attr_spec spec_env_goto = { + {"Index", false, xml_parse_s16, offsetof(envelope_point, arg)}, + }; + + LL_FOREACH(xmlNodePtr, env, envelopes->children) { + if (env->type != XML_ELEMENT_NODE) + continue; + + const char *name = XMLSTR_TO_STR(env->name); + if (!strequ(name, "Envelope")) + error("Unexpected element node %s in envelopes list (line %d)", name, env->line); + + envelope_data *envdata; + + if (env->children == NULL) { + // Empty envelopes for mm + envdata = (envelope_data *)malloc(sizeof(envelope_data)); + envdata->name = NULL; + envdata->points = NULL; + envdata->release = 0; + envdata->n_points = 0; + } else { + size_t points_cap = 4; + size_t points_num = 0; + + void *envelopes_data = malloc(sizeof(envelope_data) + points_cap * sizeof(envelope_point)); + envdata = (envelope_data *)envelopes_data; + + xml_parse_node_by_spec(envdata, env, spec_env, ARRAY_COUNT(spec_env)); + + // Ensure name is unique + LL_FOREACH(envelope_data *, envdata2, sf->envelopes) { + if (envdata2->name != NULL && strequ(envdata->name, envdata2->name)) + error("Duplicate envelope name %s (second occurrence on line %d)", envdata->name, env->line); + } + + envelope_point *pts = (envelope_point *)(envdata + 1); + + LL_FOREACH(xmlNodePtr, env_pt, env->children) { + if (points_num >= points_cap) { + points_cap *= 2; + envelopes_data = + realloc(envelopes_data, sizeof(envelope_data) + points_cap * sizeof(envelope_point)); + envdata = (envelope_data *)envelopes_data; + pts = (envelope_point *)(envdata + 1); + } + + envelope_point *pt = &pts[points_num]; + + if (env_pt->type != XML_ELEMENT_NODE) + continue; + + const char *pt_name = XMLSTR_TO_STR(env_pt->name); + + if (strequ(pt_name, "Point")) { + xml_parse_node_by_spec(pt, env_pt, spec_env_pt, ARRAY_COUNT(spec_env_pt)); + } else if (strequ(pt_name, "Disable")) { + pt->delay = ADSR_DISABLE; + pt->arg = 0; + } else if (strequ(pt_name, "Goto")) { + pt->delay = ADSR_GOTO; + xml_parse_node_by_spec(pt, env_pt, spec_env_goto, ARRAY_COUNT(spec_env_goto)); + } else if (strequ(pt_name, "Restart")) { + pt->delay = ADSR_RESTART; + pt->arg = 0; + } else if (strequ(pt_name, "Hang")) { + pt->delay = ADSR_HANG; + pt->arg = 0; + // TODO force end here and don't emit an extra hang + } else { + error("Unexpected element node %s in envelope definition (line %d)", name, env->line); + } + + points_num++; + } + envdata->points = pts; + envdata->n_points = points_num; + } + + envdata->used = false; + + // link + if (sf->envelopes == NULL) { + sf->envelopes = envdata; + sf->envelope_last = envdata; + } else { + sf->envelope_last->next = envdata; + sf->envelope_last = envdata; + } + envdata->next = NULL; + } +} + +void +read_instrs_info(soundfont *sf, xmlNodePtr instrs) +{ + static const xml_attr_spec instr_spec = { + {"ProgramNumber", true, xml_parse_uint, offsetof(instr_data, program_number) }, + { "Name", true, xml_parse_c_identifier, offsetof(instr_data, name) }, + { "Envelope", false, xml_parse_c_identifier, offsetof(instr_data, envelope_name) }, + { "Release", true, xml_parse_u8, offsetof(instr_data, release) }, + + { "Sample", true, xml_parse_c_identifier, offsetof(instr_data, sample_name_mid) }, + { "BaseNote", true, xml_parse_note_number, offsetof(instr_data, base_note_mid) }, + { "SampleRate", true, xml_parse_double, offsetof(instr_data, sample_rate_mid) }, + + { "RangeLo", true, xml_parse_note_number, offsetof(instr_data, sample_low_end) }, + { "SampleLo", true, xml_parse_c_identifier, offsetof(instr_data, sample_name_low) }, + { "BaseNoteLo", true, xml_parse_note_number, offsetof(instr_data, base_note_lo) }, + { "SampleRateLo", true, xml_parse_double, offsetof(instr_data, sample_rate_lo) }, + + { "RangeHi", true, xml_parse_note_number, offsetof(instr_data, sample_high_start)}, + { "SampleHi", true, xml_parse_c_identifier, offsetof(instr_data, sample_name_high) }, + { "BaseNoteHi", true, xml_parse_note_number, offsetof(instr_data, base_note_hi) }, + { "SampleRateHi", true, xml_parse_double, offsetof(instr_data, sample_rate_hi) }, + }; + + LL_FOREACH(xmlNodePtr, instr_node, instrs->children) { + if (instr_node->type != XML_ELEMENT_NODE) + continue; + + const char *name = XMLSTR_TO_STR(instr_node->name); + + bool is_instr = strequ(name, "Instrument"); + bool is_instr_unused = strequ(name, "InstrumentUnused"); + + if (!is_instr && !is_instr_unused) + error("Unexpected element node %s in instrument list (line %d)", name, instr_node->line); + + instr_data *instr = malloc(sizeof(instr_data)); + + instr->program_number = (unsigned)-1; + instr->name = NULL; + instr->sample_name_low = NULL; + instr->sample_name_mid = NULL; + instr->sample_name_high = NULL; + instr->sample_low_end = INSTR_LO_NONE; + instr->sample_low = NULL; + instr->sample_high_start = INSTR_HI_NONE; + instr->sample_high = NULL; + instr->base_note_mid = NOTE_UNSET; + instr->base_note_lo = NOTE_UNSET; + instr->base_note_hi = NOTE_UNSET; + instr->sample_rate_mid = -1.0; + instr->sample_rate_lo = -1.0; + instr->sample_rate_hi = -1.0; + instr->release = RELEASE_UNSET; + instr->unused = is_instr_unused; + + xml_parse_node_by_spec(instr, instr_node, instr_spec, ARRAY_COUNT(instr_spec)); + + if (!is_instr_unused) { + // Check program number, midi program number range is 0-127 but the audio driver reserves 126 and 127 for + // sfx and percussion so the range we allow is 0-125 + if (instr->program_number >= 126) + error("Program numbers must be in the range 0-125 (got %u on line %d)", instr->program_number, + instr_node->line); + + // Ensure program number is unique + unsigned upper = instr->program_number >> 5 & 3; + unsigned lower = instr->program_number & 0x1F; + if (sf->program_number_bitset[upper] & (1 << lower)) + error("Duplicate program number %u (second occurrence on line %d)", instr->program_number, + instr_node->line); + sf->program_number_bitset[upper] |= (1 << lower); + + if (instr->program_number >= sf->info.num_instruments) + sf->info.num_instruments = instr->program_number + 1; + + // Check name + if (instr->name == NULL) + error("Instrument must be named (line %d)", instr_node->line); + } + + // Check envelope + instr->envelope = sf_get_envelope(sf, instr->envelope_name); + if (instr->envelope == NULL) + error("Bad envelope name %s (line %d)", instr->envelope_name, instr_node->line); + + // Validate optionals + if (instr->release == RELEASE_UNSET) + instr->release = instr->envelope->release; + + if (instr->sample_name_mid == NULL) { + // For a used instrument to have no sample path, it must have sample children and have specified at least + // one of RangeLo or RangeHi + + if (instr->sample_low_end == INSTR_LO_NONE && instr->sample_high_start == INSTR_HI_NONE) + error("Instrument has no mid sample but also does not define a low or high sample (line %d)", + instr_node->line); + + if (instr_node->children == NULL) + error("Instrument sample list is empty, must specify at least one sample (line %d)", instr_node->line); + + bool seen_low = false; + bool seen_mid = false; + bool seen_high = false; + + LL_FOREACH(xmlNodePtr, instr_sample_node, instr_node->children) { + if (instr_sample_node->type != XML_ELEMENT_NODE) + continue; + + const char *name = XMLSTR_TO_STR(instr_sample_node->name); + if (!strequ(name, "Sample")) + error("Unexpected element node %s in instrument sample list (line %d)", name, + instr_sample_node->line); + + if (instr_sample_node->properties == NULL) + error("Expected a Low/Mid/High sample path (line %d)", instr_sample_node->line); + + xmlAttrPtr attr = instr_sample_node->properties; + if (attr->next != NULL) + error("Instrument sample should have exactly one attribute (line %d)", instr_sample_node->line); + + const char *attr_name = XMLSTR_TO_STR(attr->name); + + bool *seen; + const char **name_ptr; + + if (strequ(attr_name, "Low")) { + seen = &seen_low; + name_ptr = &instr->sample_name_low; + + if (instr->sample_low_end == INSTR_LO_NONE) + error("Useless Low sample specified (RangeLo is 0) (line %d)", instr_sample_node->line); + } else if (strequ(attr_name, "Mid")) { + seen = &seen_mid; + name_ptr = &instr->sample_name_mid; + } else if (strequ(attr_name, "High")) { + seen = &seen_high; + name_ptr = &instr->sample_name_high; + + if (instr->sample_high_start == INSTR_HI_NONE) + error("Useless High sample specified (RangeHi is 127) (line %d)", instr_sample_node->line); + } else { + error("Unexpected attribute name for instrument sample (line %d)", instr_sample_node->line); + } + + if (*seen) + error("Duplicate \"%s\" sample specifier in instrument sample (line %d)", attr_name, + instr_sample_node->line); + *seen = true; + + xmlChar *xvalue = xmlNodeListGetString(instr_sample_node->doc, attr->children, 1); + const char *value = XMLSTR_TO_STR(xvalue); + xml_parse_c_identifier(value, name_ptr); + } + + if (!seen_mid && instr->sample_low_end != instr->sample_high_start) + error("Unset-but-used Mid sample (line %d)", instr_node->line); + if (!seen_low && instr->sample_low_end != 0) + error("Unset-but-used Low sample (line %d)", instr_node->line); + if (!seen_high && instr->sample_high_start != 0) + error("Unset-but-used High sample (line %d)", instr_node->line); + } + + if (instr->sample_name_low != NULL) { + instr->sample_low = sample_data_forname(sf, instr->sample_name_low); + if (instr->sample_low == NULL) + error("Bad sample name \"%s\" for LOW sample (line %d). Is it defined in ?", + instr->sample_name_low, instr_node->line); + + if (instr->base_note_lo == NOTE_UNSET) + instr->base_note_lo = instr->sample_low->base_note; + + if (instr->sample_rate_lo < 0.0) + instr->sample_rate_lo = instr->sample_low->sample_rate; + + instr->sample_low_tuning = calc_tuning(instr->sample_rate_lo, instr->base_note_lo); + } + + instr->sample_mid = sample_data_forname(sf, instr->sample_name_mid); + if (instr->sample_mid == NULL) + error("Bad sample name \"%s\" for MID sample (line %d). Is it defined in ?", + instr->sample_name_mid, instr_node->line); + + if (instr->base_note_mid == NOTE_UNSET) + instr->base_note_mid = instr->sample_mid->base_note; + + if (instr->sample_rate_mid < 0.0) + instr->sample_rate_mid = instr->sample_mid->sample_rate; + + instr->sample_mid_tuning = calc_tuning(instr->sample_rate_mid, instr->base_note_mid); + + // Some tuning values don't decompose properly into a samplerate and basenote, they must be accounted for here + // for matching. So far this has only been seen for an Instrument mid sample. + // NOTE: Keep in sync with the BAD_FLOATS list in extraction/tuning.py + if (f2i(instr->sample_mid_tuning) == 0x3E7319DF /* 0.237403377 */) // diff = 2^-24 + instr->sample_mid_tuning = i2f(0x3E7319E3 /* 0.237403437 */); + + if (instr->sample_name_high != NULL) { + instr->sample_high = sample_data_forname(sf, instr->sample_name_high); + if (instr->sample_high == NULL) + error("Bad sample name \"%s\" for HIGH sample (line %d). Is it defined in ?", + instr->sample_name_high, instr_node->line); + + if (instr->base_note_hi == NOTE_UNSET) + instr->base_note_hi = instr->sample_high->base_note; + + if (instr->sample_rate_hi < 0.0) + instr->sample_rate_hi = instr->sample_high->sample_rate; + + instr->sample_high_tuning = calc_tuning(instr->sample_rate_hi, instr->base_note_hi); + } + + // link + if (sf->instruments == NULL) { + sf->instruments = instr; + sf->instrument_last = instr; + } else { + sf->instrument_last->next = instr; + sf->instrument_last = instr; + } + instr->next = NULL; + } +} + +void +read_drums_info(soundfont *sf, xmlNodePtr drums) +{ + static const xml_attr_spec drum_spec = { + {"Name", false, xml_parse_c_identifier, offsetof(drum_data, name) }, + { "Note", true, xml_parse_note_number, offsetof(drum_data, note) }, + { "NoteStart", true, xml_parse_note_number, offsetof(drum_data, note_start) }, + { "NoteEnd", true, xml_parse_note_number, offsetof(drum_data, note_end) }, + { "Pan", false, xml_parse_int, offsetof(drum_data, pan) }, + { "Envelope", false, xml_parse_c_identifier, offsetof(drum_data, envelope_name)}, + { "Release", true, xml_parse_u8, offsetof(drum_data, release) }, + { "Sample", false, xml_parse_c_identifier, offsetof(drum_data, sample_name) }, + { "SampleRate", true, xml_parse_double, offsetof(drum_data, sample_rate) }, + { "BaseNote", true, xml_parse_note_number, offsetof(drum_data, base_note) }, + }; + + LL_FOREACH(xmlNodePtr, drum_node, drums->children) { + if (drum_node->type != XML_ELEMENT_NODE) + continue; + + const char *name = XMLSTR_TO_STR(drum_node->name); + if (!strequ(name, "Drum")) + error("Unexpected element node %s in drums list (line %d)", name, drum_node->line); + + drum_data *drum = malloc(sizeof(drum_data)); + drum->note = NOTE_UNSET; + drum->note_start = NOTE_UNSET; + drum->note_end = NOTE_UNSET; + drum->sample_rate = -1; + drum->base_note = NOTE_UNSET; + drum->release = RELEASE_UNSET; + + if (drum_node->properties == NULL) { + // + drum->name = NULL; + drum->envelope = NULL; + drum->sample_name = NULL; + drum->sample = NULL; + goto link_drum; + } + + xml_parse_node_by_spec(drum, drum_node, drum_spec, ARRAY_COUNT(drum_spec)); + + drum->envelope = sf_get_envelope(sf, drum->envelope_name); + if (drum->envelope == NULL) + error("Bad envelope name %s (line %d)", drum->envelope_name, drum_node->line); + + // validate optionals + if (drum->release == RELEASE_UNSET) + drum->release = drum->envelope->release; + + if (drum->note == NOTE_UNSET) { + if (drum->note_start == NOTE_UNSET || drum->note_end == NOTE_UNSET) + error("Incomplete note range specification (line %d)", drum_node->line); + } else { + if (drum->note_start != NOTE_UNSET || drum->note_end != NOTE_UNSET) + error("Overspecified note range (line %d)", drum_node->line); + + drum->note_start = drum->note_end = drum->note; + } + + if (drum->note_end < drum->note_start) + error("Invalid drum note range: [%d - %d] (line %d)", drum->note_start, drum->note_end, drum_node->line); + + drum->sample = sample_data_forname(sf, drum->sample_name); + if (drum->sample == NULL) + error("Bad sample name \"%s\" (line %d). Is it defined in ?", drum->sample_name, drum_node->line); + + // set final samplerate if not overridden + if (drum->sample_rate == -1) { + drum->sample_rate = drum->sample->sample_rate; + } + + // set basenote if not overridden + if (drum->base_note == NOTE_UNSET) { + if (drum->sample->aifc.has_inst) { + drum->base_note = drum->sample->base_note; + } else { + error("No basenote for drum (line %d)", drum_node->line); + } + } + + // link + link_drum: + if (sf->drums == NULL) { + sf->drums = drum; + sf->drums_last = drum; + } else { + sf->drums_last->next = drum; + sf->drums_last = drum; + } + drum->next = NULL; + } +} + +void +read_sfx_info(soundfont *sf, xmlNodePtr effects) +{ + static const xml_attr_spec sfx_spec = { + {"Name", false, xml_parse_c_identifier, offsetof(sfx_data, name) }, + { "Sample", false, xml_parse_c_identifier, offsetof(sfx_data, sample_name)}, + { "SampleRate", true, xml_parse_double, offsetof(sfx_data, sample_rate)}, + { "BaseNote", true, xml_parse_note_number, offsetof(sfx_data, base_note) }, + }; + + LL_FOREACH(xmlNodePtr, sfx_node, effects->children) { + if (sfx_node->type != XML_ELEMENT_NODE) + continue; + + const char *name = XMLSTR_TO_STR(sfx_node->name); + if (!strequ(name, "Effect")) + error("Unexpected element node %s in effects list (line %d)", name, sfx_node->line); + + sf->info.num_effects++; + + sfx_data *sfx = malloc(sizeof(sfx_data)); + + if (sfx_node->properties == NULL) { + sfx->sample = NULL; + } else { + sfx->sample_rate = -1; + sfx->base_note = NOTE_UNSET; + xml_parse_node_by_spec(sfx, sfx_node, sfx_spec, ARRAY_COUNT(sfx_spec)); + + sfx->sample = sample_data_forname(sf, sfx->sample_name); + if (sfx->sample == NULL) + error("Bad sample name \"%s\" (line %d). Is it defined in ?", sfx->sample_name, + sfx_node->line); + + if (sfx->base_note == NOTE_UNSET) + sfx->base_note = sfx->sample->base_note; + + if (sfx->sample_rate == -1) + sfx->sample_rate = sfx->sample->sample_rate; + + sfx->tuning = calc_tuning(sfx->sample_rate, sfx->base_note); + } + + // link + if (sf->sfx == NULL) { + sf->sfx = sfx; + sf->sfx_last = sfx; + } else { + sf->sfx_last->next = sfx; + sf->sfx_last = sfx; + } + sfx->next = NULL; + } +} + +typedef struct { + bool is_dd; + bool cached; +} sample_data_defaults; + +void +read_samples_info(soundfont *sf, xmlNodePtr samples) +{ + static const xml_attr_spec samples_spec = { + {"IsDD", true, xml_parse_bool, offsetof(sample_data_defaults, is_dd) }, + { "Cached", true, xml_parse_bool, offsetof(sample_data_defaults, cached)}, + }; + static const xml_attr_spec sample_spec = { + {"Name", false, xml_parse_c_identifier, offsetof(sample_data, name) }, + { "SampleRate", true, xml_parse_double, offsetof(sample_data, sample_rate)}, + { "BaseNote", true, xml_parse_note_number, offsetof(sample_data, base_note) }, + { "IsDD", true, xml_parse_bool, offsetof(sample_data, is_dd) }, + { "Cached", true, xml_parse_bool, offsetof(sample_data, cached) }, + }; + + sample_data_defaults defaults; + defaults.is_dd = false; + defaults.cached = false; + xml_parse_node_by_spec(&defaults, samples, samples_spec, ARRAY_COUNT(samples_spec)); + + LL_FOREACH(xmlNodePtr, sample_node, samples->children) { + if (sample_node->type != XML_ELEMENT_NODE) + continue; + + const char *name = XMLSTR_TO_STR(sample_node->name); + if (!strequ(name, "Sample")) + error("Unexpected element node %s in samples list (line %d)", name, sample_node->line); + + sample_data *sample = malloc(sizeof(sample_data)); + + sample->sample_rate = -1.0; + sample->base_note = NOTE_UNSET; + sample->is_dd = defaults.is_dd; + sample->cached = defaults.cached; + + xml_parse_node_by_spec(sample, sample_node, sample_spec, ARRAY_COUNT(sample_spec)); + + samplebank *sb = (sample->is_dd) ? &sf->sbdd : &sf->sb; + + const char *sample_path = samplebank_path_forname(sb, sample->name); + if (sample_path == NULL) + error("Bad sample name %s, does it exist in the samplebank? (line %d)", sample->name, sample_node->line); + + aifc_read(&sample->aifc, sample_path, NULL, NULL); + + if (sample->sample_rate == -1.0) + sample->sample_rate = sample->aifc.sample_rate; + + if (sample->base_note == NOTE_UNSET) { + if (sample->aifc.has_inst) + sample->base_note = midinote_to_z64note(sample->aifc.basenote); + else + error("No basenote for sample %s (line %d)", sample->name, sample_node->line); + } + + if (!sample->aifc.has_book) + error("No vadpcm codebook for sample %s (line %d)", sample->name, sample_node->line); + + // link + if (sf->samples == NULL) { + sf->samples = sample; + sf->sample_last = sample; + } else { + sf->sample_last->next = sample; + sf->sample_last = sample; + } + sample->next = NULL; + } +} + +static bool +is_hex(char c) +{ + return ('0' <= c && c <= '9') || ('A' <= c && c <= 'F'); +} + +static int +from_hex(char c) +{ + if ('0' <= c && c <= '9') + return c - '0'; + if ('A' <= c && c <= 'F') + return c - 'A' + 10; + assert(false); + return -0xABABABAB; +} + +void +read_match_padding(soundfont *sf, xmlNodePtr padding_decl) +{ + if (padding_decl->properties != NULL) + error("Unexpected properties for MatchPadding declaration (line %d)", padding_decl->line); + + if (padding_decl->children == NULL || padding_decl->children->content == NULL) + error("No data declared for MatchPadding (line %d)", padding_decl->line); + + if (padding_decl->children->next != NULL) + error("Unexpected layout for MatchPadding declaration (line %d)", padding_decl->line); + + const char *data_str = XMLSTR_TO_STR(padding_decl->children->content); + size_t data_len = strlen(data_str); + + // We expect padding to be bytes like 0xAB separated by comma or whitespace, so string length / 5 is the upper bound + uint8_t *padding = malloc(data_len / 5); + + size_t k = 0; + bool must_be_delimiter = false; + + for (size_t i = 0; i < data_len - 4; i++) { + if (isspace(data_str[i]) || data_str[i] == ',') { + must_be_delimiter = false; + continue; + } + + if (must_be_delimiter) + error("Malformed padding data, expected a space or comma at position %ld", i); + + if (data_str[i + 0] != '0' || data_str[i + 1] != 'x') + error("Malformed padding data, expected an 0x prefix at position %ld", i); + + char c1 = toupper(data_str[i + 2]); + char c2 = toupper(data_str[i + 3]); + + if (!is_hex(c1) || !is_hex(c2)) + error("Malformed padding data, expected hexadecimal digits at position %ld", i + 2); + + padding[k++] = (from_hex(c1) << 4) | from_hex(c2); + must_be_delimiter = true; + i += 3; + } + + sf->match_padding = padding; + sf->match_padding_num = k; +} + +/** + * Emit a padding statement that pads to the next 0x10 byte boundary. Assumes that `pos` measures from an 0x10-byte + * aligned location. + */ +static void +emit_padding_stmt(FILE *out, unsigned pos) +{ + switch (ALIGN16(pos) - pos) { + case 0: + // Already aligned, pass silently + break; + case 4: + fprintf(out, "SF_PAD4();\n"); + break; + case 8: + fprintf(out, "SF_PAD8();\n"); + break; + case 0xC: + fprintf(out, "SF_PADC();\n"); + break; + default: + // We don't expect to need to support alignment from anything less than word-aligned. + error("[Internal] Bad alignment generated"); + break; + } +} + +size_t +emit_c_header(FILE *out, soundfont *sf) +{ + size_t size = 0; + + fprintf(out, "// HEADER\n\n"); + + // Generate externs for use in the header. + + if (sf->drums != NULL) + fprintf(out, "extern Drum* SF%d_DRUMS_PTR_LIST[];\n\n", sf->info.index); + + if (sf->sfx != NULL) + fprintf(out, "extern SoundEffect SF%d_SFX_LIST[];\n\n", sf->info.index); + + if (sf->instruments != NULL) { + // Externs are emitted in struct order + LL_FOREACH(instr_data *, instr, sf->instruments) { + if (instr->unused) + continue; + fprintf(out, "extern Instrument SF%d_%s;\n", sf->info.index, instr->name); + } + fprintf(out, "\n"); + } + + // Generate the header itself: drums -> sfx -> instruments. + + // We always need to write pointers for drums and sfx even if they are NULL. + + uint32_t pos = 0; + + if (sf->drums != NULL) + fprintf(out, "NO_REORDER SECTION_DATA Drum** SF%d_DRUMS_PTR_LIST_PTR = SF%d_DRUMS_PTR_LIST;\n", sf->info.index, + sf->info.index); + else + fprintf(out, "NO_REORDER SECTION_DATA Drum** SF%d_DRUMS_PTR_LIST_PTR = NULL;\n", sf->info.index); + + pos += 4; + size += 4; + + if (sf->sfx != NULL) + fprintf(out, "NO_REORDER SECTION_DATA SoundEffect* SF%d_SFX_LIST_PTR = SF%d_SFX_LIST;\n", sf->info.index, + sf->info.index); + else + fprintf(out, "NO_REORDER SECTION_DATA SoundEffect* SF%d_SFX_LIST_PTR = NULL;\n", sf->info.index); + + pos += 4; + size += 4; + + if (sf->instruments != NULL) { + const char **instr_names = calloc(sf->info.num_instruments, sizeof(const char *)); + + // The instrument pointer table is indexed by program number. Since sf->instruments is sorted by struct index + // we must first sort by program number. + LL_FOREACH(instr_data *, instr, sf->instruments) { + if (instr->unused) + continue; // Unused instruments are not included in the table and have no meaningful program number + instr_names[instr->program_number] = instr->name; + } + + fprintf(out, "NO_REORDER SECTION_DATA Instrument* SF%d_INSTRUMENT_PTR_LIST[] = {\n", sf->info.index); + + for (unsigned i = 0; i < sf->info.num_instruments; i++) { + if (instr_names[i] == NULL) + fprintf(out, " NULL,\n"); + else + fprintf(out, " &SF%d_%s,\n", sf->info.index, instr_names[i]); + pos += 4; + size += 4; + } + fprintf(out, "};\n"); + + free(instr_names); + } + + // Pad the header to the next 0x10-byte boundary. + emit_padding_stmt(out, pos); + fprintf(out, "\n"); + + return ALIGN16(size); +} + +/** + * Convert the compression type as indicated in the AIFC to the correspoding SampleCodec enum value. + * These must be kept in sync with the SampleCodec definition! + */ +static const char * +codec_enum(uint32_t compression_type, const char *origin_file) +{ + switch (compression_type) { + case CC4('A', 'D', 'P', '9'): + return "CODEC_ADPCM"; + + case CC4('H', 'P', 'C', 'M'): + return "CODEC_S8"; + + case CC4('A', 'D', 'P', '5'): + return "CODEC_SMALL_ADPCM"; + + case CC4('R', 'V', 'R', 'B'): + return "CODEC_REVERB"; + + case CC4('N', 'O', 'N', 'E'): + return "CODEC_S16"; + } + error("Bad compression type in aifc file %s", origin_file); + __builtin_unreachable(); +} + +static unsigned int +codec_frame_size(uint32_t compression_type) +{ + switch (compression_type) { + case CC4('A', 'D', 'P', '9'): + return 9; + + case CC4('A', 'D', 'P', '5'): + return 5; + + default: // TODO should any others not use 16? + return 16; + } +} + +/** + * Compare the codebooks of two samples. Returns true if they are identical. + */ +static bool +samples_books_equal(sample_data *s1, sample_data *s2) +{ + int32_t s1_order = s1->aifc.book.order; + int32_t s1_npredictors = s1->aifc.book.npredictors; + int32_t s2_order = s1->aifc.book.order; + int32_t s2_npredictors = s1->aifc.book.npredictors; + + if (s1_order != s2_order || s1_npredictors != s2_npredictors) + return false; + return !memcmp(*s1->aifc.book_state, *s2->aifc.book_state, 8 * (unsigned)s1_order * (unsigned)s1_npredictors); +} + +/** + * Writes all samples, their codebooks and their loops to C structures. + */ +size_t +emit_c_samples(FILE *out, soundfont *sf) +{ + size_t size = 0; + + if (sf->samples == NULL) + return size; + + int i = 0; + LL_FOREACH(sample_data *, sample, sf->samples) { + // Determine if we need to write a new book structure. If we've already emitted a book structure with the + // same contents we use that instead. + + bool new_book = true; + const char *bookname = sample->name; + + LL_FOREACH(sample_data *, sample2, sf->samples) { + if (sample2 == sample) + // Caught up to our current position, we need to write a new book. + break; + + if (samples_books_equal(sample, sample2)) { + // A book that we've already seen is the same as this one. Since the book we are comparing to here is + // the first such book, this is guaranteed to have already been written and we move the reference to + // this one. + new_book = false; + bookname = sample2->name; + break; + } + } + + fprintf(out, "// SAMPLE %d\n\n", i); + + // Write the sample header + + samplebank *sb = (sample->is_dd) ? &sf->sbdd : &sf->sb; + + // Note: We could skip writing the book extern if new_book is false, but it's probably not worth the extra code + fprintf(out, + // clang-format off + "extern u8 %s_%s_Off[];" "\n" + "extern AdpcmBook SF%d_%s_BOOK;" "\n" + "extern AdpcmLoop SF%d_%s_LOOP;" "\n" + "\n", + // clang-format on + sb->name, sample->name, sf->info.index, bookname, sf->info.index, sample->name); + + const char *codec_name = codec_enum(sample->aifc.compression_type, sample->aifc.path); + + fprintf(out, + // clang-format off + "NO_REORDER SECTION_DATA ALIGNED(16) Sample SF%d_%s_HEADER = {" "\n" + " " +#ifdef SFC_MM + // MM has an extra unused field in the sample structure compared to OoT + "%d, " +#endif + "%s, %d, %s, %s," "\n" + " 0x%06lX," "\n" + " %s_%s_Off," "\n" + " &SF%d_%s_LOOP," "\n" + " &SF%d_%s_BOOK," "\n" + "};" "\n" + "\n", + // clang-format on + sf->info.index, sample->name, +#ifdef SFC_MM + 0, +#endif + codec_name, sample->is_dd, BOOL_STR(sample->cached), BOOL_STR(false), sample->aifc.ssnd_size, sb->name, + sample->name, sf->info.index, sample->name, sf->info.index, bookname); + size += 0x10; + + // Write the book if it hasn't been deduplicated. + + if (new_book) { + // Since books are variable-size structures and we want to support a C89 compiler, we first write the + // header as one structure and the book state as an array. We then declare a weak symbol for the book + // header to alias it to the correct type without casts, avoiding potential type conflicts with externs. + size_t book_size = 0; + + fprintf(out, + // clang-format off + "NO_REORDER SECTION_DATA ALIGNED(16) AdpcmBookHeader SF%d_%s_BOOK_HEADER = {" "\n" + " %d, %d," "\n" + "};" "\n" + "NO_REORDER SECTION_DATA AdpcmBookData SF%d_%s_BOOK_DATA = {" "\n", + // clang-format on + sf->info.index, bookname, sample->aifc.book.order, sample->aifc.book.npredictors, sf->info.index, + bookname); + book_size += 8; + + for (size_t j = 0; j < (unsigned)sample->aifc.book.order * (unsigned)sample->aifc.book.npredictors; j++) { + fprintf( + out, + // clang-format off + " (s16)0x%04X, (s16)0x%04X, (s16)0x%04X, (s16)0x%04X, " + "(s16)0x%04X, (s16)0x%04X, (s16)0x%04X, (s16)0x%04X,\n", + // clang-format on + (uint16_t)(*sample->aifc.book_state)[j * 8 + 0], (uint16_t)(*sample->aifc.book_state)[j * 8 + 1], + (uint16_t)(*sample->aifc.book_state)[j * 8 + 2], (uint16_t)(*sample->aifc.book_state)[j * 8 + 3], + (uint16_t)(*sample->aifc.book_state)[j * 8 + 4], (uint16_t)(*sample->aifc.book_state)[j * 8 + 5], + (uint16_t)(*sample->aifc.book_state)[j * 8 + 6], (uint16_t)(*sample->aifc.book_state)[j * 8 + 7]); + } + + fprintf(out, + // clang-format off + "};" "\n" + "#pragma weak SF%d_%s_BOOK = SF%d_%s_BOOK_HEADER" "\n", + // clang-format on + sf->info.index, bookname, sf->info.index, bookname); + + // We assume here that book structures begin on 0x10-byte boundaries. Book structures are always + // `4 + 4 + 8 * order * npredictors` large, emit a padding statement to the next 0x10-byte boundary. + book_size += 2 * 8 * (unsigned)sample->aifc.book.order * (unsigned)sample->aifc.book.npredictors; + emit_padding_stmt(out, book_size); + fprintf(out, "\n"); + + size += ALIGN16(book_size); + } + + // Write the loop + + // Can't use sample->aifc.num_frames directly, the original vadpcm_enc tool occasionally got the number + // of frames wrong (off-by-1) which we must reproduce here for matching (rather than reproducing it in the + // aifc and wav/aiff files themselves) + uint32_t frame_count = (sample->aifc.ssnd_size * 16) / codec_frame_size(sample->aifc.compression_type); + + // We cannot deduplicate or skip writing loops in general as the audio driver assumes that at least a loop + // header exists for every sample. We could deduplicate on the special case that two samples have the same + // frame count? TODO + + if (!sample->aifc.has_loop || sample->aifc.loop.count == 0) { + // No loop present, or a loop with a count of 0 was explicitly written into the aifc. + // Write a header only, using the same weak symbol trick as with books. + + uint32_t start; + uint32_t end; + uint32_t count; + + if (!sample->aifc.has_loop) { + // No loop, write a loop header that spans the entire sample with a count of 0. + // The audio driver expects that a loop structure always exists for a sample. + start = 0; + end = frame_count; + count = 0; + } else { + // There is a count=0 loop in the aifc file, trust it. + start = sample->aifc.loop.start; + end = sample->aifc.loop.end; + count = sample->aifc.loop.count; + } + + fprintf(out, + // clang-format off + "NO_REORDER SECTION_DATA ALIGNED(16) AdpcmLoopHeader SF%d_%s_LOOP_HEADER = {" "\n" + " %u, %u, %u, 0," "\n" + "};" "\n" + "#pragma weak SF%d_%s_LOOP = SF%d_%s_LOOP_HEADER" "\n" + "\n", + // clang-format on + sf->info.index, sample->name, start, end, count, sf->info.index, sample->name, sf->info.index, + sample->name); + size += 0x10; + } else { + // With state, since loop states are a fixed size there is no need for a weak alias. + + // Some soundfonts include the total frame count of the sample, but not all of them. + // Set the frame count to 0 here to inhibit writing it into the loop structure if this is + // a soundfont that does not include it. + if (!sf->info.loops_have_frames) + frame_count = 0; + + char count_str[12]; + + if (sample->aifc.loop.count == 0xFFFFFFFF) + snprintf(count_str, sizeof(count_str), "0x%08X", sample->aifc.loop.count); + else + snprintf(count_str, sizeof(count_str), "%u", sample->aifc.loop.count); + + fprintf(out, + // clang-format off + "NO_REORDER SECTION_DATA ALIGNED(16) AdpcmLoop SF%d_%s_LOOP = {" "\n" + " { %u, %u, %s, %u }," "\n" + " {" "\n" + " (s16)0x%04X, (s16)0x%04X, (s16)0x%04X, (s16)0x%04X," "\n" + " (s16)0x%04X, (s16)0x%04X, (s16)0x%04X, (s16)0x%04X," "\n" + " (s16)0x%04X, (s16)0x%04X, (s16)0x%04X, (s16)0x%04X," "\n" + " (s16)0x%04X, (s16)0x%04X, (s16)0x%04X, (s16)0x%04X," "\n" + " }," "\n" + "};" "\n" + "\n", + // clang-format on + sf->info.index, sample->name, sample->aifc.loop.start, sample->aifc.loop.end, count_str, + frame_count, (uint16_t)sample->aifc.loop.state[0], (uint16_t)sample->aifc.loop.state[1], + (uint16_t)sample->aifc.loop.state[2], (uint16_t)sample->aifc.loop.state[3], + (uint16_t)sample->aifc.loop.state[4], (uint16_t)sample->aifc.loop.state[5], + (uint16_t)sample->aifc.loop.state[6], (uint16_t)sample->aifc.loop.state[7], + (uint16_t)sample->aifc.loop.state[8], (uint16_t)sample->aifc.loop.state[9], + (uint16_t)sample->aifc.loop.state[10], (uint16_t)sample->aifc.loop.state[11], + (uint16_t)sample->aifc.loop.state[12], (uint16_t)sample->aifc.loop.state[13], + (uint16_t)sample->aifc.loop.state[14], (uint16_t)sample->aifc.loop.state[15]); + size += 0x30; + } + i++; + } + return size; +} + +/** + * Write envelope structures. + */ +size_t +emit_c_envelopes(FILE *out, soundfont *sf) +{ + size_t size = 0; + + if (sf->envelopes == NULL) + return size; + + fprintf(out, "// ENVELOPES\n\n"); + + size_t empty_num = 0; + + LL_FOREACH(envelope_data *, envdata, sf->envelopes) { + if (sf->matching && envdata->name == NULL) { + // For MM: write 16 bytes of 0 when matching + + fprintf(out, + // clang-format off + "NO_REORDER SECTION_DATA ALIGNED(16) EnvelopePoint SF%d_ENV_EMPTY_%lu[] = {" "\n" + " { 0, 0, }," "\n" + " { 0, 0, }," "\n" + " { 0, 0, }," "\n" + " { 0, 0, }," "\n" + "};" "\n" + "\n", + // clang-format on + sf->info.index, empty_num); + + empty_num++; + size += 0x10; + } else { + fprintf(out, "NO_REORDER SECTION_DATA ALIGNED(16) EnvelopePoint SF%d_%s[] = {\n", sf->info.index, + envdata->name); + + // Write all points + for (size_t j = 0; j < envdata->n_points; j++) { + envelope_point *pt = &envdata->points[j]; + + switch (pt->delay) { + case ADSR_DISABLE: + fprintf(out, " ENVELOPE_DISABLE(),\n"); + break; + case ADSR_GOTO: + fprintf(out, " ENVELOPE_GOTO(%d),\n", pt->arg); + break; + case ADSR_HANG: + fprintf(out, " ENVELOPE_HANG(),\n"); + break; + case ADSR_RESTART: + fprintf(out, " ENVELOPE_RESTART(),\n"); + break; + default: + fprintf(out, " ENVELOPE_POINT(%5d, %5d),\n", pt->delay, pt->arg); + break; + } + } + + // Automatically add a HANG command at the end + fprintf(out, " ENVELOPE_HANG(),\n" + "};\n"); + + // Pad to 0x10-byte boundary + size_t env_size = 4 * (envdata->n_points + 1); + emit_padding_stmt(out, env_size); + fprintf(out, "\n"); + + size += ALIGN16(env_size); + } + } + return size; +} + +#define F32_FMT "%.22f" + +size_t +emit_c_instruments(FILE *out, soundfont *sf) +{ + size_t size = 0; + + fprintf(out, "// INSTRUMENTS\n\n"); + + size_t unused_instr_num = 0; + + LL_FOREACH(instr_data *, instr, sf->instruments) { + if (instr->unused) { + fprintf(out, "NO_REORDER SECTION_DATA Instrument SF%d_INSTR_UNUSED_%lu = {\n", sf->info.index, + unused_instr_num); + unused_instr_num++; + } else { + fprintf(out, "NO_REORDER SECTION_DATA Instrument SF%d_%s = {\n", sf->info.index, instr->name); + } + + char nlo[5]; + snprintf(nlo, sizeof(nlo), "%3d", instr->sample_low_end); + char nhi[5]; + snprintf(nhi, sizeof(nhi), "%3d", instr->sample_high_start); + + fprintf(out, + // clang-format off + " false," "\n" + " %s," "\n" + " %s," "\n" + " %d," "\n" + " SF%d_%s," "\n", + // clang-format on + (instr->sample_low_end == INSTR_LO_NONE) ? "INSTR_SAMPLE_LO_NONE" : nlo, + (instr->sample_high_start == INSTR_HI_NONE) ? "INSTR_SAMPLE_HI_NONE" : nhi, instr->release, + sf->info.index, instr->envelope_name); + + if (instr->sample_low != NULL) + fprintf(out, " { &SF%d_%s_HEADER, " F32_FMT "f },\n", sf->info.index, instr->sample_name_low, + instr->sample_low_tuning); + else + fprintf(out, " INSTR_SAMPLE_NONE,\n"); + + fprintf(out, " { &SF%d_%s_HEADER, " F32_FMT "f },\n", sf->info.index, instr->sample_name_mid, + instr->sample_mid_tuning); + + if (instr->sample_high != NULL) + fprintf(out, " { &SF%d_%s_HEADER, " F32_FMT "f },\n", sf->info.index, instr->sample_name_high, + instr->sample_high_tuning); + else + fprintf(out, " INSTR_SAMPLE_NONE,\n"); + + fprintf(out, "};\n\n"); + + size += 0x20; + } + return size; +} + +size_t +emit_c_drums(FILE *out, soundfont *sf) +{ + size_t size = 0; + + if (sf->drums == NULL) + return size; + + fprintf(out, "// DRUMS\n\n"); + + // Prepare pointer table data to be filled in while writing the drum structures. Init to 0 so if any low notes are + // not covered by any drum group the name will be NULL. + struct { + const char *name; + int n; + } ptr_table[64]; + memset(ptr_table, 0, sizeof(ptr_table)); + + // While writing the drum structures we record the maximum note covered by this soundfont. Some "oddball" soundfonts + // like soundfont 0 do not have an array entry for all 64 notes. We use this to know when to stop writing entries in + // the pointer table. + int max_note = -1; + + LL_FOREACH(drum_data *, drum, sf->drums) { + if (drum->name == NULL) { + max_note++; + continue; + } + + if (drum->note_end > max_note) + max_note = drum->note_end; + + size_t length = drum->note_end - drum->note_start + 1; + + // Drum structures are duplicated for each note in the range they cover, the basenote for each is incremented + // by one but the data is otherwise identical. We write a preprocessor definition to make the resulting source + // more compact for easier inspection. + + fprintf(out, + // clang-format off + "#define SF%d_%s_ENTRY(tuning) \\" "\n" + " { \\" "\n" + " %d, \\" "\n" + " %d, \\" "\n" + " false, \\" "\n" + " { &SF%d_%s_HEADER, (tuning) }, \\" "\n" + " SF%d_%s, \\" "\n" + " }" "\n" + "NO_REORDER SECTION_DATA Drum SF%d_%s[%lu] = {" "\n", + // clang-format on + sf->info.index, drum->name, drum->release, drum->pan, sf->info.index, drum->sample->name, + sf->info.index, drum->envelope->name, sf->info.index, drum->name, length); + + // Write each structure while building the drum pointer table + + if (drum->note_end + 1 > 64) + error("Bad drum range for drum spanning %d to %d, should be within 0 to 63", drum->note_start, + drum->note_end); + + for (size_t note_offset = 0; note_offset < length; note_offset++) { + size_t ptr_offset = drum->note_start + note_offset; + + ptr_table[ptr_offset].name = drum->name; + ptr_table[ptr_offset].n = note_offset; + + // wrap note on overflow + int note = drum->base_note + note_offset; + if (note > 127) + note -= 128; + + float tuning = calc_tuning(drum->sample_rate, note); + + fprintf(out, " SF%d_%s_ENTRY(" F32_FMT "f),\n", sf->info.index, drum->name, tuning); + } + + fprintf(out, "};\n\n"); + size += 0x10 * length; + } + + // Write the drum pointer table. Always start at 0 and end at the maximum used note. If any low notes are not used, + // NULL is written into the array. + + size_t table_len = max_note + 1; + if (table_len > 64) + error("Bad drum pointer table length %lu, should be at most 64", table_len); + + fprintf(out, "NO_REORDER SECTION_DATA ALIGNED(16) Drum* SF%d_DRUMS_PTR_LIST[%lu] = {\n", sf->info.index, table_len); + + for (size_t i = 0; i < table_len; i++) { + if (ptr_table[i].name == NULL) { + fprintf(out, " NULL,\n"); + continue; + } + + if (i != 0 && ptr_table[i].n == 0) // Add some space between different drum groups + fprintf(out, "\n"); + fprintf(out, " &SF%d_%s[%d],\n", sf->info.index, ptr_table[i].name, ptr_table[i].n); + } + + sf->info.num_drums = table_len; + + fprintf(out, "};\n"); + emit_padding_stmt(out, table_len * 4); + fprintf(out, "\n"); + + size += ALIGN16(table_len * 4); + return size; +} + +size_t +emit_c_effects(FILE *out, soundfont *sf) +{ + size_t size = 0; + + if (sf->sfx == NULL) + return size; + + fprintf(out, "// EFFECTS\n\n"); + + // Effects are all contained in the same array. We write empty entries as NULL entries in this array. + + fprintf(out, "NO_REORDER SECTION_DATA ALIGNED(16) SoundEffect SF%d_SFX_LIST[] = {\n", sf->info.index); + + LL_FOREACH(sfx_data *, sfx, sf->sfx) { + if (sfx->sample != NULL) + fprintf(out, " { { &SF%d_%s_HEADER, " F32_FMT "f } },\n", sf->info.index, sfx->sample->name, + sfx->tuning); + else + fprintf(out, " { { NULL, 0.0f } },\n"); + + size += 8; + } + + fprintf(out, "};\n\n"); + + return size; +} + +void +emit_c_match_padding(FILE *out, soundfont *sf, size_t size) +{ + if (sf->match_padding != NULL && sf->match_padding_num != 0) { + // Sometimes a soundfont will have non-zero padding at the end, add these values manually + size_t expected = sf->match_padding_num; + + // Don't pad any further than the next 0x10 byte boundary + size_t remaining = ALIGN16(size) - size; + size_t amount = (expected > remaining) ? remaining : expected; + + fprintf(out, "// MATCH PADDING\n\n"); + + fprintf(out, "NO_REORDER SECTION_DATA u8 SF%d_MATCH_PADDING[] = {\n", sf->info.index); + for (size_t i = 0; i < amount; i++) + fprintf(out, " 0x%02X,\n", sf->match_padding[i]); + fprintf(out, "};\n\n"); + + size += amount; + } + + if (sf->info.pad_to_size != 0) { + if (sf->info.pad_to_size <= size) { + warning("PadToSize directive ignored."); + } else { + fprintf(out, "// MATCH SIZE PADDING\n\n"); + + // pad to given size + size_t amount = sf->info.pad_to_size - size; + fprintf(out, "NO_REORDER SECTION_DATA u8 SF%d_MATCH_PADDING_TO_SIZE[%lu] = { 0 };\n", sf->info.index, + amount); + } + } +} + +void +emit_h_instruments(FILE *out, soundfont *sf) +{ + if (sf->instruments == NULL) + return; + + // Example output: + // #define FONT{Index}_INSTR_{EnumName} {EnumValue} + + LL_FOREACH(instr_data *, instr, sf->instruments) { + if (!instr->unused) { + fprintf(out, "#define SF%d_%s %d\n", sf->info.index, instr->name, instr->program_number); + } + } + fprintf(out, "\n"); +} + +static const char * +z64_note_name(int note_num) +{ + static const char *const note_names[] = { + "A0", "BF0", "B0", "C1", "DF1", "D1", "EF1", "E1", "F1", "GF1", "G1", "AF1", "A1", "BF1", "B1", + "C2", "DF2", "D2", "EF2", "E2", "F2", "GF2", "G2", "AF2", "A2", "BF2", "B2", "C3", "DF3", "D3", + "EF3", "E3", "F3", "GF3", "G3", "AF3", "A3", "BF3", "B3", "C4", "DF4", "D4", "EF4", "E4", "F4", + "GF4", "G4", "AF4", "A4", "BF4", "B4", "C5", "DF5", "D5", "EF5", "E5", "F5", "GF5", "G5", "AF5", + "A5", "BF5", "B5", "C6", "DF6", "D6", "EF6", "E6", "F6", "GF6", "G6", "AF6", "A6", "BF6", "B6", + "C7", "DF7", "D7", "EF7", "E7", "F7", "GF7", "G7", "AF7", "A7", "BF7", "B7", "C8", "DF8", "D8", + "EF8", "E8", "F8", "GF8", "G8", "AF8", "A8", "BF8", "B8", "C9", "DF9", "D9", "EF9", "E9", "F9", + "GF9", "G9", "AF9", "A9", "BF9", "B9", "C10", "DF10", "D10", "EF10", "E10", "F10", "BFNEG1", "BNEG1", "C0", + "DF0", "D0", "EF0", "E0", "F0", "GF0", "G0", "AF0", + }; + return note_names[note_num]; +} + +void +emit_h_drums(FILE *out, soundfont *sf) +{ + if (sf->drums == NULL) + return; + + // Emit drum defines in groups, named like [DrumName]_[NoteName] + // e.g. a drum called "MY_DRUM" with a sample basenote of C4 covering a note range of 0..3 looks like + // #define MY_DRUM_C4 0 + // #define MY_DRUM_DF4 1 + // #define MY_DRUM_D4 2 + // #define MY_DRUM_EF4 3 + + LL_FOREACH(drum_data *, drum, sf->drums) { + if (drum->name == NULL) + continue; + + int length = drum->note_end - drum->note_start + 1; + + for (int note_offset = 0; note_offset < length; note_offset++) { + // wrap note on overflow + int note = drum->base_note + note_offset; + if (note > 127) + note -= 128; + + fprintf(out, "#define SF%d_%s_%s %d\n", sf->info.index, drum->name, z64_note_name(note), + drum->note_start + note_offset); + } + + fprintf(out, "\n"); + } +} + +void +emit_h_effects(FILE *out, soundfont *sf) +{ + if (sf->sfx == NULL) + return; + + int i = 0; + LL_FOREACH(sfx_data *, sfx, sf->sfx) { + if (sfx->sample != NULL) + fprintf(out, "#define SF%d_%s %d\n", sf->info.index, sfx->name, i); + i++; + } + fprintf(out, "\n"); +} + +NORETURN static void +usage(const char *progname) +{ + fprintf(stderr, "Usage: %s [--matching] \n", progname); + exit(EXIT_FAILURE); +} + +int +main(int argc, char **argv) +{ + char *filename_in = NULL; + char *filename_out_c = NULL; + char *filename_out_h = NULL; + char *filename_out_name = NULL; + const char *mdfilename = NULL; + FILE *mdfile; + xmlDocPtr document; + soundfont sf; + + sf.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 (sf.matching) + arg_error("Received --matching option twice"); + + sf.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_in = argv[i]; + break; + case 1: + filename_out_c = argv[i]; + break; + case 2: + filename_out_h = argv[i]; + break; + case 3: + filename_out_name = argv[i]; + break; + default: + arg_error("Unknown positional argument \"%s\"", argv[i]); + break; + } + argn++; + } + } + if (argn != 4) + arg_error("Not enough positional arguments"); + +#undef arg_error + + document = xmlReadFile(filename_in, NULL, XML_PARSE_NONET); + if (document == NULL) + return EXIT_FAILURE; + + xmlNodePtr root = xmlDocGetRootElement(document); + if (!strequ(XMLSTR_TO_STR(root->name), "Soundfont")) + error("Root node must be "); + read_soundfont_info(&sf, root); + + sf.envelopes = sf.envelope_last = NULL; + + // read all envelopes first irrespective of their positioning in the xml + LL_FOREACH(xmlNodePtr, node, root->children) { + const char *name = XMLSTR_TO_STR(node->name); + + if (strequ(name, "Envelopes")) + read_envelopes_info(&sf, node); + } + + // read all samples + sf.samples = NULL; + LL_FOREACH(xmlNodePtr, node, root->children) { + const char *name = XMLSTR_TO_STR(node->name); + + if (strequ(name, "Samples")) + read_samples_info(&sf, node); + } + + // read all instruments + memset(sf.program_number_bitset, 0, sizeof(sf.program_number_bitset)); + sf.instruments = NULL; + sf.drums = NULL; + sf.sfx = NULL; + LL_FOREACH(xmlNodePtr, node, root->children) { + const char *name = XMLSTR_TO_STR(node->name); + + if (strequ(name, "Instruments")) + read_instrs_info(&sf, node); + if (strequ(name, "Drums")) + read_drums_info(&sf, node); + if (strequ(name, "Effects")) + read_sfx_info(&sf, node); + } + + // read match padding if it exists + sf.match_padding = NULL; + LL_FOREACH(xmlNodePtr, node, root->children) { + const char *name = XMLSTR_TO_STR(node->name); + + if (strequ(name, "MatchPadding")) + read_match_padding(&sf, node); + } + + // emit C source + + FILE *out_c = fopen(filename_out_c, "w"); + fprintf(out_c, "#include \"soundfont_file.h\"\n\n"); + + size_t size = 0; + size += emit_c_header(out_c, &sf); + size += emit_c_samples(out_c, &sf); + size += emit_c_envelopes(out_c, &sf); + size += emit_c_instruments(out_c, &sf); + size += emit_c_drums(out_c, &sf); + size += emit_c_effects(out_c, &sf); + emit_c_match_padding(out_c, &sf, size); + + fclose(out_c); + + // emit C header + + FILE *out_h = fopen(filename_out_h, "w"); + fprintf(out_h, + // clang-format off + "#ifndef SOUNDFONT_%d_H_" "\n" + "#define SOUNDFONT_%d_H_" "\n" + "\n", + // clang-format on + sf.info.index, sf.info.index); + + fprintf(out_h, + // clang-format off + "#ifdef _LANGUAGE_ASEQ" "\n" + ".pushsection .fonts, \"\", @note" "\n" + " .byte %d /*sf id*/" "\n" + ".popsection" "\n" + "#endif" "\n" + "\n", + // clang-format on + sf.info.index); + + fprintf(out_h, + // clang-format off + "#define %s_ID %d" "\n" + "\n" + "#define SF%d_NUM_INSTRUMENTS %d" "\n" + "#define SF%d_NUM_DRUMS %d" "\n" + "#define SF%d_NUM_SFX %d" "\n" + "\n", + // clang-format on + sf.info.name, sf.info.index, sf.info.index, sf.info.num_instruments, sf.info.index, sf.info.num_drums, + sf.info.index, sf.info.num_effects); + + emit_h_instruments(out_h, &sf); + emit_h_drums(out_h, &sf); + emit_h_effects(out_h, &sf); + + fprintf(out_h, "#endif\n"); + fclose(out_h); + + // emit name marker + + FILE *out_name = fopen(filename_out_name, "w"); + fprintf(out_name, "%s", sf.info.name); + fclose(out_name); + + // emit dependency file if wanted + + if (mdfilename != NULL) { + mdfile = fopen(mdfilename, "w"); + if (mdfile == NULL) + error("Unable to open dependency file [%s] for writing", mdfilename); + + // Begin rule + depend on the soundfont xml input + fprintf(mdfile, "%s %s %s: \\\n %s", filename_out_c, filename_out_h, filename_out_name, filename_in); + + // Depend on the referenced samplebank xmls + if (sf.info.bank_path != NULL) + fprintf(mdfile, " \\\n %s", sf.info.bank_path); + if (sf.info.bank_path_dd != NULL) + fprintf(mdfile, " \\\n %s", sf.info.bank_path_dd); + + // Depend on the aifc files used by this soundfont + LL_FOREACH(sample_data *, sample, sf.samples) { + fprintf(mdfile, " \\\n %s", sample->aifc.path); + } + + fputs("\n", mdfile); + fclose(mdfile); + } + + // done + + xmlFreeDoc(document); + return EXIT_SUCCESS; +}