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