diff --git a/assets/xml/audio/samplebanks/SampleBank_0.xml b/assets/xml/audio/samplebanks/SampleBank_0.xml index 336d170287..e90a084930 100644 --- a/assets/xml/audio/samplebanks/SampleBank_0.xml +++ b/assets/xml/audio/samplebanks/SampleBank_0.xml @@ -1,105 +1,105 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/Makefile b/tools/Makefile index c4688f6646..62bc881e17 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -20,13 +20,16 @@ endif all: $(PROGRAMS) $(MAKE) -C ZAPD $(MAKE) -C fado + $(MAKE) -C audio clean: $(RM) $(PROGRAMS) $(addsuffix .exe,$(PROGRAMS)) $(MAKE) -C ZAPD clean $(MAKE) -C fado clean + $(MAKE) -C audio clean distclean: clean + $(MAKE) -C audio distclean .PHONY: all clean distclean diff --git a/tools/audio/Makefile b/tools/audio/Makefile new file mode 100644 index 0000000000..69ffccbb4e --- /dev/null +++ b/tools/audio/Makefile @@ -0,0 +1,14 @@ + +.PHONY: all clean distclean format + +all: + $(MAKE) -C sampleconv + +clean: + $(MAKE) -C sampleconv clean + +distclean: clean + $(MAKE) -C sampleconv distclean + +format: + $(MAKE) -C sampleconv format diff --git a/tools/audio/extraction/audio_extract.py b/tools/audio/extraction/audio_extract.py index ac51d2e0e7..ea4f8612c3 100644 --- a/tools/audio/extraction/audio_extract.py +++ b/tools/audio/extraction/audio_extract.py @@ -4,17 +4,18 @@ # Extract audio files # -import os +import os, shutil, time from dataclasses import dataclass from enum import auto, Enum +from multiprocessing.pool import ThreadPool from typing import Dict, List, Tuple, Union from xml.etree import ElementTree from xml.etree.ElementTree import Element from .audio_tables import AudioCodeTable, AudioCodeTableEntry, AudioStorageMedium -from .audiotable import AudioTableFile +from .audiotable import AudioTableData, AudioTableFile, AudioTableSample from .audiobank_file import AudiobankFile -from .util import align, debugm, error, incbin +from .util import align, debugm, error, incbin, program_get class MMLVersion(Enum): OOT = auto() @@ -114,13 +115,34 @@ def collect_soundfonts(audiobank_seg : memoryview, extracted_dir : str, version_ return soundfonts -def extract_samplebank(extracted_dir : str, sample_banks : List[Union[AudioTableFile, int]], bank : AudioTableFile, - write_xml : bool): +def aifc_extract_one_sample(base_path : str, sample : AudioTableSample): + aifc_path = f"{base_path}/aifc/{sample.filename}" + ext_compressed = sample.codec_file_extension_compressed() + ext_decompressed = sample.codec_file_extension_decompressed() + wav_path = f"{base_path}/{sample.filename.replace(ext_compressed, ext_decompressed)}" + # export to AIFC + sample.to_file(aifc_path) + # decode to AIFF/WAV + program_get(f"{SAMPLECONV_PATH} --matching pcm16 {aifc_path} {wav_path}") + +def aifc_extract_one_bin(base_path : str, sample : AudioTableData): + # export to BIN + if BASEROM_DEBUG: + sample.to_file(f"{base_path}/aifc/{sample.filename}") + # copy to correct location + shutil.copyfile(f"{base_path}/aifc/{sample.filename}", f"{base_path}/{sample.filename}") + else: + sample.to_file(f"{base_path}/{sample.filename}") + +def extract_samplebank(pool : ThreadPool, extracted_dir : str, sample_banks : List[Union[AudioTableFile, int]], + bank : AudioTableFile, write_xml : bool): # deal with remaining gaps, have to blob them unless we can find an exact match in another bank bank.finalize_coverage(sample_banks) # assign names bank.assign_names() + base_path = f"{extracted_dir}/assets/audio/samples/{bank.name}" + # write xml with open(f"{extracted_dir}/assets/audio/samplebanks/{bank.file_name}.xml", "w") as outfile: outfile.write(bank.to_xml(f"assets/audio/samples/{bank.name}")) @@ -129,6 +151,31 @@ def extract_samplebank(extracted_dir : str, sample_banks : List[Union[AudioTable if write_xml: bank.write_extraction_xml(f"assets/xml/audio/samplebanks/{bank.file_name}.xml") + # write sample sand blobs + + os.makedirs(f"{base_path}/aifc", exist_ok=True) + + aifc_samples = [sample for sample in bank.samples_final if isinstance(sample, AudioTableSample)] + bin_samples = [sample for sample in bank.samples_final if not isinstance(sample, AudioTableSample)] + + t_start = time.time() + + # we assume the number of bin samples are very small and don't multiprocess it + for sample in bin_samples: + aifc_extract_one_bin(base_path, sample) + + # multiprocess aifc extraction + decompression + async_results = [pool.apply_async(aifc_extract_one_sample, args=(base_path, sample)) for sample in aifc_samples] + # block until done + [res.get() for res in async_results] + + dt = time.time() - t_start + print(f"Samplebank {bank.name} extraction took {dt:.3f}s") + + # drop aifc dir if not in debug mode + if not BASEROM_DEBUG: + shutil.rmtree(f"{base_path}/aifc") + def extract_audio_for_version(version_info : GameVersionInfo, extracted_dir : str, read_xml : bool, write_xml : bool): print("Setting up...") @@ -234,13 +281,17 @@ def extract_audio_for_version(version_info : GameVersionInfo, extracted_dir : st print("Extracting samplebanks...") + # Check that the sampleconv binary is available + assert os.path.isfile(SAMPLECONV_PATH) , "Compile sampleconv" + os.makedirs(f"{extracted_dir}/assets/audio/samplebanks", exist_ok=True) if write_xml: os.makedirs(f"assets/xml/audio/samplebanks", exist_ok=True) - for bank in sample_banks: - if isinstance(bank, AudioTableFile): - extract_samplebank(extracted_dir, sample_banks, bank, write_xml) + with ThreadPool(processes=os.cpu_count()) as pool: + for bank in sample_banks: + if isinstance(bank, AudioTableFile): + extract_samplebank(pool, extracted_dir, sample_banks, bank, write_xml) # ================================================================================================================== # Extract soundfonts diff --git a/tools/audio/extraction/audiobank_file.py b/tools/audio/extraction/audiobank_file.py index 8c6bd58b99..83efbc71c7 100644 --- a/tools/audio/extraction/audiobank_file.py +++ b/tools/audio/extraction/audiobank_file.py @@ -438,8 +438,8 @@ class AudiobankFile: self.coverage.append([[unref_start_offset, Padding], [unref_end_offset, Padding]]) continue - coverage_log(f"Unaccounted: 0x{unref_start_offset:X}({unref_start_type.__name__}) " + \ - f"to 0x{unref_end_offset:X}({unref_end_type.__name__})") + coverage_log(f"Unaccounted: 0x{unref_start_offset:04X}({unref_start_type.__name__}) " + \ + f"to 0x{unref_end_offset:04X}({unref_end_type.__name__})") coverage_log([f"0x{b:02X}" for b in unaccounted_data]) try: diff --git a/tools/audio/extraction/audiotable.py b/tools/audio/extraction/audiotable.py index e02a6f285c..9511acecda 100644 --- a/tools/audio/extraction/audiotable.py +++ b/tools/audio/extraction/audiotable.py @@ -4,7 +4,7 @@ # # -import struct +import math, struct from typing import Dict, Tuple from xml.etree.ElementTree import Element @@ -474,7 +474,9 @@ class AudioTableFile: if sample.start in self.extraction_sample_info: return self.extraction_sample_info[sample.start]["FileName"] + ext print(f"WARNING: Missing extraction xml entry for sample at offset=0x{sample.start:X}") - return f"Sample{index}{ext}" + + npad = int(math.floor(1 + math.log10(len(self.samples)))) if len(self.samples) != 0 else 0 + return f"Sample{index:0{npad}}{ext}" def blob_filename(self, start, end): if self.extraction_blob_info is not None: diff --git a/tools/audio/sampleconv/.clang-format b/tools/audio/sampleconv/.clang-format new file mode 100644 index 0000000000..20dda610d7 --- /dev/null +++ b/tools/audio/sampleconv/.clang-format @@ -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'] diff --git a/tools/audio/sampleconv/.gitignore b/tools/audio/sampleconv/.gitignore new file mode 100644 index 0000000000..468e4e0371 --- /dev/null +++ b/tools/audio/sampleconv/.gitignore @@ -0,0 +1,2 @@ +build/ +sampleconv diff --git a/tools/audio/sampleconv/Makefile b/tools/audio/sampleconv/Makefile new file mode 100644 index 0000000000..a023b237e1 --- /dev/null +++ b/tools/audio/sampleconv/Makefile @@ -0,0 +1,36 @@ + +CC := gcc +CFLAGS := -Wall -Wextra -MMD +OPTFLAGS := -Og -g3 +LDFLAGS := + +CLANG_FORMAT := clang-format-14 +FORMAT_ARGS := -i -style=file + +SRC_DIRS := $(shell find src -type d) +C_FILES := $(foreach dir,$(SRC_DIRS),$(wildcard $(dir)/*.c)) +O_FILES := $(foreach f,$(C_FILES:.c=.o),build/$f) + +DEP_FILES := $(foreach f,$(C_FILES:.c=.d),build/$f) + +$(shell mkdir -p build $(foreach dir,$(SRC_DIRS),build/$(dir))) + +.PHONY: all clean distclean format + +all: sampleconv + +clean: + $(RM) -rf build sampleconv + +distclean: clean + +format: + $(CLANG_FORMAT) $(FORMAT_ARGS) $(C_FILES) $(foreach dir,$(SRC_DIRS),$(wildcard $(dir)/*.h)) + +sampleconv: $(O_FILES) + $(CC) $(LDFLAGS) $(O_FILES) -lm -o $@ + +build/src/%.o: src/%.c + $(CC) -c $(CFLAGS) $(OPTFLAGS) $< -o $@ + +-include $(DEP_FILES) diff --git a/tools/audio/sampleconv/src/codec/codec.h b/tools/audio/sampleconv/src/codec/codec.h new file mode 100644 index 0000000000..5ebaae855a --- /dev/null +++ b/tools/audio/sampleconv/src/codec/codec.h @@ -0,0 +1,38 @@ +/** + * 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 CODEC_H_ +#define CODEC_H_ + +#include + +#include "../container/container.h" + +#include "vadpcm.h" +#include "uncompressed.h" + +typedef struct enc_dec_opts { + // Matching + bool matching; + + // VADPCM options + bool truncate; + uint32_t min_loop_length; + table_design_spec design; +} enc_dec_opts; + +typedef struct codec_spec { + const char *name; + sample_data_type type; + int frame_size; + bool compressed; + int (*decode)(container_data *ctnr, const struct codec_spec *codec, const struct enc_dec_opts *opts); + int (*encode)(container_data *ctnr, const struct codec_spec *codec, const struct enc_dec_opts *opts); +} codec_spec; + +#endif diff --git a/tools/audio/sampleconv/src/codec/uncompressed.c b/tools/audio/sampleconv/src/codec/uncompressed.c new file mode 100644 index 0000000000..ccd37f3813 --- /dev/null +++ b/tools/audio/sampleconv/src/codec/uncompressed.c @@ -0,0 +1,56 @@ +/** + * 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 "../util.h" + +#include "codec.h" +#include "../container/container.h" + +int +pcm16_enc_dec(UNUSED container_data *ctnr, UNUSED const codec_spec *codec, UNUSED const enc_dec_opts *opts) +{ + // Since we decode to and encode from pcm16, there's nothing to do. + return 0; +} + +// TODO +int +pcm8_dec(UNUSED container_data *ctnr, UNUSED const codec_spec *codec, UNUSED const enc_dec_opts *opts) +{ +#if 0 + for (size_t i = 0; i < num_samples; i++) { + uint8_t insamp = ((uint8_t *)in)[i]; + + int16_t outsamp = insamp << 8; // - 0x80 before shift ? + + ((int16_t *)out)[i] = outsamp; + } +#endif + return 0; +} + +// TODO +int +pcm8_enc(UNUSED container_data *ctnr, UNUSED const codec_spec *codec, UNUSED const enc_dec_opts *opts) +{ +#if 0 + for (size_t i = 0; i < num_samples; i++) { + uint16_t insamp = ((uint16_t *)in)[i]; + + uint8_t outsamp = insamp >> 8; + + ((uint8_t *)out)[i] = outsamp; + } +#endif + return 0; +} diff --git a/tools/audio/sampleconv/src/codec/uncompressed.h b/tools/audio/sampleconv/src/codec/uncompressed.h new file mode 100644 index 0000000000..41ef2f11d5 --- /dev/null +++ b/tools/audio/sampleconv/src/codec/uncompressed.h @@ -0,0 +1,21 @@ +/** + * 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 CODEC_UNCOMPRESSED_H +#define CODEC_UNCOMPRESSED_H + +int +pcm16_dec(struct container_data *ctnr, const struct codec_spec *codec, const struct enc_dec_opts *opts); + +int +pcm16_enc(struct container_data *ctnr, const struct codec_spec *codec, const struct enc_dec_opts *opts); + +int +pcm16_enc_dec(struct container_data *ctnr, const struct codec_spec *codec, const struct enc_dec_opts *opts); + +#endif diff --git a/tools/audio/sampleconv/src/codec/vadpcm.c b/tools/audio/sampleconv/src/codec/vadpcm.c new file mode 100644 index 0000000000..a77c4948c8 --- /dev/null +++ b/tools/audio/sampleconv/src/codec/vadpcm.c @@ -0,0 +1,1319 @@ +/** + * SPDX-FileCopyrightText: Copyright (C) 2024 ZeldaRET + * SPDX-License-Identifier: CC0-1.0 + */ +#include +#include +#include +#include +#include +#include +#include + +#include "../util.h" + +#include "codec.h" + +int +expand_codebook(int16_t *book_data, int32_t ****table_out, int32_t order, int32_t npredictors) +{ + int32_t ***table = MALLOC_CHECKED_INFO(npredictors * sizeof(int32_t **), "npredictors=%d", npredictors); + + for (int32_t i = 0; i < npredictors; i++) { + table[i] = MALLOC_CHECKED(8 * sizeof(int32_t *)); + for (int32_t j = 0; j < 8; j++) + table[i][j] = MALLOC_CHECKED_INFO((order + 8) * sizeof(int32_t), "order=%d", order); + } + + for (int32_t i = 0; i < npredictors; i++) { + int32_t **table_entry = table[i]; + + for (int32_t j = 0; j < order; j++) { + for (int32_t k = 0; k < 8; k++) + table_entry[k][j] = *(book_data++); + } + + for (int32_t k = 1; k < 8; k++) + table_entry[k][order] = table_entry[k - 1][order - 1]; + + table_entry[0][order] = 1 << 11; + + for (int32_t k = 1; k < 8; k++) { + int32_t j = 0; + + for (; j < k; j++) + table_entry[j][k + order] = 0; + for (; j < 8; j++) + table_entry[j][k + order] = table_entry[j - k][order]; + } + } + + *table_out = table; + return 0; +} + +int +compressed_expanded_codebook(int16_t **book_data_out, int32_t ***table, int order, int npredictors) +{ + int16_t *book_data = + MALLOC_CHECKED_INFO(sizeof(int16_t) * 8 * order * npredictors, "order=%d, npredictors=%d", order, npredictors); + + int n = 0; + for (int32_t i = 0; i < npredictors; i++) { + for (int32_t j = 0; j < order; j++) { + for (int32_t k = 0; k < 8; k++) + book_data[n++] = table[i][k][j]; + } + } + *book_data_out = book_data; + return 0; +} + +int +destroy_expanded_codebook(int32_t ***table, int npredictors) +{ + for (int i = 0; i < npredictors; i++) { + for (int j = 0; j < 8; j++) + free(table[i][j]); + free(table[i]); + } + free(table); + return 0; +} + +static uint32_t +myrand(void) +{ + static uint64_t state = 1619236481962341ull; + state *= 3123692312237ull; + state++; + return state >> 33; +} + +static int16_t +qsample(float x, int32_t scale) +{ + if (x > 0.0f) + return (int16_t)((x / scale) + 0.4999999); + else + return (int16_t)((x / scale) - 0.4999999); +} + +/** + * Round all ('fs' many) values in 'e' to the nearest 'bits'-bit integer, + * outputting to 'ie'. + */ +static void +clamp(int32_t fs, float *e, int32_t *ie, int32_t bits) +{ + int32_t i; + float ulevel; + float llevel; + + llevel = -(float)(1 << (bits - 1)); + ulevel = -llevel - 1.0f; + for (i = 0; i < fs; i++) { + if (e[i] > ulevel) + e[i] = ulevel; + if (e[i] < llevel) + e[i] = llevel; + + if (e[i] > 0.0f) + ie[i] = (int32_t)(e[i] + 0.5); + else + ie[i] = (int32_t)(e[i] - 0.5); + } +} + +static void +clamp_to_s16(float *in, int32_t *out) +{ + for (int i = 0; i < 16; i++) { + if (in[i] > 0x7fff) + in[i] = 0x7fff; + if (in[i] < -0x8000) + in[i] = -0x8000; + + if (in[i] > 0.0f) + out[i] = (int32_t)(in[i] + 0.5); + else + out[i] = (int32_t)(in[i] - 0.5); + } +} + +static int16_t +clamp_bits(int32_t x, int32_t bits) +{ + int32_t lim = 1 << (bits - 1); + + if (x < -lim) + return -lim; + if (x > lim - 1) + return lim - 1; + return x; +} + +static int32_t +inner_product(int32_t length, int32_t *v1, int32_t *v2) +{ + int32_t out = 0; + for (int32_t i = 0; i < length; i++) + out += v1[i] * v2[i]; + + // Compute "out / 2^11", rounded down. + int32_t dout = out / (1 << 11); + int32_t fiout = dout * (1 << 11); + return dout - (out - fiout < 0); +} + +static void +vdecodeframe(uint8_t *frame, int32_t *prescaled, int32_t *state, int32_t order, int32_t ***coef_tbl, int frame_size) +{ + uint8_t header = frame[0]; + int32_t scale = 1 << ((header >> 4) & 0xF); + int32_t optimalp = (header >> 0) & 0xF; + + int32_t ix[16]; + + // Unpack + if (frame_size == 5) { + for (int32_t i = 0; i < 16; i += 4) { + uint8_t c = frame[1 + i / 4]; + ix[i + 0] = (c >> 6) & 0b11; + ix[i + 1] = (c >> 4) & 0b11; + ix[i + 2] = (c >> 2) & 0b11; + ix[i + 3] = (c >> 0) & 0b11; + } + } else { + for (int32_t i = 0; i < 16; i += 2) { + uint8_t c = frame[1 + i / 2]; + ix[i + 0] = (c >> 4) & 0xF; + ix[i + 1] = (c >> 0) & 0xF; + } + } + + // Sign-extend and scale + for (int32_t i = 0; i < 16; i++) { + if (frame_size == 5) { + if (ix[i] >= 2) // 2-bit sign extension + ix[i] -= 4; + } else { + if (ix[i] >= 8) // 4-bit sign extension + ix[i] -= 16; + } + prescaled[i] = ix[i]; // save decompressed frame data before scaling + ix[i] *= scale; + } + + int32_t **coef_page = coef_tbl[optimalp]; + + // Inner product with predictor coefficients + for (int32_t j = 0; j < 2; j++) { + int32_t in_vec[16]; + + for (int32_t i = 0; i < order; i++) + in_vec[i] = state[(2 - j) * 8 - order + i]; + + for (int32_t i = 0; i < 8; i++) { + int32_t ind = j * 8 + i; + + in_vec[order + i] = ix[ind]; + state[ind] = inner_product(order + i, coef_page[i], in_vec) + ix[ind]; + } + } +} + +/** + * Similar to vencodeframe but sources data differently and doubles up certain operations. This is used during + * brute-force decoding of compressed data to decompressed data that matches on round-trip. + */ +static void +my_encodeframe(uint8_t *out, int16_t *in_buf, int32_t *orig_state, int32_t ***coef_tbl, int32_t order, + int32_t npredictors, int frame_size) +{ + int16_t ix[16]; + int32_t prediction[16]; + int32_t in_vec[16]; + int32_t optimalp = 0; + int32_t scale; + int32_t encBits = (frame_size == 5) ? 2 : 4; + int32_t llevel = -(1 << (encBits - 1)); + int32_t ulevel = -llevel - 1; + int32_t ie[16]; + float e[16]; + + // Determine the best-fitting predictor. + float min = 1e30; + int32_t scale_factor = 16 - encBits; + + for (int32_t k = 0; k < npredictors; k++) { + for (int32_t j = 0; j < 2; j++) { + // Copy over the last 'order' samples from the previous output. + for (int32_t i = 0; i < order; i++) + in_vec[i] = (j == 0) ? orig_state[16 - order + i] : in_buf[8 - order + i]; + + // For 8 samples... + for (int32_t i = 0; i < 8; i++) { + // Compute a prediction based on 'order' values from the old state, + // plus previous errors in this chunk, as an inner product with the + // coefficient table. + prediction[j * 8 + i] = inner_product(order + i, coef_tbl[k][i], in_vec); + // Record the error in in_vec (thus, its first 'order' samples + // will contain actual values, the rest will be error terms), and + // in floating point form in e (for no particularly good reason). + in_vec[i + order] = in_buf[j * 8 + i] - prediction[j * 8 + i]; + e[j * 8 + i] = (float)in_vec[i + order]; + } + } + + // Compute the L2 norm of the errors; the lowest norm decides which + // predictor to use. + float se = 0.0f; + for (int32_t j = 0; j < 16; j++) + se += e[j] * e[j]; + + if (se < min) { + min = se; + optimalp = k; + } + } + + // Do exactly the same thing again, for real. + for (int32_t j = 0; j < 2; j++) { + for (int32_t i = 0; i < order; i++) + in_vec[i] = (j == 0) ? orig_state[16 - order + i] : in_buf[8 - order + i]; + + for (int32_t i = 0; i < 8; i++) { + prediction[j * 8 + i] = inner_product(order + i, coef_tbl[optimalp][i], in_vec); + e[j * 8 + i] = in_vec[i + order] = in_buf[j * 8 + i] - prediction[j * 8 + i]; + } + } + + // Clamp the errors to 16-bit signed ints, and put them in ie. + clamp_to_s16(e, ie); + + // Find a value with highest absolute value. + // @bug If this first finds -2^n and later 2^n, it should set 'max' to the + // latter, which needs a higher value for 'scale'. + int32_t max = 0; + for (int32_t i = 0; i < 16; i++) { + if (abs(ie[i]) > abs(max)) + max = ie[i]; + } + + // Compute which power of two we need to scale down by in order to make + // all values representable as 4-bit signed integers (i.e. be in [-8, 7]). + // The worst-case 'max' is -2^15, so this will be at most 12. + for (scale = 0; scale <= scale_factor; scale++) { + if (max <= ulevel && max >= llevel) + break; + max /= 2; + } + + int32_t state[16]; + for (int32_t i = 0; i < 16; i++) + state[i] = orig_state[i]; + + // Try with the computed scale, but if it turns out we don't fit in 4 bits + // (if some |cV| >= 2), use scale + 1 instead (i.e. downscaling by another + // factor of 2). + bool again = true; + for (int32_t nIter = 0; nIter < 2 && again; nIter++) { + again = false; + + if (nIter == 1) + scale++; + + if (scale > scale_factor) + scale = scale_factor; + + for (int32_t j = 0; j < 2; j++) { + int32_t base = j * 8; + + // Copy over the last 'order' samples from the previous output. + for (int32_t i = 0; i < order; i++) + in_vec[i] = (j == 0) ? orig_state[16 - order + i] : state[8 - order + i]; + + // For 8 samples... + for (int32_t i = 0; i < 8; i++) { + // Compute a prediction based on 'order' values from the old state, + // plus previous *quantized* errors in this chunk (because that's + // all the decoder will have available). + prediction[base + i] = inner_product(order + i, coef_tbl[optimalp][i], in_vec); + + // Compute the error, and divide it by 2^scale, rounding to the + // nearest integer. This should ideally result in a 4-bit integer. + float se = (float)in_buf[base + i] - (float)prediction[base + i]; + ix[base + i] = qsample(se, 1 << scale); + + // Clamp the error to a 4-bit signed integer, and record what delta + // was needed for that. + int32_t cV = clamp_bits(ix[base + i], encBits) - ix[base + i]; + if (cV > 1 || cV < -1) + again = true; + ix[base + i] += cV; + + // Record the quantized error in in_vec for later predictions, + // and the quantized (decoded) output in state (for use in the next + // batch of 8 samples). + in_vec[i + order] = ix[base + i] * (1 << scale); + state[base + i] = prediction[base + i] + in_vec[i + order]; + } + } + } + + // Write 1-byte header + out[0] = ((scale & 0xF) << 4) | ((optimalp & 0xF) << 0); + + // Write 4/8-byte frame + if (frame_size == 5) { + for (int32_t i = 0; i < 16; i += 4) { + uint8_t c = ((ix[i + 0] & 0b11) << 6) | ((ix[i + 1] & 0b11) << 4) | ((ix[i + 2] & 0b11) << 2) | + ((ix[i + 3] & 0b11) << 0); + out[1 + i / 4] = c; + } + } else { + for (int32_t i = 0; i < 16; i += 2) { + uint8_t c = ((ix[i + 0] & 0xF) << 4) | ((ix[i + 1] & 0xF) << 0); + out[1 + i / 2] = c; + } + } +} + +static void +permute(int32_t *out, int32_t *in, int32_t *prescaled, int32_t scale, int frame_size) +{ + bool normal = myrand() % 3 == 0; + + for (int32_t i = 0; i < 16; i++) { + int32_t lo = in[i] - scale / 2; + int32_t hi = in[i] + scale / 2; + + if (frame_size == 9) { + if (prescaled[i] == -8 && myrand() % 10 == 0) + lo -= scale * 3 / 2; + else if (prescaled[i] == 7 && myrand() % 10 == 0) + hi += scale * 3 / 2; + } else if (prescaled[i] == -2 && myrand() % 7 == 0) { + lo -= scale * 3 / 2; + } else if (prescaled[i] == 1 && myrand() % 10 == 0) { + hi += scale * 3 / 2; + } else if (normal) { + // + } else if (prescaled[i] == 0) { + if (myrand() % 3) { + lo = in[i] - scale / 5; + hi = in[i] + scale / 5; + } else if (myrand() % 2) { + lo = in[i] - scale / 3; + hi = in[i] + scale / 3; + } + } else if (myrand() % 3) { + if (prescaled[i] < 0) + lo = in[i] + scale / 4; + if (prescaled[i] > 0) + hi = in[i] - scale / 4; + } else if (myrand() % 2) { + if (prescaled[i] < 0) + lo = in[i] - scale / 4; + if (prescaled[i] > 0) + hi = in[i] + scale / 4; + } + + out[i] = clamp_bits(lo + myrand() % (hi - lo + 1), 16); + } +} + +/** + * Like vencodeframe/my_encodeframe but assigns a score to the output for informing brute-force decoding + */ +static int64_t +scored_encode(int32_t *in_buf, int32_t *orig_state, int32_t ***coef_tbl, int32_t order, int32_t npredictors, + int32_t wanted_predictor, int32_t wanted_scale, int32_t wanted_ix[16], int frame_size) +{ + int32_t prediction[16]; + int32_t in_vec[16]; + int32_t optimalp = 0; + int32_t scale; + int32_t enc_bits = (frame_size == 5) ? 2 : 4; + int32_t llevel = -(1 << (enc_bits - 1)); + int32_t ulevel = -llevel - 1; + int32_t ie[16]; + float e[16]; + float errs[4]; + + // Determine the best-fitting predictor. + float min = 1e30; + int32_t scale_factor = 16 - enc_bits; + + int64_t scoreA = 0, scoreB = 0, scoreC = 0; + + for (int32_t k = 0; k < npredictors; k++) { + for (int32_t j = 0; j < 2; j++) { + // Copy over the last 'order' samples from the previous output. + for (int32_t i = 0; i < order; i++) + in_vec[i] = (j == 0) ? orig_state[16 - order + i] : in_buf[8 - order + i]; + + // For 8 samples... + for (int32_t i = 0; i < 8; i++) { + // Compute a prediction based on 'order' values from the old state, + // plus previous errors in this chunk, as an inner product with the + // coefficient table. + prediction[j * 8 + i] = inner_product(order + i, coef_tbl[k][i], in_vec); + // Record the error in in_vec (thus, its first 'order' samples + // will contain actual values, the rest will be error terms), and + // in floating point form in e (for no particularly good reason). + in_vec[i + order] = in_buf[j * 8 + i] - prediction[j * 8 + i]; + e[j * 8 + i] = (float)in_vec[i + order]; + } + } + + // Compute the L2 norm of the errors; the lowest norm decides which + // predictor to use. + float se = 0.0f; + for (int32_t j = 0; j < 16; j++) + se += e[j] * e[j]; + + errs[k] = se; + + if (se < min) { + min = se; + optimalp = k; + } + } + + for (int32_t k = 0; k < npredictors; k++) { + if (errs[k] < errs[wanted_predictor]) + scoreA += (int64_t)(errs[wanted_predictor] - errs[k]); + } + + if (optimalp != wanted_predictor) { + // probably penalized above, but add extra penalty in case the error + // values were the exact same + scoreA++; + } + optimalp = wanted_predictor; + + int32_t **coefPage = coef_tbl[optimalp]; + + // Do exactly the same thing again, for real. + for (int32_t j = 0; j < 2; j++) { + for (int32_t i = 0; i < order; i++) + in_vec[i] = (j == 0 ? orig_state[16 - order + i] : in_buf[8 - order + i]); + + for (int32_t i = 0; i < 8; i++) { + prediction[j * 8 + i] = inner_product(order + i, coefPage[i], in_vec); + e[j * 8 + i] = in_vec[i + order] = in_buf[j * 8 + i] - prediction[j * 8 + i]; + } + } + + // Clamp the errors to 16-bit signed ints, and put them in ie. + clamp_to_s16(e, ie); + + // Find a value with highest absolute value. + // @bug If this first finds -2^n and later 2^n, it should set 'max' to the + // latter, which needs a higher value for 'scale'. + int32_t max = 0; + for (int32_t i = 0; i < 16; i++) { + if (abs(ie[i]) > abs(max)) + max = ie[i]; + } + + // Compute which power of two we need to scale down by in order to make + // all values representable as 4-bit signed integers (i.e. be in [-8, 7]). + // The worst-case 'max' is -2^15, so this will be at most 12. + for (scale = 0; scale <= scale_factor; scale++) { + if (max <= ulevel && max >= llevel) + break; + max /= 2; + } + + // Preliminary ix computation, computes whether scale needs to be incremented + int32_t state[16]; + + // Try with the computed scale, but if it turns out we don't fit in 4 bits + // (if some |cV| >= 2), use scale + 1 instead (i.e. downscaling by another + // factor of 2). + bool again = false; + for (int32_t j = 0; j < 2; j++) { + int32_t base = j * 8; + + // Copy over the last 'order' samples from the previous output. + for (int32_t i = 0; i < order; i++) + in_vec[i] = (j == 0) ? orig_state[16 - order + i] : state[8 - order + i]; + + // For 8 samples... + for (int32_t i = 0; i < 8; i++) { + // Compute a prediction based on 'order' values from the old state, + // plus previous *quantized* errors in this chunk (because that's + // all the decoder will have available). + prediction[base + i] = inner_product(order + i, coefPage[i], in_vec); + + // Compute the error, and divide it by 2^scale, rounding to the + // nearest integer. This should ideally result in a 4-bit integer. + float se = (float)in_buf[base + i] - (float)prediction[base + i]; + int32_t ix = qsample(se, 1 << scale); + int32_t clampedIx = clamp_bits(ix, enc_bits); + + // Clamp the error to a 4-bit signed integer, and record what delta + // was needed for that. + int32_t cV = clampedIx - ix; + if (cV > 1 || cV < -1) + again = true; + + // Record the quantized error in in_vec for later predictions, + // and the quantized (decoded) output in state (for use in the next + // batch of 8 samples). + in_vec[i + order] = clampedIx * (1 << scale); + state[base + i] = prediction[base + i] + in_vec[i + order]; + } + } + + if (again && scale < scale_factor) + scale++; + + if (scale != wanted_scale) { + // We could do math to penalize scale mismatches accurately, but it's + // simpler to leave it as a constraint by setting an infinite penalty. + scoreB += 100000000; + scale = wanted_scale; + } + + // Then again for real, but now also with penalty computation + for (int32_t j = 0; j < 2; j++) { + int32_t base = j * 8; + + // Copy over the last 'order' decoded samples from the above chunk. + for (int32_t i = 0; i < order; i++) + in_vec[i] = (j == 0) ? orig_state[16 - order + i] : state[8 - order + i]; + + // ... and do the same thing as before. + for (int32_t i = 0; i < 8; i++) { + prediction[base + i] = inner_product(order + i, coefPage[i], in_vec); + + int64_t ise = (int64_t)in_buf[base + i] - (int64_t)prediction[base + i]; + float se = (float)in_buf[base + i] - (float)prediction[base + i]; + int32_t ix = qsample(se, 1 << scale); + int32_t clampedIx = clamp_bits(ix, enc_bits); + int32_t val = wanted_ix[base + i] * (1 << scale); + + if (clampedIx != wanted_ix[base + i]) { + assert(ix != wanted_ix[base + i]); + + int32_t lo = val - (1 << scale) / 2; + int32_t hi = val + (1 << scale) / 2; + + int64_t diff = 0; + if (ise < lo) + diff = lo - ise; + else if (ise > hi) + diff = ise - hi; + + scoreC += diff * diff + 1; + } + in_vec[i + order] = val; + state[base + i] = prediction[base + i] + val; + } + } + + // Penalties for going outside int16_t + for (int32_t i = 0; i < 16; i++) { + int64_t diff = 0; + + if (in_buf[i] < -0x8000) + diff = -0x8000 - in_buf[i]; + if (in_buf[i] > 0x7fff) + diff = in_buf[i] - 0x7fff; + + scoreC += diff * diff; + } + + return scoreA + scoreB + 10 * scoreC; +} + +static bool +descent(int32_t guess[16], int32_t min_vals[16], int32_t max_vals[16], int32_t prev_state[16], int32_t ***coef_tbl, + int32_t order, int32_t npredictors, int32_t wanted_predictor, int32_t wanted_scale, int32_t wanted_ix[16], + int frame_size) +{ + const double inf = 1e100; + + int64_t curScore = scored_encode(guess, prev_state, coef_tbl, order, npredictors, wanted_predictor, wanted_scale, + wanted_ix, frame_size); + + while (true) { + double delta[16]; + + if (curScore == 0) + return true; + + // Compute gradient, and how far to move along it at most. + double maxMove = inf; + for (int32_t i = 0; i < 16; i++) { + guess[i]++; + int64_t scoreUp = scored_encode(guess, prev_state, coef_tbl, order, npredictors, wanted_predictor, + wanted_scale, wanted_ix, frame_size); + guess[i] -= 2; + int64_t scoreDown = scored_encode(guess, prev_state, coef_tbl, order, npredictors, wanted_predictor, + wanted_scale, wanted_ix, frame_size); + guess[i]++; + + if (scoreUp >= curScore && scoreDown >= curScore) { + // Don't touch this coordinate + delta[i] = 0; + continue; + } + + int32_t val = (scoreDown < scoreUp) ? min_vals[i] : max_vals[i]; + double ds = (scoreDown < scoreUp) ? (scoreDown - curScore) : (curScore - scoreUp); + + if (guess[i] == val) { + delta[i] = 0; + } else { + delta[i] = ds; + maxMove = fmin(maxMove, (val - guess[i]) / delta[i]); + } + } + if (maxMove == inf || maxMove <= 0) { + return false; + } + + // Try exponentially spaced candidates along the gradient. + int32_t nguess[16]; + int32_t bestGuess[16]; + int64_t bestScore = curScore; + while (true) { + bool changed = false; + + for (int32_t i = 0; i < 16; i++) { + nguess[i] = (int32_t)round(guess[i] + delta[i] * maxMove); + + if (nguess[i] != guess[i]) + changed = true; + } + + if (!changed) + break; + + int64_t score = scored_encode(nguess, prev_state, coef_tbl, order, npredictors, wanted_predictor, + wanted_scale, wanted_ix, frame_size); + if (score < bestScore) { + bestScore = score; + memcpy(bestGuess, nguess, sizeof(nguess)); + } + maxMove *= 0.7; + } + + if (bestScore == curScore) + return false; // No improvements along that line, give up. + + curScore = bestScore; + memcpy(guess, bestGuess, sizeof(bestGuess)); + } +} + +static void +get_bounds(int32_t *in, int32_t *prescaled, int32_t scale, int32_t *min_vals, int32_t *max_vals, int frame_size) +{ + int32_t minv, maxv; + + if (frame_size == 9) { + minv = -8; + maxv = 8 - 1; + } else { + minv = -2; + maxv = 2 - 1; + } + + for (int32_t i = 0; i < 16; i++) { + int32_t lo = in[i] - scale / 2 - scale; + int32_t hi = in[i] + scale / 2 + scale; + + if (prescaled[i] == minv) + lo -= scale; + else if (prescaled[i] == maxv) + hi += scale; + + min_vals[i] = lo; + max_vals[i] = hi; + } +} + +#define VADPCM_BRUTEFORCE_LIMIT 100000 + +static int32_t +bruteforce(int32_t guess[16], uint8_t input[9], int32_t decoded[16], int32_t prescaled[16], int32_t prev_state[16], + int32_t ***coef_tbl, int32_t order, int32_t npredictors, int frame_size) +{ + int32_t scale = (input[0] >> 4) & 0xF; + int32_t predictor = (input[0] >> 0) & 0xF; + + int32_t min_vals[16]; + int32_t max_vals[16]; + get_bounds(decoded, prescaled, 1 << scale, min_vals, max_vals, frame_size); + + int i = 0; + while (true) { + int64_t bestScore = -1; + int32_t bestGuess[16]; + + for (int tries = 0; tries < 100; tries++) { + permute(guess, decoded, prescaled, 1 << scale, frame_size); + + int64_t score = + scored_encode(guess, prev_state, coef_tbl, order, npredictors, predictor, scale, prescaled, frame_size); + if (score == 0) + return true; + + if (bestScore == -1 || score < bestScore) { + bestScore = score; + memcpy(bestGuess, guess, sizeof(bestGuess)); + } + } + + memcpy(guess, bestGuess, sizeof(bestGuess)); + + if (descent(guess, min_vals, max_vals, prev_state, coef_tbl, order, npredictors, predictor, scale, prescaled, + frame_size)) + return true; + + i++; + if (i == VADPCM_BRUTEFORCE_LIMIT) + error("Bruteforce decoding failed"); + } +} + +/** + * vadpcm encoder used when encoding data + */ +static void +vencodeframe(uint8_t *out_buf, int16_t *in_buf, int32_t *state, int32_t ***coef_tbl, int32_t order, int32_t npredictors, + int frame_size) +{ + int32_t in_vec[16]; + int32_t prediction[16]; + int32_t optimalp; + float e[16]; + float se; + float min = 1e30; + int32_t i; + int32_t j; + int32_t k; + + // Determine the best-fitting predictor. + + optimalp = 0; + for (k = 0; k < npredictors; k++) { + // Copy over the last 'order' samples from the previous output. + for (i = 0; i < order; i++) + in_vec[i] = state[16 - order + i]; + + // For 8 samples... + for (i = 0; i < 8; i++) { + // Compute a prediction based on 'order' values from the old state, + // plus previous *quantized* errors in this chunk (because that's + // all the decoder will have available). + prediction[i] = inner_product(order + i, coef_tbl[k][i], in_vec); + + // Record the error in in_vec (thus, its first 'order' samples + // will contain actual values, the rest will be error terms), and + // in floating point form in e (for no particularly good reason). + in_vec[i + order] = in_buf[i] - prediction[i]; + e[i] = (float)in_vec[i + order]; + } + + // For the next 8 samples, start with 'order' values from the end of + // the previous 8-sample chunk of in_buf. (The code is equivalent to + // in_vec[i] = in_buf[8 - order + i].) + for (i = 0; i < order; i++) + in_vec[i] = prediction[8 - order + i] + in_vec[8 + i]; + + // ... and do the same thing as before to get predictions. + for (i = 0; i < 8; i++) { + prediction[8 + i] = inner_product(order + i, coef_tbl[k][i], in_vec); + in_vec[i + order] = in_buf[8 + i] - prediction[8 + i]; + e[8 + i] = (float)in_vec[i + order]; + } + + // Compute the L2 norm of the errors; the lowest norm decides which predictor to use. + se = 0.0f; + for (j = 0; j < 16; j++) + se += e[j] * e[j]; + + if (se < min) { + min = se; + optimalp = k; + } + } + + // Do exactly the same thing again, for real. + + for (i = 0; i < order; i++) + in_vec[i] = state[16 - order + i]; + + for (i = 0; i < 8; i++) { + prediction[i] = inner_product(order + i, coef_tbl[optimalp][i], in_vec); + in_vec[i + order] = in_buf[i] - prediction[i]; + e[i] = (float)in_vec[i + order]; + } + + for (i = 0; i < order; i++) + in_vec[i] = prediction[8 - order + i] + in_vec[8 + i]; + + for (i = 0; i < 8; i++) { + prediction[8 + i] = inner_product(order + i, coef_tbl[optimalp][i], in_vec); + in_vec[i + order] = in_buf[8 + i] - prediction[8 + i]; + e[8 + i] = (float)in_vec[i + order]; + } + + int32_t ie[16]; + int32_t max = 0; + + // Clamp the errors to 16-bit signed ints, and put them in ie. + clamp(16, e, ie, 16); + + // Find a value with highest absolute value. + // Reproduce original tool bug: + // If this first finds -2^n and later 2^n, it should set 'max' to the + // latter, which needs a higher value for 'scale'. + for (i = 0; i < 16; i++) { + if (abs(ie[i]) > abs(max)) + max = ie[i]; + } + + // Compute which power of two we need to scale down by in order to make + // all values representable as 4-bit (resp. 2-bit) signed integers + // (i.e. be in [-8, 7] (resp. [-2, 1])). + // The worst-case 'max' is -2^15, so this will be at most 12 (resp. 14). + // Depends on whether we are encoding for long or short VADPCM. + + int32_t enc_bits = (frame_size == 5) ? 2 : 4; + int32_t scale_factor = 16 - enc_bits; + int32_t llevel = -(1 << (enc_bits - 1)); + int32_t ulevel = -llevel - 1; + int32_t scale; + + for (scale = 0; scale <= scale_factor; scale++) { + if (max <= ulevel && max >= llevel) + break; + max /= 2; + } + + int32_t saveState[16]; + memcpy(saveState, state, sizeof(saveState)); + + int16_t ix[16]; + int32_t nIter; + int32_t cV; + int32_t maxClip; + + // Try with the computed scale, but if it turns out we don't fit in 4 bits + // (if some |cV| >= 2), use scale + 1 instead (i.e. downscaling by another + // factor of 2). + scale--; + nIter = 0; + do { + nIter++; + maxClip = 0; + scale++; + + if (scale > scale_factor) + scale = scale_factor; + + // Copy over the last 'order' samples from the previous output. + for (i = 0; i < order; i++) + in_vec[i] = saveState[16 - order + i]; + + // For 8 samples... + for (i = 0; i < 8; i++) { + // Compute a prediction based on 'order' values from the old state, + // plus previous *quantized* errors in this chunk (because that's + // all the decoder will have available). + prediction[i] = inner_product(order + i, coef_tbl[optimalp][i], in_vec); + + // Compute the error, and divide it by 2^scale, rounding to the + // nearest integer. This should ideally result in a 4-bit integer. + se = (float)in_buf[i] - (float)prediction[i]; + ix[i] = qsample(se, 1 << scale); + + // Clamp the error to a 4-bit signed integer, and record what delta + // was needed for that. + cV = clamp_bits(ix[i], enc_bits) - ix[i]; + if (maxClip < abs(cV)) + maxClip = abs(cV); + ix[i] += cV; + + // Record the quantized error in in_vec for later predictions, + // and the quantized (decoded) output in state (for use in the next + // batch of 8 samples). + in_vec[i + order] = ix[i] * (1 << scale); + state[i] = prediction[i] + in_vec[i + order]; + } + + // Copy over the last 'order' decoded samples from the above chunk. + for (i = 0; i < order; i++) + in_vec[i] = state[8 - order + i]; + + // ... and do the same thing as before. + for (i = 0; i < 8; i++) { + prediction[8 + i] = inner_product(order + i, coef_tbl[optimalp][i], in_vec); + + se = (float)in_buf[8 + i] - (float)prediction[8 + i]; + ix[8 + i] = qsample(se, 1 << scale); + + cV = clamp_bits(ix[8 + i], enc_bits) - ix[8 + i]; + if (maxClip < abs(cV)) + maxClip = abs(cV); + + ix[8 + i] += cV; + in_vec[i + order] = ix[8 + i] * (1 << scale); + state[8 + i] = prediction[8 + i] + in_vec[i + order]; + } + } while (maxClip >= 2 && nIter < 2); + + // The scale, the predictor index, and the 16 computed outputs are now all + // 4-bit numbers. Write them out as either 1 + 8 bytes or 1 + 4 bytes depending + // on whether we are encoding for long or short VADPCM. + + *(out_buf++) = ((scale & 0b1111) << 4) | ((optimalp & 0b1111) << 0); + + switch (frame_size) { + case 5: + for (i = 0; i < 16; i += 4) + *(out_buf++) = ((ix[i + 0] & 0b11) << 6) | ((ix[i + 1] & 0b11) << 4) | ((ix[i + 2] & 0b11) << 2) | + ((ix[i + 3] & 0b11) << 0); + break; + case 9: + for (i = 0; i < 16; i += 2) + *(out_buf++) = ((ix[i + 0] & 0b1111) << 4) | ((ix[i + 1] & 0b1111) << 0); + break; + } +} + +int +vadpcm_enc(container_data *ctnr, const codec_spec *codec, const enc_dec_opts *opts) +{ + if (ctnr->num_channels != 1) + error("Only mono input can be converted to vadpcm, was %u-channel", ctnr->num_channels); + if (ctnr->bit_depth != 16) + error("Only 16-bit samples can be converted to vadpcm, was %u-bit", ctnr->bit_depth); + + int frame_size = codec->frame_size; + + int32_t state[16]; + memset(state, 0, sizeof(state)); + + unsigned int nloops = ctnr->num_loops; + if (nloops > 1) + error("Only 1 loop supported"); + + unsigned i; + unsigned j; + + // synthesize ADPCM loop structures, state will be filled later + ALADPCMloop *aloops = NULL; + if (nloops != 0) { + container_loop *loops = ctnr->loops; + aloops = MALLOC_CHECKED_INFO(nloops * sizeof(ALADPCMloop), "nloops=%u", nloops); + + for (i = 0; i < nloops; i++) { + aloops[i].start = loops[i].start; + aloops[i].end = loops[i].end; + aloops[i].count = loops[i].num; + } + ctnr->vadpcm.loop_version = 1; + ctnr->vadpcm.loops = aloops; + ctnr->vadpcm.num_loops = nloops; + } + + ctnr->vadpcm.book_version = 1; + if (!ctnr->vadpcm.has_book) { + // If there is no prediction codebook embedded in the input file, design one for the data + tabledesign_run(&ctnr->vadpcm.book_header.order, &ctnr->vadpcm.book_header.npredictors, &ctnr->vadpcm.book_data, + ctnr->data, ctnr->num_samples, &opts->design); + ctnr->vadpcm.has_book = true; + } + + uint32_t currentPos = 0; + int nRepeats; + uint32_t nBytes = 0; + + int order = ctnr->vadpcm.book_header.order; + int npredictors = ctnr->vadpcm.book_header.npredictors; + + int32_t ***coef_tbl; + expand_codebook(ctnr->vadpcm.book_data, &coef_tbl, ctnr->vadpcm.book_header.order, + ctnr->vadpcm.book_header.npredictors); + + int16_t in_buf[16]; + + uint16_t *indata = ctnr->data; + uint8_t *outdata = MALLOC_CHECKED_INFO(ctnr->data_size, "data_size=%lu", ctnr->data_size); + + unsigned nFrames = ctnr->num_samples; + /* printf("Num samples: %u\n", nFrames); */ + + for (i = 0; i < nloops; i++) { + // printf("Process loop: start=%u end=%u count=%u\n", aloops[i].start, aloops[i].end, aloops[i].count); + + // Duplicate the loop until it's longer than the minimum loop length + nRepeats = 0; + uint32_t newEnd = aloops[i].end; + while (newEnd - aloops[i].start < opts->min_loop_length) { + newEnd += aloops[i].end - aloops[i].start; + nRepeats++; + } + + // Encode [current, loop_start) + while (currentPos <= aloops[i].start) { + // printf(" pre-loop %u / %u\n", currentPos, aloops[i].start); + + memcpy(in_buf, &indata[currentPos], sizeof(in_buf)); + currentPos += 16; + + vencodeframe(&outdata[nBytes], in_buf, state, coef_tbl, order, npredictors, frame_size); + nBytes += frame_size; + } + + // Emplace loop state + // printf(" wr loop state\n"); + for (j = 0; j < 16; j++) { + if (state[j] > 0x7FFF) + state[j] = 0x7FFF; + if (state[j] < -0x7FFF) + state[j] = -0x7FFF; + aloops[i].state[j] = state[j]; + } + + aloops[i].count = -1; + + // Encode the loop for n repeats + while (nRepeats > 0) { + // printf(" repeat #%u\n", nRepeats); + // Encode [loop_start, loop_end] + while (currentPos + 16 < aloops[i].end) { + // printf(" loop-repeat %u / %u\n", currentPos, aloops[i].end); + + memcpy(in_buf, &indata[currentPos], sizeof(in_buf)); + currentPos += 16; + + vencodeframe(&outdata[nBytes], in_buf, state, coef_tbl, order, npredictors, frame_size); + nBytes += frame_size; + } + + // Handling for when loop_end is halfway through a frame + + uint32_t left = aloops[i].end - currentPos; + // printf(" rem=%u\n", left); + + memcpy(in_buf, &indata[currentPos], left * sizeof(int16_t)); + + currentPos = aloops[i].start * 2; + + memcpy(in_buf + left, &indata[currentPos], (16 - left) * sizeof(int16_t)); + + vencodeframe(&outdata[nBytes], in_buf, state, coef_tbl, order, npredictors, frame_size); + nBytes += frame_size; + + // Return to loop start + + currentPos = aloops[i].start - left + 16; + nRepeats--; + } + aloops[i].end = newEnd; + } + + // Get frame count, truncate to loop end if desired + if (nloops > 0 && opts->truncate) + nFrames = MIN(aloops[nloops - 1].end + 16, nFrames); + + // Encode remaining data + + while (currentPos < nFrames) { + uint32_t nsam = MIN(nFrames - currentPos, 16); + // printf(" post-loop: %u / %u (%u)\n", currentPos, nFrames, nsam); + + memcpy(in_buf, &indata[currentPos], sizeof(int16_t) * nsam); + currentPos += nsam; + + memset(in_buf, 0, (16 - nsam) * sizeof(int16_t)); + + vencodeframe(&outdata[nBytes], in_buf, state, coef_tbl, order, npredictors, frame_size); + nBytes += frame_size; + } + + // Pad to even number + if (nBytes % 2 != 0) + outdata[nBytes++] = 0; + + assert(nBytes <= ctnr->data_size); + + // Write out + + ctnr->num_loops = 0; + if (ctnr->loops != NULL) + free(ctnr->loops); + ctnr->loops = NULL; + + free(ctnr->data); + + ctnr->data = outdata; + ctnr->data_size = nBytes; + ctnr->data_type = (frame_size == 5) ? SAMPLE_TYPE_VADPCM_HALF : SAMPLE_TYPE_VADPCM; + + destroy_expanded_codebook(coef_tbl, npredictors); + + return 0; +} + +int +vadpcm_dec(container_data *ctnr, UNUSED const codec_spec *codec, const enc_dec_opts *opts) +{ + if (ctnr->num_channels != 1) + error("vadpcm can only be mono, was %u-channel", ctnr->num_channels); + if (ctnr->bit_depth != 16) + error("vadpcm can only be converted to 16-bit samples, was %u-bit", ctnr->bit_depth); + + if (!ctnr->vadpcm.has_book) + error("Codebook missing from bitstream"); + + int16_t order = ctnr->vadpcm.book_header.order; + int16_t npredictors = ctnr->vadpcm.book_header.npredictors; + int16_t nloops = ctnr->vadpcm.num_loops; + + if (nloops > 1) + error("AIFC should only have at most 1 loop"); + + ALADPCMloop *aloops = ctnr->vadpcm.loops; + + int32_t ***coef_tbl = NULL; + expand_codebook(ctnr->vadpcm.book_data, &coef_tbl, order, npredictors); + + int32_t state[16]; + int32_t prescaled[16]; + int32_t in_pos = 0; + int32_t cur_pos = 0; + uint32_t nSamples = (ctnr->num_samples + 15) & ~15; + + int frame_size = (ctnr->data_type == SAMPLE_TYPE_VADPCM_HALF) ? 5 : 9; + + uint8_t *input_buf = ctnr->data; + + for (int32_t i = 0; i < order; i++) + state[15 - i] = 0; + + // Align to (decoded) frame size, the original undecompressed sample may have had only a fraction of a frame at the + // end but we always decompress to entire frames. + size_t output_size = nSamples * sizeof(int16_t); + uint8_t *output_buf = MALLOC_CHECKED_INFO(output_size, "output_size=%lu", output_size); + + int fails = 0; + + for (size_t frame_number = 0; frame_number * 16 < nSamples; frame_number++) { + uint8_t input[9]; + uint8_t encoded[9]; + int32_t prev_state[16]; + int32_t decoded[16]; + int16_t guess[16]; + int16_t origGuess[16]; + + memcpy(prev_state, state, sizeof(state)); + + memcpy(input, &input_buf[in_pos], frame_size); + in_pos += frame_size; + + // Initial decode using the standard decoder + vdecodeframe(input, prescaled, state, order, coef_tbl, frame_size); + + // Create a guess from that, by clamping to 16 bits + for (int32_t i = 0; i < 16; i++) + guess[i] = clamp_bits(state[i], 16); + + // If we aren't going for matching, the initial decode is sufficient and we skip re-encoding and bruteforcing + if (opts->matching) { + memcpy(decoded, state, sizeof(state)); + memcpy(origGuess, guess, sizeof(guess)); + + // Encode the guess + my_encodeframe(encoded, guess, prev_state, coef_tbl, order, npredictors, frame_size); + + if (memcmp(input, encoded, frame_size) != 0) { + // If it doesn't match, bruteforce the matching. + + int32_t guess32[16]; + if (bruteforce(guess32, input, decoded, prescaled, prev_state, coef_tbl, order, npredictors, + frame_size)) { + for (int i = 0; i < 16; i++) { + assert(-0x8000 <= guess32[i] && guess32[i] <= 0x7fff); + guess[i] = guess32[i]; + } + + my_encodeframe(encoded, guess, prev_state, coef_tbl, order, npredictors, frame_size); + assert(memcmp(input, encoded, frame_size) == 0); + } else { + fails++; + error("FAIL [%d/%d]\n", cur_pos, nSamples); + } + + // Bring the match closer to the original decode (not strictly + // necessary, but it will move us closer to the target on average). + for (int32_t failures = 0; failures < 50; failures++) { + int32_t ind = myrand() % 16; + int32_t old = guess[ind]; + + if (old == origGuess[ind]) + continue; + + guess[ind] = origGuess[ind]; + + if (myrand() % 2) + guess[ind] += (old - origGuess[ind]) / 2; + + my_encodeframe(encoded, guess, prev_state, coef_tbl, order, npredictors, frame_size); + + if (memcmp(input, encoded, frame_size) == 0) + failures = -1; + else + guess[ind] = old; + } + } + } + + memcpy(output_buf + cur_pos * 2, guess, sizeof(guess)); + cur_pos += 16; + } + + if (fails != 0) + error("Decoding failures: %d\n", fails); + + // Convert VADPCM loop to regular loop, if it exists + + if (nloops != 0) { + container_loop *loop = MALLOC_CHECKED(sizeof(container_loop)); + *loop = (container_loop){ + .id = 0, + .type = LOOP_FORWARD, + .start = aloops[0].start, + .end = aloops[0].end, + .fraction = 0, + .num = aloops[0].count, + }; + ctnr->num_loops = 1; + ctnr->loops = loop; + + ctnr->vadpcm.num_loops = 0; + free(ctnr->vadpcm.loops); + ctnr->vadpcm.loops = NULL; + } + + // Assign new data + + free(ctnr->data); + ctnr->data = output_buf; + ctnr->data_size = output_size; + ctnr->data_type = SAMPLE_TYPE_PCM16; + ctnr->num_samples = nSamples; + + destroy_expanded_codebook(coef_tbl, npredictors); + return 0; +} diff --git a/tools/audio/sampleconv/src/codec/vadpcm.h b/tools/audio/sampleconv/src/codec/vadpcm.h new file mode 100644 index 0000000000..f6a080bf84 --- /dev/null +++ b/tools/audio/sampleconv/src/codec/vadpcm.h @@ -0,0 +1,50 @@ +/** + * SPDX-FileCopyrightText: Copyright (C) 2024 ZeldaRET + * SPDX-License-Identifier: CC0-1.0 + */ +#ifndef CODEC_VADPCM_H +#define CODEC_VADPCM_H + +#include +#include + +#define VADPCM_BOOK_SIZE(order, npredictors) (8 * (order) * (npredictors)) +#define VADPCM_BOOK_SIZE_BYTES(order, npredictors) (sizeof(int16_t) * VADPCM_BOOK_SIZE(order, npredictors)) + +typedef struct { + int16_t order; + int16_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 { + unsigned int order; + unsigned int bits; + unsigned int refine_iters; + double thresh; + unsigned int frame_size; +} table_design_spec; + +int +tabledesign_run(int16_t *order_out, int16_t *npredictors_out, int16_t **book_data_out, void *sample_data, + size_t num_samples, const table_design_spec *design); + +struct container_data; +struct codec_spec; +struct enc_dec_opts; + +int +vadpcm_enc(struct container_data *ctnr, const struct codec_spec *codec, const struct enc_dec_opts *opts); + +int +vadpcm_dec(struct container_data *ctnr, const struct codec_spec *codec, const struct enc_dec_opts *opts); + +#endif diff --git a/tools/audio/sampleconv/src/codec/vadpcm_tabledesign.c b/tools/audio/sampleconv/src/codec/vadpcm_tabledesign.c new file mode 100644 index 0000000000..90bad83499 --- /dev/null +++ b/tools/audio/sampleconv/src/codec/vadpcm_tabledesign.c @@ -0,0 +1,655 @@ +/** + * SPDX-FileCopyrightText: Copyright (C) 2024 ZeldaRET + * SPDX-License-Identifier: CC0-1.0 + */ +#include +#include +#include +#include + +#include "../util.h" +#include "vadpcm.h" + +// Levinson-Durbin algorithm for iteratively solving for prediction coefficients +// https://en.wikipedia.org/wiki/Levinson_recursion +static int +durbin(double *acvec, int order, double *reflection_coeffs, double *prediction_coeffs, double *error) +{ + int i, j; + double sum, E; + int ret; + + prediction_coeffs[0] = 1.0; + E = acvec[0]; // E[0] = r{xx}[0] + ret = 0; + + for (i = 1; i <= order; i++) { + // SUM(j, a[i-1][j] * r{xx}[i - j] ) + sum = 0.0; + for (j = 1; j <= i - 1; j++) { + sum += prediction_coeffs[j] * acvec[i - j]; + } + + // a[i][i] = -Delta[i-1] / E[i-1] + prediction_coeffs[i] = (E > 0.0) ? (-(acvec[i] + sum) / E) : 0.0; + // k[i] = a[i][i] + reflection_coeffs[i] = prediction_coeffs[i]; + + if (fabs(reflection_coeffs[i]) > 1.0) { + // incr when a predictor coefficient is > 1 (indicates numerical instability) + ret++; + } + + for (j = 1; j < i; j++) { + // a[i][j] = a[i-1][j] + a[i-1][i - j] * a[i][i] + prediction_coeffs[j] += prediction_coeffs[i - j] * prediction_coeffs[i]; + } + + // E[i] = E[i-1] * (1.0 - k[i] ** 2) + // = E[i-1] * (1.0 - a[i][i] ** 2) + E *= 1.0 - prediction_coeffs[i] * prediction_coeffs[i]; + } + *error = E; + return ret; +} + +// Reflection coefficients (k) -> Predictor coefficients (a) +// A subset of Levinson-Durbin that only computes predictors from known reflection coefficients and previous predictors +static void +afromk(double *k, double *ai, int order) +{ + int i, j; + + ai[0] = 1.0; + + for (i = 1; i <= order; i++) { + // a[i][i] = k[i] + ai[i] = k[i]; + + for (j = 1; j <= i - 1; j++) { + // a[i][j] = a[i-1][j] + a[i-1][i - j] * k[i] = a[i-1][j] + a[i-1][i - j] * a[i][i] + ai[j] += ai[i - j] * ai[i]; + } + } +} + +// Prediction coefficients (a) -> Reflection coefficients (k) +// Performs afromk in reverse? +// Returns 0 if numerically stable, otherwise returns non-zero +static int +kfroma(double *in, double *out, int order) +{ + int i, j; + double div; + double temp; + double next[(order + 1)]; + int ret = 0; + + out[order] = in[order]; + + for (i = order - 1; i >= 1; i--) { + for (j = 0; j <= i; j++) { + temp = out[i + 1]; + div = 1.0 - temp * temp; + + if (div == 0.0) { + return 1; + } + + next[j] = (in[j] - in[i - j + 1] * temp) / div; + } + + for (j = 0; j <= i; j++) { + in[j] = next[j]; + } + + out[i] = next[i]; + if (fabs(out[i]) > 1.0) { + // Not numerically stable + ret++; + } + } + + return ret; +} + +// autocorrelation (r{xx}) from predictors (a) ? +static void +rfroma(double *in, int n, double *out) +{ + int i, j; + double mat[n + 1][n + 1]; + double div; + + mat[n][0] = 1.0; + for (i = 1; i <= n; i++) { + mat[n][i] = -in[i]; + } + + for (i = n; i >= 1; i--) { + div = 1.0 - mat[i][i] * mat[i][i]; + + for (j = 1; j <= i - 1; j++) { + mat[i - 1][j] = (mat[i][i - j] * mat[i][i] + mat[i][j]) / div; + } + } + + out[0] = 1.0; + for (i = 1; i <= n; i++) { + out[i] = 0.0; + for (j = 1; j <= i; j++) { + out[i] += mat[i][j] * out[i - j]; + } + } +} + +static double +model_dist(double *predictors, double *data, int order) +{ + double autocorrelation_data[order + 1]; + double autocorrelation_predictors[order + 1]; + double ret; + int i, j; + + // autocorrelation from data + rfroma(data, order, autocorrelation_data); + + // autocorrelation from predictors + for (i = 0; i <= order; i++) { + autocorrelation_predictors[i] = 0.0; + for (j = 0; j <= order - i; j++) { + autocorrelation_predictors[i] += predictors[j] * predictors[i + j]; + } + } + + // compute "model distance" (scaled L2 norm: 2 * inner(ac1, ac2) ) + ret = autocorrelation_data[0] * autocorrelation_predictors[0]; + for (i = 1; i <= order; i++) { + ret += 2 * autocorrelation_data[i] * autocorrelation_predictors[i]; + } + + return ret; +} + +// Calculate the autocorrelation matrix of two vectors at x and x - xlen +// https://en.wikipedia.org/wiki/Autocorrelation +static void +acmat(int16_t *x, int order, int xlen, double **ac) +{ + int i, j, k; + + for (i = 1; i <= order; i++) { + for (j = 1; j <= order; j++) { + // R{xx}[i,j] = E[X[i] * X[j]] + + ac[i][j] = 0.0; + for (k = 0; k < xlen; k++) { + ac[i][j] += x[k - i] * x[k - j]; + } + } + } +} + +// Computes the autocorrelation vector of two vectors at x and x - xlen +static void +acvect(int16_t *x, int order, int xlen, double *ac) +{ + int i, j; + + for (i = 0; i <= order; i++) { + ac[i] = 0.0; + // r{xx} = E(x(m)x) = SUM(j, x[j - i] * x[j]) + for (j = 0; j < xlen; j++) { + ac[i] -= x[j - i] * x[j]; + } + } +} + +/** + * Lower-Upper (with Permutation vector) (LUP) Decomposition + * + * Replaces a real n-by-n matrix "a" with the LU decomposition of a row-wise + * permutation of itself. + * + * Input parameters: + * a: The matrix which is operated on. 1-indexed; it should be of size + * (n+1) x (n+1), and row/column index 0 is not used. + * n: The size of the matrix. + * + * Output parameters: + * indx: The row permutation performed. 1-indexed; it should be of size n+1, + * and index 0 is not used. + * d: the determinant of the permutation matrix. + * + * Returns 1 to indicate failure if the matrix is singular or has zeroes on the + * diagonal, 0 on success. + * + * Derived from ludcmp in "Numerical Recipes in C: The Art of Scientific Computing", + * with modified error handling. + */ +static int +lud(double **a, int n, int *perm, int *d) +{ + int i, imax = 0, j, k; + double big, dum, sum, temp; + double min, max; + double vv[n + 1]; + + *d = 1; + for (i = 1; i <= n; i++) { + big = 0.0; + for (j = 1; j <= n; j++) + if ((temp = fabs(a[i][j])) > big) + big = temp; + + if (big == 0.0) + return 1; + + vv[i] = 1.0 / big; + } + + for (j = 1; j <= n; j++) { + for (i = 1; i < j; i++) { + sum = a[i][j]; + for (k = 1; k < i; k++) + sum -= a[i][k] * a[k][j]; + + a[i][j] = sum; + } + + big = 0.0; + + for (i = j; i <= n; i++) { + sum = a[i][j]; + for (k = 1; k < j; k++) + sum -= a[i][k] * a[k][j]; + + a[i][j] = sum; + + if ((dum = vv[i] * fabs(sum)) >= big) { + big = dum; + imax = i; + } + } + + if (j != imax) { + for (k = 1; k <= n; k++) { + dum = a[imax][k]; + a[imax][k] = a[j][k]; + a[j][k] = dum; + } + + *d = -(*d); + vv[imax] = vv[j]; + } + + perm[j] = imax; + + if (a[j][j] == 0.0) + return 1; + + if (j != n) { + dum = 1.0 / (a[j][j]); + for (i = j + 1; i <= n; i++) + a[i][j] *= dum; + } + } + + min = 1e10; + max = 0.0; + for (i = 1; i <= n; i++) { + temp = fabs(a[i][i]); + if (temp < min) + min = temp; + if (temp > max) + max = temp; + } + return (min / max < 1e-10) ? 1 : 0; +} + +/** + * Solves the set of n linear equations Ax = b, using LU decomposition back-substitution. + * + * Input parameters: + * a: The LU decomposition of a matrix, created by "lud". + * n: The size of the matrix. + * indx: Row permutation vector, created by "lud". + * b: The vector b in the equation. 1-indexed; is should be of size n+1, and index 0 is not used. + * + * Output parameters: + * b: The output vector x. 1-indexed. + * + * From "Numerical Recipes in C: The Art of Scientific Computing". + */ +static void +lubksb(double **a, int n, int *perm, double *b) +{ + int i, ii = 0, ip, j; + double sum; + + for (i = 1; i <= n; i++) { + ip = perm[i]; + sum = b[ip]; + b[ip] = b[i]; + + if (ii) { + for (j = ii; j <= i - 1; j++) + sum -= a[i][j] * b[j]; + } else if (sum) { + ii = i; + } + + b[i] = sum; + } + + for (i = n; i >= 1; i--) { + sum = b[i]; + for (j = i + 1; j <= n; j++) + sum -= a[i][j] * b[j]; + b[i] = sum / a[i][i]; + } +} + +static void +split(double **predictors, double *delta, int order, int npredictors, double scale) +{ + int i, j; + + for (i = 0; i < npredictors; i++) { + for (j = 0; j <= order; j++) { + predictors[i + npredictors][j] = predictors[i][j] + delta[j] * scale; + } + } +} + +static void +refine(double **predictors, int order, int npredictors, double *data, int data_size, int refine_iters) +{ + int iter; + double dist; + double dummy; + double best_value; + int best_index; + int i, j; + + double rsums[npredictors][order + 1]; + int counts[npredictors]; + double vec[order + 1]; + + for (iter = 0; iter < refine_iters; iter++) { + // For some number of refinement iterations + + // Initialize averages + memset(counts, 0, npredictors * sizeof(int)); + memset(rsums, 0, npredictors * (order + 1) * sizeof(double)); + + // Sum autocorrelations + for (i = 0; i < data_size; i++) { + best_value = 1e30; + best_index = 0; + + // Find index that minimizes the "model distance" + for (j = 0; j < npredictors; j++) { + dist = model_dist(predictors[j], &data[(order + 1) * i], order); + + if (dist < best_value) { + best_value = dist; + best_index = j; + } + } + + counts[best_index]++; + rfroma(&data[(order + 1) * i], order, vec); // compute autocorrelation from predictors + + for (j = 0; j <= order; j++) + rsums[best_index][j] += vec[j]; // add to average autocorrelation + } + + // finalize average autocorrelations + for (i = 0; i < npredictors; i++) { + if (counts[i] > 0) { + for (j = 0; j <= order; j++) { + rsums[i][j] /= counts[i]; + } + } + } + + for (i = 0; i < npredictors; i++) { + // compute predictors from average autocorrelation + durbin(rsums[i], order, vec, predictors[i], &dummy); + // vec is reflection coeffs + + // clamp reflection coeffs + for (j = 1; j <= order; j++) { + if (vec[j] >= 1.0) + vec[j] = 0.9999999999; + if (vec[j] <= -1.0) + vec[j] = -0.9999999999; + } + + // clamped reflection coeffs -> predictors + afromk(vec, predictors[i], order); + } + } +} + +static int +read_row(int16_t *p, double *row, int order) +{ + double fval; + int ival; + int i, j, k; + int overflows; + double table[8][order]; + + for (i = 0; i < order; i++) { + for (j = 0; j < i; j++) + table[i][j] = 0.0; + + for (j = i; j < order; j++) + table[i][j] = -row[order - j + i]; + } + + for (i = order; i < 8; i++) + for (j = 0; j < order; j++) + table[i][j] = 0.0; + + for (i = 1; i < 8; i++) + for (j = 1; j <= order; j++) + if (i - j >= 0) + for (k = 0; k < order; k++) + table[i][k] -= row[j] * table[i - j][k]; + + overflows = 0; + + for (i = 0; i < order; i++) { + for (j = 0; j < 8; j++) { + fval = table[j][i] * (double)(1 << 11); + if (fval < 0.0) { + ival = (int)(fval - 0.5); + if (ival < -0x8000) + overflows++; + } else { + ival = (int)(fval + 0.5); + if (ival >= 0x8000) + overflows++; + } + + *(p++) = ival; + } + } + + return overflows; +} + +int +tabledesign_run(int16_t *order_out, int16_t *npredictors_out, int16_t **book_data_out, void *sample_data, + size_t num_samples, const table_design_spec *design) +{ + static const table_design_spec default_design = { + .order = 2, + .bits = 2, + .refine_iters = 2, + .thresh = 10.0, + .frame_size = 16, + }; + + if (design == NULL) + design = &default_design; + + int16_t order = design->order; + int16_t npredictors = 1 << design->bits; + unsigned int frame_size = design->frame_size; + + int num_order = order + 1; + + double vec[num_order]; + int perm[num_order]; + double reflection_coeffs[num_order]; + + int16_t *buffer = MALLOC_CHECKED_INFO(2 * frame_size * sizeof(int16_t), "frame_size=%u", frame_size); + + double **predictors = MALLOC_CHECKED_INFO(npredictors * sizeof(double *), "npredictors=%d", npredictors); + for (int i = 0; i < npredictors; i++) + predictors[i] = MALLOC_CHECKED_INFO(num_order * sizeof(double), "npredictors=%d", npredictors); + + double **autocorrelation_matrix = MALLOC_CHECKED_INFO(num_order * sizeof(double *), "num_order=%d", num_order); + for (int i = 0; i < num_order; i++) + autocorrelation_matrix[i] = MALLOC_CHECKED_INFO(num_order * sizeof(double), "num_order=%d", num_order); + + size_t nframes = num_samples - (num_samples % frame_size); + + double *data = + MALLOC_CHECKED_INFO(nframes * num_order * sizeof(double), "nframes=%lu, num_order=%d", nframes, num_order); + uint32_t data_size = 0; + + int16_t *sample = sample_data; + // (back-)align to a multiple of the frame size + int16_t *sample_end = sample + nframes; + + memset(buffer, 0, frame_size * sizeof(int16_t)); + + for (; sample < sample_end; sample += frame_size) { + // Copy sample data into second half of buffer, during the first iteration the first half is 0 while in + // later iterations the second half of the previous iteration is shifted into the first half. + memcpy(&buffer[frame_size], sample, frame_size * sizeof(int16_t)); + + // Compute autocorrelation vector of the two vectors in the buffer + acvect(&buffer[frame_size], order, frame_size, vec); + + // First element is the largest(?) + if (fabs(vec[0]) > design->thresh) { + // Over threshold + + // Computes the autocorrelation matrix of the two vectors in the buffer + acmat(&buffer[frame_size], order, frame_size, autocorrelation_matrix); + + // Compute the LUP decomposition of the autocorrelation matrix + int perm_det; + if (lud(autocorrelation_matrix, order, perm, &perm_det) == 0) { // Continue only if numerically stable + // Back-substitution to solve the linear equation Ra = r + // where + // R = autocorrelation matrix + // r = autocorrelation vector + // a = linear prediction coefficients + // After this vec contains the prediction coefficients + lubksb(autocorrelation_matrix, order, perm, vec); + vec[0] = 1.0; + + // Compute reflection coefficients from prediction coefficients + if (kfroma(vec, reflection_coeffs, order) == 0) { // Continue only if numerically stable + data[data_size * num_order + 0] = 1.0; + + // clamp the reflection coefficients + for (int i = 1; i < num_order; i++) { + if (reflection_coeffs[i] >= 1.0) + reflection_coeffs[i] = 0.9999999999; + if (reflection_coeffs[i] <= -1.0) + reflection_coeffs[i] = -0.9999999999; + } + + // Compute prediction coefficients from reflection coefficients + afromk(reflection_coeffs, &data[data_size * num_order], order); + data_size++; + } + } + } + + // Move second vector to first vector + memcpy(&buffer[0], &buffer[frame_size], frame_size * sizeof(int16_t)); + } + + // Create a vector [1.0, 0.0, ..., 0.0] + vec[0] = 1.0; + for (int i = 1; i < num_order; i++) + vec[i] = 0.0; + + for (uint32_t i = 0; i < data_size; i++) { + // Compute autocorrelation from predictors + rfroma(&data[i * num_order], order, predictors[0]); + + for (int k = 1; k < num_order; k++) + vec[k] += predictors[0][k]; + } + + for (int i = 1; i < num_order; i++) + vec[i] /= data_size; + + // vec is the average autocorrelation + + // Compute predictors for average autocorrelation using Levinson-Durbin algorithm + double dummy; + durbin(vec, order, reflection_coeffs, predictors[0], &dummy); + + // clamp results + for (int i = 1; i < num_order; i++) { + if (reflection_coeffs[i] >= 1.0) + reflection_coeffs[i] = 0.9999999999; + if (reflection_coeffs[i] <= -1.0) + reflection_coeffs[i] = -0.9999999999; + } + + // Convert clamped reflection coefficients to predictors + afromk(reflection_coeffs, predictors[0], order); + + // Split and refine predictors + for (unsigned cur_bits = 0; cur_bits < design->bits; cur_bits++) { + double split_delta[num_order]; + + for (int i = 0; i < num_order; i++) + split_delta[i] = 0.0; + split_delta[order - 1] = -1.0; + + split(predictors, split_delta, order, 1 << cur_bits, 0.01); + refine(predictors, order, 1 << (1 + cur_bits), data, data_size, design->refine_iters); + } + + int16_t *book_data = MALLOC_CHECKED_INFO((8 * order * npredictors + 2) * sizeof(int16_t), + "order=%d, npredictors=%d", order, npredictors); + + *order_out = order; + *npredictors_out = npredictors; + + int num_oflow = 0; + for (int i = 0; i < npredictors; i++) + num_oflow += read_row(&book_data[8 * order * i], predictors[i], order); + + if (num_oflow) { + error("overflow detected in tabledesign"); + } + + *book_data_out = book_data; + + free(buffer); + free(data); + + for (int i = 0; i < num_order; i++) + free(autocorrelation_matrix[i]); + free(autocorrelation_matrix); + + for (int i = 0; i < npredictors; i++) + free(predictors[i]); + free(predictors); + + return 0; +} diff --git a/tools/audio/sampleconv/src/container/aiff.c b/tools/audio/sampleconv/src/container/aiff.c new file mode 100644 index 0000000000..d550e80055 --- /dev/null +++ b/tools/audio/sampleconv/src/container/aiff.c @@ -0,0 +1,837 @@ +/** + * 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 "../util.h" +#include "../codec/vadpcm.h" +#include "aiff.h" + +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 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_CHECKED_INFO(len + 1, "len=%u", 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); + + return out; +} + +static void +write_pstring(FILE *f, const char *in) +{ + unsigned char zr = 0; + size_t inlen = strlen(in); + + if (inlen > 255) + error("Invalid pstring length."); + + unsigned char len = inlen; + + CHUNK_WRITE(f, &len); + CHUNK_WRITE_RAW(f, in, len); + if (!(len & 1)) + CHUNK_WRITE(f, &zr); +} + +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 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; +} + +int +aiff_aifc_common_read(container_data *out, FILE *in, UNUSED bool matching, uint32_t size) +{ + bool has_comm = false; + bool has_inst = false; + bool has_ssnd = false; + + size_t num_markers = 0; + container_marker *markers = NULL; + + memset(out, 0, sizeof(*out)); + + while (true) { + long start = ftell(in); + if (start > 8 + size) { + error("Overran file"); + } + if (start == 8 + size) { + break; + } + + char cc4[4]; + uint32_t chunk_size; + + FREAD(in, cc4, 4); + FREAD(in, &chunk_size, 4); + chunk_size = be32toh(chunk_size); + + chunk_size++; + chunk_size &= ~1; + + 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); + + uint32_t num_samples = (comm.numFramesH << 16) | comm.numFramesL; + double sample_rate; + f80_to_f64(&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); + } + + out->num_channels = comm.numChannels; + out->sample_rate = sample_rate; + out->bit_depth = comm.sampleSize; + out->num_samples = num_samples; + + switch (comp_type) { + case CC4('N', 'O', 'N', 'E'): + out->data_type = SAMPLE_TYPE_PCM16; + break; + + case CC4('H', 'P', 'C', 'M'): + out->data_type = SAMPLE_TYPE_PCM8; + break; + + case CC4('A', 'D', 'P', '9'): + out->data_type = SAMPLE_TYPE_VADPCM; + break; + + case CC4('A', 'D', 'P', '5'): + out->data_type = SAMPLE_TYPE_VADPCM_HALF; + break; + + default: + error("Unrecognized aiff/aifc compression type"); + break; + } + + if (chunk_size > sizeof(aiff_COMM) + 4) { + char compression_name[257]; + read_pstring(in, 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); + + out->base_note = inst.baseNote; + out->fine_tune = inst.detune; + out->key_low = inst.lowNote; + out->key_hi = inst.highNote; + out->vel_low = inst.lowVelocity; + out->vel_hi = inst.highVelocity; + out->gain = inst.gain; + + size_t nloops = 0; + nloops += inst.sustainLoop.playMode != LOOP_PLAYMODE_NONE; + nloops += inst.releaseLoop.playMode != LOOP_PLAYMODE_NONE; + + if (nloops != 0) { + out->loops = MALLOC_CHECKED_INFO(nloops * sizeof(container_loop), "nloops=%lu", nloops); + + size_t i = 0; + + if (inst.sustainLoop.playMode != LOOP_PLAYMODE_NONE) { + out->loops[i].id = 0; + + switch (inst.sustainLoop.playMode) { + case LOOP_PLAYMODE_FWD: + out->loops[i].type = LOOP_FORWARD; + break; + + case LOOP_PLAYMODE_FWD_BWD: + out->loops[i].type = LOOP_FORWARD_BACKWARD; + break; + + default: + error("Unrecognized PlayMode in sustainLoop"); + break; + } + out->loops[i].start = inst.sustainLoop.beginLoop; + out->loops[i].end = inst.sustainLoop.endLoop; + out->loops[i].num = 0xFFFFFFFF; + out->loops[i].fraction = 0; + + i++; + } + + if (inst.releaseLoop.playMode != LOOP_PLAYMODE_NONE) { + out->loops[i].id = 1; + + switch (inst.sustainLoop.playMode) { + case LOOP_PLAYMODE_FWD: + out->loops[i].type = LOOP_FORWARD; + break; + + case LOOP_PLAYMODE_FWD_BWD: + out->loops[i].type = LOOP_FORWARD_BACKWARD; + break; + + default: + error("Unrecognized PlayMode in releaseLoop"); + break; + } + out->loops[i].start = inst.releaseLoop.beginLoop; + out->loops[i].end = inst.releaseLoop.endLoop; + out->loops[i].num = 0xFFFFFFFF; + out->loops[i].fraction = 0; + + i++; + } + } + + has_inst = true; + } break; + + case CC4('M', 'A', 'R', 'K'): { + aiff_MARK mark; + FREAD(in, &mark, sizeof(mark)); + mark.nMarkers = be16toh(mark.nMarkers); + + num_markers = mark.nMarkers; + markers = MALLOC_CHECKED_INFO(num_markers * sizeof(container_marker), "num_markers=%lu", num_markers); + + for (size_t i = 0; i < num_markers; i++) { + Marker marker; + char *name; + FREAD(in, &marker, sizeof(marker)); + name = read_pstring_alloc(in); + + markers[i].id = be16toh(marker.MarkerID); + markers[i].frame_number = (be16toh(marker.positionH) << 16) | be16toh(marker.positionL); + markers[i].name = name; + } + } break; + + case CC4('A', 'P', 'P', 'L'): { + char subcc4[4]; + + FREAD(in, subcc4, 4); + + 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); + + 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); + + size_t book_size_bytes = VADPCM_BOOK_SIZE_BYTES(order, npredictors); + int16_t *book_state = + MALLOC_CHECKED_INFO(book_size_bytes, "order=%u, npredictors=%u", order, npredictors); + FREAD(in, book_state, book_size_bytes); + + out->vadpcm.book_version = version; + out->vadpcm.book_header.order = order; + out->vadpcm.book_header.npredictors = npredictors; + out->vadpcm.book_data = book_state; + + for (size_t i = 0; i < VADPCM_BOOK_SIZE(order, npredictors); i++) + out->vadpcm.book_data[i] = be16toh(out->vadpcm.book_data[i]); + + out->vadpcm.has_book = true; + } 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); + + out->vadpcm.loop_version = version; + out->vadpcm.num_loops = nloops; + + if (out->vadpcm.num_loops) + out->vadpcm.loops = MALLOC_CHECKED_INFO(out->vadpcm.num_loops * sizeof(ALADPCMloop), + "num_loops=%u", out->vadpcm.num_loops); + + for (size_t i = 0; i < out->vadpcm.num_loops; i++) { + FREAD(in, &out->vadpcm.loops[i], sizeof(ALADPCMloop)); + out->vadpcm.loops[i].start = be32toh(out->vadpcm.loops[i].start); + out->vadpcm.loops[i].end = be32toh(out->vadpcm.loops[i].end); + out->vadpcm.loops[i].count = be32toh(out->vadpcm.loops[i].count); + for (size_t j = 0; j < ARRAY_COUNT(out->vadpcm.loops[i].state); j++) + out->vadpcm.loops[i].state[j] = be16toh(out->vadpcm.loops[i].state[j]); + } + } 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); + + size_t data_size = chunk_size - sizeof(ssnd); + + void *data = MALLOC_CHECKED_INFO(data_size, "SSND chunk size = %lu", data_size); + FREAD(in, data, data_size); + + if (ssnd.offset != 0 || ssnd.blockSize != 0) + error("Bad SSND chunk in aiff/aifc, offset/blockSize != 0"); + + out->data = data; + out->data_size = data_size; + + has_ssnd = true; + } break; + + default: + warning("Skipping unknown aiff chunk: \"%c%c%c%c\"", cc4[0], cc4[1], cc4[2], cc4[3]); + 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_inst) + error("aiff/aifc has no INST chunk"); + if (!has_ssnd) + error("aiff/aifc has no SSND chunk"); + + if (out->data_type == SAMPLE_TYPE_PCM16) { + assert(out->data_size % 2 == 0); + assert(out->bit_depth == 16); + + for (size_t i = 0; i < out->data_size / 2; i++) + ((uint16_t *)(out->data))[i] = be16toh(((uint16_t *)(out->data))[i]); + } + + for (size_t i = 0; i < out->num_loops; i++) { + container_marker *marker; + + marker = NULL; + for (size_t j = 0; j < num_markers; j++) { + if (markers[j].id == out->loops[i].start) { + marker = &markers[j]; + break; + } + } + if (marker == NULL) + error("AIFF/C loop references non-existent marker"); + + out->loops[i].start = marker->frame_number; + + marker = NULL; + for (size_t j = 0; j < num_markers; j++) { + if (markers[j].id == out->loops[i].end) { + marker = &markers[j]; + break; + } + } + if (marker == NULL) + error("AIFF/C loop references non-existent marker"); + + out->loops[i].end = marker->frame_number; + } + + fclose(in); + return 0; +} + +int +aifc_read(container_data *out, const char *path, bool matching) +{ + FILE *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); + + if (!CC4_CHECK(form, "FORM") || !CC4_CHECK(aifc, "AIFC")) + error("Not an aifc file?"); + + return aiff_aifc_common_read(out, in, matching, size); +} + +int +aiff_read(container_data *out, const char *path, bool matching) +{ + FILE *in = fopen(path, "rb"); + if (in == NULL) + error("Failed to open \"%s\" for reading", path); + + char form[4]; + uint32_t size; + char aiff[4]; + + FREAD(in, form, 4); + FREAD(in, &size, 4); + size = be32toh(size); + FREAD(in, aiff, 4); + + if (!CC4_CHECK(form, "FORM") || !CC4_CHECK(aiff, "AIFF")) + error("Not an aiff file?"); + + return aiff_aifc_common_read(out, in, matching, size); +} + +static int +aiff_aifc_common_write(container_data *in, const char *path, bool aifc, bool matching) +{ + FILE *out = fopen(path, "wb"); + if (out == NULL) + error("Failed to open %s for writing\n", path); + + const char *aifc_head = "FORM\0\0\0\0AIFC"; + const char *aiff_head = "FORM\0\0\0\0AIFF"; + FWRITE(out, (aifc) ? aifc_head : aiff_head, 12); + + long chunk_start; + + uint32_t compression_type; + const char *compression_name; + + switch (in->data_type) { + case SAMPLE_TYPE_PCM16: + compression_type = CC4('N', 'O', 'N', 'E'); + compression_name = NULL; + break; + + case SAMPLE_TYPE_PCM8: + compression_type = CC4('H', 'P', 'C', 'M'); + compression_name = "Half-frame PCM"; + break; + + case SAMPLE_TYPE_VADPCM: + compression_type = CC4('A', 'D', 'P', '9'); + compression_name = "Nintendo/SGI VADPCM 9-bytes/frame"; + break; + + case SAMPLE_TYPE_VADPCM_HALF: + compression_type = CC4('A', 'D', 'P', '5'); + compression_name = "Nintendo/SGI VADPCM 5-bytes/frame"; + break; + + default: + error("Unhandled data type for aiff/aifc container"); + break; + } + + if (compression_type != CC4('N', 'O', 'N', 'E') && !aifc) { + error("Compressed data types must use aifc output"); + } + + aiff_COMM comm = { + .numChannels = htobe16(in->num_channels), + .numFramesH = htobe16(in->num_samples >> 16), + .numFramesL = htobe16(in->num_samples), + .sampleSize = htobe16(in->bit_depth), + .sampleRate = { 0 }, + }; + f64_to_f80(in->sample_rate, comm.sampleRate); + CHUNK_BEGIN(out, "COMM", &chunk_start); + CHUNK_WRITE(out, &comm); + if (compression_name != NULL) { + uint32_t compressionType = htobe32(compression_type); + CHUNK_WRITE(out, &compressionType); + write_pstring(out, compression_name); + } + CHUNK_END(out, chunk_start, htobe32); + + if (in->num_loops > 2) + error("Only up to two loops are supported for AIFF/C"); + + size_t num_markers = 0; + container_marker markers[4]; + static char *marker_names[4] = { + "start", + "end", + "start", + "end", + }; + Loop sustainLoop = { 0 }; + Loop releaseLoop = { 0 }; + + for (size_t i = 0; i < in->num_loops; i++) { + Loop *target; + + switch (in->loops[i].id) { + case 0: + target = &sustainLoop; + break; + + case 1: + target = &releaseLoop; + break; + + default: + continue; + } + + unsigned type; + switch (in->loops[i].type) { + case LOOP_FORWARD: + type = LOOP_PLAYMODE_FWD; + break; + + case LOOP_FORWARD_BACKWARD: + type = LOOP_PLAYMODE_FWD_BWD; + break; + + default: + error("Unsupported loop type in aiff/aifc"); + break; + } + + target->playMode = htobe16(type); + + markers[num_markers].id = num_markers + 1; + markers[num_markers].name = marker_names[num_markers]; + markers[num_markers].frame_number = in->loops[i].start; + target->beginLoop = htobe16(1 + num_markers++); + + markers[num_markers].id = num_markers + 1; + markers[num_markers].name = marker_names[num_markers]; + markers[num_markers].frame_number = in->loops[i].end; + target->endLoop = htobe16(1 + num_markers++); + } + + if (num_markers != 0) { + CHUNK_BEGIN(out, "MARK", &chunk_start); + + aiff_MARK mark = { + .nMarkers = htobe16(num_markers), + }; + CHUNK_WRITE(out, &mark); + + for (size_t i = 0; i < num_markers; i++) { + Marker marker = { + .MarkerID = htobe16(markers[i].id), + .positionH = htobe16(markers[i].frame_number >> 16), + .positionL = htobe16(markers[i].frame_number), + }; + CHUNK_WRITE(out, &marker); + write_pstring(out, markers[i].name); + } + + CHUNK_END(out, chunk_start, htobe32); + } + + aiff_INST inst = { + .baseNote = in->base_note, + .detune = in->fine_tune, + .lowNote = in->key_low, + .highNote = in->key_hi, + .lowVelocity = in->vel_low, + .highVelocity = in->vel_hi, + .gain = htobe16(in->gain), + .sustainLoop = sustainLoop, + .releaseLoop = releaseLoop, + }; + CHUNK_BEGIN(out, "INST", &chunk_start); + CHUNK_WRITE(out, &inst); + CHUNK_END(out, chunk_start, htobe32); + + if (aifc || matching) { + // If we're writing an aifc, or we want to match on round-trip, emit an application-specific chunk for the + // vadpcm codebook. + if (in->vadpcm.has_book) { + // APPL::stoc::VADPCMCODES + CHUNK_BEGIN(out, "APPL", &chunk_start); + CHUNK_WRITE_RAW(out, "stoc", 4); + write_pstring(out, "VADPCMCODES"); + + int16_t version = htobe16(in->vadpcm.book_version); + int16_t order = htobe16(in->vadpcm.book_header.order); + int16_t npredictors = htobe16(in->vadpcm.book_header.npredictors); + + CHUNK_WRITE(out, &version); + CHUNK_WRITE(out, &order); + CHUNK_WRITE(out, &npredictors); + + size_t book_size = VADPCM_BOOK_SIZE(in->vadpcm.book_header.order, in->vadpcm.book_header.npredictors); + + int16_t book_data_out[book_size]; + for (size_t i = 0; i < book_size; i++) + book_data_out[i] = htobe16(in->vadpcm.book_data[i]); + + CHUNK_WRITE_RAW(out, book_data_out, sizeof(int16_t) * book_size); + + CHUNK_END(out, chunk_start, htobe32); + } + + // Only write a vadpcm loop structure for compressed output. Loop states match on round-trip so we need not + // save them in uncompressed output. + if (aifc && in->vadpcm.num_loops != 0) { + // APPL::stoc::VADPCMLOOPS + CHUNK_BEGIN(out, "APPL", &chunk_start); + CHUNK_WRITE_RAW(out, "stoc", 4); + write_pstring(out, "VADPCMLOOPS"); + + int16_t version = htobe16(in->vadpcm.loop_version); + int16_t nloops = htobe16(in->vadpcm.num_loops); + + CHUNK_WRITE(out, &version); + CHUNK_WRITE(out, &nloops); + + for (size_t i = 0; i < in->vadpcm.num_loops; i++) { + ALADPCMloop outloop = in->vadpcm.loops[i]; + outloop.start = htobe32(outloop.start); + outloop.end = htobe32(outloop.end); + outloop.count = htobe32(outloop.count); + for (size_t i = 0; i < ARRAY_COUNT(outloop.state); i++) + outloop.state[i] = htobe16(outloop.state[i]); + + CHUNK_WRITE(out, &outloop); + } + + CHUNK_END(out, chunk_start, htobe32); + } + } + + if (in->data_type == SAMPLE_TYPE_PCM16) { + assert(in->data_size % 2 == 0); + assert(in->bit_depth == 16); + + for (size_t i = 0; i < in->data_size / 2; i++) { + ((uint16_t *)in->data)[i] = htobe16(((uint16_t *)in->data)[i]); + } + } + + aiff_SSND ssnd = { + .offset = 0, + .blockSize = 0, + }; + CHUNK_BEGIN(out, "SSND", &chunk_start); + CHUNK_WRITE(out, &ssnd); + CHUNK_WRITE_RAW(out, in->data, in->data_size); + CHUNK_END(out, chunk_start, htobe32); + + uint32_t size = htobe32(ftell(out) - 8); + fseek(out, 4, SEEK_SET); + fwrite(&size, 4, 1, out); + fclose(out); + + return 0; +} + +int +aifc_write(container_data *data, const char *path, bool matching) +{ + return aiff_aifc_common_write(data, path, true, matching); +} + +int +aiff_write(container_data *data, const char *path, bool matching) +{ + return aiff_aifc_common_write(data, path, false, matching); +} diff --git a/tools/audio/sampleconv/src/container/aiff.h b/tools/audio/sampleconv/src/container/aiff.h new file mode 100644 index 0000000000..757a57e749 --- /dev/null +++ b/tools/audio/sampleconv/src/container/aiff.h @@ -0,0 +1,28 @@ +/** + * 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 AIFF_H +#define AIFF_H + +#include +#include + +#include "../codec/vadpcm.h" +#include "container.h" + +int +aifc_read(container_data *out, const char *path, bool matching); +int +aiff_read(container_data *out, const char *path, bool matching); + +int +aifc_write(container_data *in, const char *path, bool matching); +int +aiff_write(container_data *in, const char *path, bool matching); + +#endif diff --git a/tools/audio/sampleconv/src/container/container.c b/tools/audio/sampleconv/src/container/container.c new file mode 100644 index 0000000000..f2df3448e6 --- /dev/null +++ b/tools/audio/sampleconv/src/container/container.c @@ -0,0 +1,25 @@ +/** + * 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 "container.h" + +int +container_destroy(container_data *data) +{ + if (data->data != NULL) + free(data->data); + if (data->loops != NULL) + free(data->loops); + if (data->vadpcm.book_data != NULL) + free(data->vadpcm.book_data); + if (data->vadpcm.loops != NULL) + free(data->vadpcm.loops); + return 0; +} diff --git a/tools/audio/sampleconv/src/container/container.h b/tools/audio/sampleconv/src/container/container.h new file mode 100644 index 0000000000..680b64012d --- /dev/null +++ b/tools/audio/sampleconv/src/container/container.h @@ -0,0 +1,99 @@ +/** + * 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 CONTAINER_H +#define CONTAINER_H + +#include +#include + +#include "../codec/vadpcm.h" + +typedef enum { + // Uncompressed + SAMPLE_TYPE_PCM16, + SAMPLE_TYPE_PCM8, + + // Compressed + SAMPLE_TYPE_VADPCM, + SAMPLE_TYPE_VADPCM_HALF +} sample_data_type; + +typedef struct { + const char *fext; + int (*read)(struct container_data *out, const char *path, bool matching); + int (*write)(struct container_data *out, const char *path, bool matching); +} container_spec; + +typedef enum { + LOOP_NONE = 0, + LOOP_FORWARD = 1, + LOOP_FORWARD_BACKWARD = 2, + LOOP_BACKWARD = 3 +} loop_type; + +typedef struct { + uint16_t id; + loop_type type; + uint32_t start; + uint32_t end; + uint32_t fraction; + uint32_t num; +} container_loop; + +typedef struct { + uint16_t id; + uint32_t frame_number; + char *name; +} container_marker; + +typedef struct container_data { + // Sample details + double sample_rate; + unsigned int num_channels; + uint32_t byte_rate; + uint16_t block_align; + uint16_t bit_depth; // also called sample_size + uint32_t num_samples; // also apparently called num_frames? but that's wrong + + // Instrument details + int8_t base_note; + int8_t fine_tune; + int8_t gain; + int8_t key_low; + int8_t key_hi; + int8_t vel_low; + int8_t vel_hi; + + // Sample data, possibly compressed + sample_data_type data_type; + void *data; + size_t data_size; + + unsigned num_loops; + container_loop *loops; + + // VADPCM-specific data + struct { + // VADPCM codebook + int16_t book_version; + ALADPCMbookhead book_header; + int16_t *book_data; + bool has_book; + + // VADPCM loop data + int16_t loop_version; + unsigned num_loops; + ALADPCMloop *loops; + } vadpcm; +} container_data; + +int +container_destroy(container_data *data); + +#endif diff --git a/tools/audio/sampleconv/src/container/wav.c b/tools/audio/sampleconv/src/container/wav.c new file mode 100644 index 0000000000..885acb35eb --- /dev/null +++ b/tools/audio/sampleconv/src/container/wav.c @@ -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 +#include +#include +#include +#include +#include + +#include "../util.h" +#include "../codec/vadpcm.h" +#include "wav.h" + +typedef enum { + WAVE_TYPE_PCM = 1, + WAVE_TYPE_FLOAT = 3, + WAVE_TYPE_ALAW = 6, + WAVE_TYPE_MULAW = 7, + WAVE_TYPE_EXTENSIBLE = 0xFFFE, +} wav_type; + +typedef struct { + uint16_t type; + uint16_t num_channels; + uint32_t sample_rate; + uint32_t byte_rate; + uint16_t block_align; + uint16_t bit_depth; +} wav_fmt; + +typedef struct { + uint32_t num_samples; +} wav_fact; + +typedef struct { + int8_t base_note; + int8_t fine_tune; + int8_t gain; + int8_t key_low; + int8_t key_hi; + int8_t vel_low; + int8_t vel_hi; + char _pad[1]; +} wav_inst; + +typedef struct { + uint32_t manufacturer; + uint32_t product; + uint32_t sample_period; + uint32_t unity_note; + char _pad[3]; + uint8_t fine_tune; + uint32_t format; + uint32_t offset; + uint32_t num_sample_loops; + uint32_t sampler_data; +} wav_smpl; + +typedef struct { + uint32_t cue_point_index; + uint32_t type; + uint32_t start; + uint32_t end; + uint32_t fraction; + uint32_t num; +} wav_loop; + +static const char * +wav_type_name(int type) +{ + switch (type) { + case WAVE_TYPE_PCM: + return "PCM"; + case WAVE_TYPE_FLOAT: + return "Float"; + case WAVE_TYPE_ALAW: + return "ALAW"; + case WAVE_TYPE_MULAW: + return "MULAW"; + case WAVE_TYPE_EXTENSIBLE: + return "Extensible"; + default: + return "Unknown (should never be here)"; + } +} + +int +wav_read(container_data *out, const char *path, UNUSED bool matching) +{ + bool has_fmt = false; + bool has_fact = false; + bool has_data = false; + bool has_inst = false; + bool has_smpl = false; + memset(out, 0, sizeof(*out)); + + FILE *in = fopen(path, "rb"); + if (in == NULL) + error("Failed to open \"%s\" for reading", path); + + char riff[4]; + uint32_t size; + char wave[4]; + + FREAD(in, riff, 4); + FREAD(in, &size, 4); + size = le32toh(size); + FREAD(in, wave, 4); + + if (!CC4_CHECK(riff, "RIFF") || !CC4_CHECK(wave, "WAVE")) + error("Not a wav file?"); + + while (true) { + bool skipped = false; + + long start = ftell(in); + if (start > 8 + size) { + error("Overran file"); + } + if (start == 8 + size) { + break; + } + + char cc4[4]; + uint32_t chunk_size; + + FREAD(in, cc4, 4); + FREAD(in, &chunk_size, 4); + chunk_size = le32toh(chunk_size); + + switch (CC4(cc4[0], cc4[1], cc4[2], cc4[3])) { + case CC4('f', 'm', 't', ' '): { + wav_fmt fmt; + FREAD(in, &fmt, sizeof(fmt)); + fmt.type = le16toh(fmt.type); + fmt.num_channels = le16toh(fmt.num_channels); + fmt.sample_rate = le32toh(fmt.sample_rate); + fmt.byte_rate = le32toh(fmt.byte_rate); + fmt.block_align = le16toh(fmt.block_align); + fmt.bit_depth = le16toh(fmt.bit_depth); + + if (fmt.bit_depth != 16) + error("Wav input format should be 16-bit PCM, was %u-bit", fmt.bit_depth); + + switch (fmt.type) { + case WAVE_TYPE_PCM: + out->data_type = SAMPLE_TYPE_PCM16; + break; + + case WAVE_TYPE_FLOAT: + case WAVE_TYPE_MULAW: + case WAVE_TYPE_ALAW: + case WAVE_TYPE_EXTENSIBLE: + error("Unhandled sample type: %s, should be PCM", wav_type_name(fmt.type)); + break; + + default: + error("Unrecognized sample type: %d, should be PCM", fmt.type); + break; + } + + out->num_channels = fmt.num_channels; + out->sample_rate = fmt.sample_rate; + out->byte_rate = fmt.byte_rate; + out->block_align = fmt.block_align; + out->bit_depth = fmt.bit_depth; + + has_fmt = true; + } break; + + case CC4('f', 'a', 'c', 't'): { + wav_fact fact; + FREAD(in, &fact, sizeof(fact)); + fact.num_samples = le32toh(fact.num_samples); + + out->num_samples = fact.num_samples; + + has_fact = true; + } break; + + case CC4('d', 'a', 't', 'a'): { + void *data = MALLOC_CHECKED_INFO(chunk_size, "data size = %u", chunk_size); + FREAD(in, data, chunk_size); + + out->data = data; + out->data_size = chunk_size; + + has_data = true; + } break; + + case CC4('i', 'n', 's', 't'): { + wav_inst inst; + FREAD(in, &inst, sizeof(inst)); + + out->base_note = inst.base_note; + out->fine_tune = inst.fine_tune; + out->gain = inst.gain; + out->key_low = inst.key_low; + out->key_hi = inst.key_hi; + out->vel_low = inst.vel_low; + out->vel_hi = inst.vel_hi; + + has_inst = true; + } break; + + case CC4('s', 'm', 'p', 'l'): { + wav_smpl smpl; + FREAD(in, &smpl, sizeof(smpl)); + smpl.manufacturer = le32toh(smpl.manufacturer); + smpl.product = le32toh(smpl.product); + smpl.sample_period = le32toh(smpl.sample_period); + smpl.unity_note = le32toh(smpl.unity_note); + smpl.format = le32toh(smpl.format); + smpl.offset = le32toh(smpl.offset); + smpl.num_sample_loops = le32toh(smpl.num_sample_loops); + smpl.sampler_data = le32toh(smpl.sampler_data); + + out->num_loops = smpl.num_sample_loops; + if (out->num_loops != 0) { + out->loops = + MALLOC_CHECKED_INFO(out->num_loops * sizeof(container_loop), "num_loops=%u", out->num_loops); + + for (size_t i = 0; i < out->num_loops; i++) { + wav_loop loop; + FREAD(in, &loop, sizeof(loop)); + loop.cue_point_index = le32toh(loop.cue_point_index); + loop.type = le32toh(loop.type); + loop.start = le32toh(loop.start); + loop.end = le32toh(loop.end); + loop.fraction = le32toh(loop.fraction); + loop.num = le32toh(loop.num); + + loop_type type; + switch (loop.type) { + case 0: + type = LOOP_FORWARD; + break; + + case 1: + type = LOOP_FORWARD_BACKWARD; + break; + + case 2: + type = LOOP_BACKWARD; + break; + + default: + error("Unrecognized loop type in wav"); + } + + out->loops[i].id = i; + out->loops[i].type = type; + out->loops[i].start = loop.start; + out->loops[i].end = loop.end; + out->loops[i].fraction = loop.fraction; + out->loops[i].num = loop.num; + } + } + has_smpl = true; + } break; + + case CC4('z', 'z', 'b', 'k'): { + char vadpcmcodes[12]; + uint16_t version; + uint16_t order; + uint16_t npredictors; + + FREAD(in, vadpcmcodes, sizeof(vadpcmcodes)); + FREAD(in, &version, sizeof(version)); + version = le16toh(version); + FREAD(in, &order, sizeof(order)); + order = le16toh(order); + FREAD(in, &npredictors, sizeof(npredictors)); + npredictors = le16toh(npredictors); + + size_t book_size = VADPCM_BOOK_SIZE(order, npredictors); + size_t book_data_size = sizeof(int16_t) * book_size; + int16_t *book_data = + MALLOC_CHECKED_INFO(book_data_size, "order=%u, npredictors=%u", order, npredictors); + FREAD(in, book_data, book_data_size); + + out->vadpcm.book_header.order = order; + out->vadpcm.book_header.npredictors = npredictors; + out->vadpcm.book_data = book_data; + + for (size_t i = 0; i < book_size; i++) { + out->vadpcm.book_data[i] = le16toh(out->vadpcm.book_data[i]); + } + + out->vadpcm.has_book = true; + } break; + + case CC4('z', 'z', 'l', 'p'): { + uint16_t version; + uint16_t nloops; + + FREAD(in, &version, sizeof(version)); + version = le16toh(version); + FREAD(in, &nloops, sizeof(nloops)); + nloops = le16toh(nloops); + + if (nloops != 0) + out->vadpcm.loops = MALLOC_CHECKED_INFO(nloops * sizeof(ALADPCMloop), "nloops=%u", nloops); + + for (size_t i = 0; i < nloops; i++) { + uint32_t loop_start; + uint32_t loop_end; + uint32_t loop_num; + + FREAD(in, &loop_start, sizeof(loop_start)); + loop_start = le32toh(loop_start); + FREAD(in, &loop_end, sizeof(loop_end)); + loop_end = le32toh(loop_end); + FREAD(in, &loop_num, sizeof(loop_num)); + loop_num = le32toh(loop_num); + + out->vadpcm.loops[i].start = loop_start; + out->vadpcm.loops[i].end = loop_end; + out->vadpcm.loops[i].count = loop_num; + + if (out->vadpcm.loops[i].count != 0) { + FREAD(in, out->vadpcm.loops[i].state, sizeof(out->vadpcm.loops[i].state)); + + for (size_t j = 0; j < ARRAY_COUNT(out->vadpcm.loops[i].state); j++) { + out->vadpcm.loops[i].state[j] = le16toh(out->vadpcm.loops[i].state[j]); + } + } + } + } break; + + default: + warning("Skipping unknown wav chunk: \"%c%c%c%c\"", cc4[0], cc4[1], cc4[2], cc4[3]); + skipped = true; + break; + } + + long read_size = ftell(in) - start - 8; + + if (read_size > chunk_size) + error("overran chunk"); + else if (!skipped && 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_fmt) + error("wav has no fmt chunk"); + if (!has_data) + error("wav has no data chunk"); + + if (!has_fact) { + out->num_samples = out->data_size / (out->bit_depth / 8); + } + + if (!has_inst) { + out->base_note = 60; // C4 + out->fine_tune = 0; + out->gain = 0; + out->key_low = 0; + out->key_hi = 0; + out->vel_low = 0; + out->vel_hi = 0; + } + + if (!has_smpl) { + out->num_loops = 0; + out->loops = NULL; + } + + if (out->data_type == SAMPLE_TYPE_PCM16) { + if (out->data_size % 2 != 0) + error("wav data size is not a multiple of 2 despite being pcm16-formatted?"); + + for (size_t i = 0; i < out->data_size / 2; i++) + ((uint16_t *)(out->data))[i] = le16toh(((uint16_t *)(out->data))[i]); + } + + fclose(in); + return 0; +} + +int +wav_write(container_data *in, const char *path, bool matching) +{ + long chunk_start; + + FILE *out = fopen(path, "wb"); + FWRITE(out, "RIFF\0\0\0\0WAVE", 12); + + uint16_t fmt_type; + switch (in->data_type) { + case SAMPLE_TYPE_PCM16: + fmt_type = WAVE_TYPE_PCM; + break; + + default: + error("Unrecognized sample type for wav output"); + break; + } + + wav_fmt fmt = { + .type = htole16(fmt_type), + .num_channels = htole16(in->num_channels), + .sample_rate = htole32(in->sample_rate), + .byte_rate = htole32(in->sample_rate * (in->bit_depth / 8)), + .block_align = htole16(in->num_channels * (in->bit_depth / 8)), + .bit_depth = htole16(in->bit_depth), + }; + CHUNK_BEGIN(out, "fmt ", &chunk_start); + CHUNK_WRITE(out, &fmt); + CHUNK_END(out, chunk_start, htole32); + + wav_fact fact = { + .num_samples = htole32(in->num_samples), + }; + CHUNK_BEGIN(out, "fact", &chunk_start); + CHUNK_WRITE(out, &fact); + CHUNK_END(out, chunk_start, htole32); + + if (in->data_type == SAMPLE_TYPE_PCM16) { + assert(in->bit_depth == 16); + assert(in->data_size % 2 == 0); + + for (size_t i = 0; i < in->data_size / 2; i++) { + ((uint16_t *)in->data)[i] = htole16(((uint16_t *)in->data)[i]); + } + } + + CHUNK_BEGIN(out, "data", &chunk_start); + CHUNK_WRITE_RAW(out, in->data, in->data_size); + CHUNK_END(out, chunk_start, htole32); + + wav_inst inst = { + .base_note = in->base_note, + .fine_tune = in->fine_tune, + .gain = in->gain, + .key_low = in->key_low, + .key_hi = in->key_hi, + .vel_low = in->vel_low, + .vel_hi = in->vel_hi, + ._pad = { 0 }, + }; + CHUNK_BEGIN(out, "inst", &chunk_start); + CHUNK_WRITE(out, &inst); + CHUNK_END(out, chunk_start, htole32); + + wav_smpl smpl = { + .manufacturer = 0, + .product = 0, + .sample_period = 0, + .unity_note = 0, + ._pad = {0, 0, 0}, + .fine_tune = 0, + .format = 0, + .offset = 0, + .num_sample_loops = htole32(in->num_loops), + .sampler_data = 0, + }; + CHUNK_BEGIN(out, "smpl", &chunk_start); + CHUNK_WRITE(out, &smpl); + for (size_t i = 0; i < in->num_loops; i++) { + uint32_t wav_loop_type; + switch (in->loops[i].type) { + case LOOP_FORWARD: + wav_loop_type = 0; + break; + + case LOOP_FORWARD_BACKWARD: + wav_loop_type = 1; + break; + + case LOOP_BACKWARD: + wav_loop_type = 2; + break; + + default: + error("Unrecognized loop type for wav output"); + } + + wav_loop loop = { + .cue_point_index = 0, + .type = htole32(wav_loop_type), + .start = htole32(in->loops[i].start), + .end = htole32(in->loops[i].end), + .fraction = htole32(in->loops[i].fraction), + .num = htole32(in->loops[i].num), + }; + CHUNK_WRITE(out, &loop); + } + CHUNK_END(out, chunk_start, htole32); + + // Books generally don't match on round-trip, if matching we should write a vadpcm book chunk to the uncompressed + // output so it may be read back when re-encoding. + if (in->vadpcm.has_book && matching) { + uint32_t book_size = VADPCM_BOOK_SIZE(in->vadpcm.book_header.order, in->vadpcm.book_header.npredictors); + + uint16_t version = htole16(in->vadpcm.book_version); + uint16_t order = htole16(in->vadpcm.book_header.order); + uint16_t npredictors = htole16(in->vadpcm.book_header.npredictors); + + int16_t book_state[book_size]; + for (size_t i = 0; i < book_size; i++) { + book_state[i] = htole16(in->vadpcm.book_data[i]); + } + + CHUNK_BEGIN(out, "zzbk", &chunk_start); + CHUNK_WRITE_RAW(out, "VADPCMCODES", sizeof("VADPCMCODES")); + CHUNK_WRITE(out, &version); + CHUNK_WRITE(out, &order); + CHUNK_WRITE(out, &npredictors); + CHUNK_WRITE_RAW(out, book_state, sizeof(int16_t) * book_size); + CHUNK_END(out, chunk_start, htole32); + } + + // Loop states match on round-trip while books don't, so don't write a vadpcm loop chunk if the output format + // is uncompressed. + if (in->vadpcm.num_loops != 0 && in->data_type != SAMPLE_TYPE_PCM16) { + CHUNK_BEGIN(out, "zzlp", &chunk_start); + + for (size_t i = 0; i < in->vadpcm.num_loops; i++) { + uint32_t loop_start = htole32(in->vadpcm.loops[i].start); + uint32_t loop_end = htole32(in->vadpcm.loops[i].end); + uint32_t loop_count = htole32(in->vadpcm.loops[i].count); + + int16_t loop_state[16]; + for (size_t j = 0; j < ARRAY_COUNT(in->vadpcm.loops[i].state); j++) { + loop_state[j] = htole16(in->vadpcm.loops[i].state[j]); + } + + CHUNK_WRITE(out, &loop_start); + CHUNK_WRITE(out, &loop_end); + CHUNK_WRITE(out, &loop_count); + CHUNK_WRITE_RAW(out, loop_state, sizeof(loop_state)); + } + + CHUNK_END(out, chunk_start, htole32); + } + + uint32_t size = htole32(ftell(out) - 8); + fseek(out, 4, SEEK_SET); + fwrite(&size, 4, 1, out); + fclose(out); + + return 0; +} diff --git a/tools/audio/sampleconv/src/container/wav.h b/tools/audio/sampleconv/src/container/wav.h new file mode 100644 index 0000000000..71e6416909 --- /dev/null +++ b/tools/audio/sampleconv/src/container/wav.h @@ -0,0 +1,20 @@ +/** + * 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 WAV_H +#define WAV_H + +#include "../codec/vadpcm.h" +#include "container.h" + +int +wav_write(container_data *in, const char *path, bool matching); +int +wav_read(container_data *out, const char *path, bool matching); + +#endif diff --git a/tools/audio/sampleconv/src/main.c b/tools/audio/sampleconv/src/main.c new file mode 100644 index 0000000000..6c839bd828 --- /dev/null +++ b/tools/audio/sampleconv/src/main.c @@ -0,0 +1,209 @@ +/** + * 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 "util.h" + +#include "codec/codec.h" +#include "codec/uncompressed.h" +#include "codec/vadpcm.h" + +#include "container/container.h" +#include "container/wav.h" +#include "container/aiff.h" + +static const codec_spec codecs[] = { + {"pcm16", SAMPLE_TYPE_PCM16, 16, false, pcm16_enc_dec, pcm16_enc_dec}, + { "vadpcm", SAMPLE_TYPE_VADPCM, 9, true, vadpcm_dec, vadpcm_enc }, + { "vadpcm-half", SAMPLE_TYPE_VADPCM_HALF, 5, true, vadpcm_dec, vadpcm_enc }, + // { "pcm8", SAMPLE_TYPE_PCM8, TODO, TODO, pcm8_dec, pcm8_enc }, +}; + +static const container_spec containers[] = { + {".wav", wav_read, wav_write }, + { ".aiff", aiff_read, aiff_write}, + { ".aifc", aifc_read, aifc_write}, +}; + +static bool +str_endswith(const char *str1, const char *str2) +{ + size_t len1 = strlen(str1); + size_t len2 = strlen(str2); + + if (len2 > len1) + return false; + return strequ(str1 + len1 - len2, str2); +} + +NORETURN static void +help(const char *progname) +{ + fprintf(stderr, "%s [--matching] out_codec_name in_path out_path\n", progname); + fprintf(stderr, "Supported codecs:\n"); + fprintf(stderr, " pcm16\n"); + fprintf(stderr, " vadpcm\n"); + fprintf(stderr, " vadpcm-half\n"); + fprintf(stderr, "Possible input/output formats with supported codecs, automatically selected by file extension:\n"); + fprintf(stderr, " wav [pcm16]\n"); + fprintf(stderr, " aiff [pcm16]\n"); + fprintf(stderr, " aifc [vadpcm, vadpcm-half]\n"); + exit(EXIT_FAILURE); +} + +NORETURN static void +usage(const char *progname) +{ + fprintf(stderr, "%s [--matching] out_codec_name in_path out_path\n", progname); + exit(EXIT_FAILURE); +} + +static const codec_spec * +codec_from_name(const char *name) +{ + for (size_t i = 0; i < ARRAY_COUNT(codecs); i++) { + if (strequ(name, codecs[i].name)) + return &codecs[i]; + } + return NULL; +} + +static const codec_spec * +codec_from_type(sample_data_type type) +{ + for (size_t i = 0; i < ARRAY_COUNT(codecs); i++) { + if (type == codecs[i].type) + return &codecs[i]; + } + return NULL; +} + +static const container_spec * +container_from_name(const char *name) +{ + for (size_t i = 0; i < ARRAY_COUNT(containers); i++) { + if (str_endswith(name, containers[i].fext)) + return &containers[i]; + } + return NULL; +} + +int +main(int argc, char **argv) +{ + const char *progname = argv[0]; + // Required arguments + const char *in_path = NULL; + const char *out_path = NULL; + const char *out_codec_name = NULL; + // Optional arguments + enc_dec_opts opts = { + .matching = false, + // VADPCM + .truncate = false, + .min_loop_length = 800, + .design.order = 2, + .design.bits = 2, + .design.refine_iters = 2, + .design.thresh = 10.0, + .design.frame_size = 16, + }; + + // parse args + +#define arg_error(fmt, ...) \ + do { \ + fprintf(stderr, fmt "\n", ##__VA_ARGS__); \ + usage(progname); \ + } while (0) + + int argn = 0; + for (int i = 1; i < argc; i++) { + if (argv[i][0] == '-') { + // Optional args + + if (strequ(argv[i], "--help")) { + help(progname); + } else if (strequ(argv[i], "--matching")) { + if (opts.matching) + arg_error("Received --matching option twice"); + + opts.matching = true; + continue; + } + arg_error("Unknown option \"%s\"", argv[i]); + } else { + // Required args + + switch (argn) { + case 0: + out_codec_name = argv[i]; + break; + case 1: + in_path = argv[i]; + break; + case 2: + out_path = argv[i]; + break; + default: + arg_error("Unknown positional argument \"%s\"", argv[i]); + break; + } + argn++; + } + } + if (argn != 3) + arg_error("Not enough positional arguments"); + +#undef arg_error + + const container_spec *in_container = container_from_name(in_path); + if (in_container == NULL) + error("Unsupported input format"); + + const container_spec *out_container = container_from_name(out_path); + if (out_container == NULL) + error("Unsupported output format"); + + const codec_spec *out_codec = codec_from_name(out_codec_name); + if (out_codec == NULL) + error("Unrecognized output codec: \"%s\"", out_codec_name); + + container_data ctnr; + + // Read input from container + if (in_container->read(&ctnr, in_path, opts.matching)) + error("Error reading input file"); + + // Determine input codec from input container automatically + const codec_spec *in_codec = codec_from_type(ctnr.data_type); + if (in_codec == NULL) + error("Unrecognized input codec: type=%d", ctnr.data_type); + + // Decode to PCM16 (this does nothing if in_codec is PCM16) + if (in_codec->decode(&ctnr, in_codec, &opts)) + error("Error in decoding"); + + // Encode to output (this does nothing if out_codec is PCM16) + if (out_codec->encode(&ctnr, out_codec, &opts)) + error("Error in encoding"); + + // Write output to container + if (out_container->write(&ctnr, out_path, opts.matching)) + error("Error reading output file"); + + container_destroy(&ctnr); + return EXIT_SUCCESS; +} diff --git a/tools/audio/sampleconv/src/util.c b/tools/audio/sampleconv/src/util.c new file mode 100644 index 0000000000..43219db74c --- /dev/null +++ b/tools/audio/sampleconv/src/util.c @@ -0,0 +1,43 @@ +/** + * 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 "util.h" + +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); +} diff --git a/tools/audio/sampleconv/src/util.h b/tools/audio/sampleconv/src/util.h new file mode 100644 index 0000000000..0c1c0e1792 --- /dev/null +++ b/tools/audio/sampleconv/src/util.h @@ -0,0 +1,116 @@ +/** + * 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 UTIL_H +#define UTIL_H + +#include + +// Endian + +#if defined(__linux__) || defined(__CYGWIN__) +#include +#elif defined(__APPLE__) +#include + +#define htobe16(x) OSSwapHostToBigInt16(x) +#define htole16(x) OSSwapHostToLittleInt16(x) +#define be16toh(x) OSSwapBigToHostInt16(x) +#define le16toh(x) OSSwapLittleToHostInt16(x) + +#define htobe32(x) OSSwapHostToBigInt32(x) +#define htole32(x) OSSwapHostToLittleInt32(x) +#define be32toh(x) OSSwapBigToHostInt32(x) +#define le32toh(x) OSSwapLittleToHostInt32(x) + +#define htobe64(x) OSSwapHostToBigInt64(x) +#define htole64(x) OSSwapHostToLittleInt64(x) +#define be64toh(x) OSSwapBigToHostInt64(x) +#define le64toh(x) OSSwapLittleToHostInt64(x) +#else +#error "Endian conversion unsupported, add it" +#endif + +#define ARRAY_COUNT(arr) (sizeof(arr) / sizeof((arr)[0])) + +#define NORETURN __attribute__((noreturn)) +#define UNUSED __attribute__((unused)) + +#define strequ(s1, s2) ((__builtin_constant_p(s2) ? strncmp(s1, s2, sizeof(s2) - 1) : strcmp(s1, s2)) == 0) + +#define MALLOC_CHECKED(length) \ + ({ \ + size_t malloc_len_ = (size_t)(length); \ + void *result_ = malloc(malloc_len_); \ + if (result_ == NULL) \ + error("[malloc] Failed to allocate %lu bytes @ [%s:%u]", malloc_len_, __FILE__, __LINE__); \ + result_; \ + }) + +#define MALLOC_CHECKED_INFO(length, fmt, ...) \ + ({ \ + size_t malloc_len_ = (size_t)(length); \ + void *result_ = malloc(malloc_len_); \ + if (result_ == NULL) \ + error("[malloc] Failed to allocate %lu bytes @ [%s:%u] (" fmt ")", malloc_len_, __FILE__, __LINE__, \ + ##__VA_ARGS__); \ + result_; \ + }) + +#define MIN(a, b) (((a) < (b)) ? (a) : (b)) + +#define ABS(x) (((x) < 0) ? (-(x)) : (x)) + +#define FWRITE(file, data, size) \ + do { \ + if (fwrite((data), (size), 1, (file)) != 1) { \ + error("[%s:%d] Could not write %lu bytes to file", __FILE__, __LINE__, (size_t)(size)); \ + } \ + } while (0) + +#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 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 CHUNK_BEGIN(file, name, start) \ + do { \ + *(start) = ftell(out); \ + FWRITE(file, name "\0\0\0\0", 8); \ + } while (0) + +#define CHUNK_WRITE(file, structure) \ + do { \ + FWRITE(file, structure, sizeof(*(structure))); \ + } while (0) + +#define CHUNK_WRITE_RAW(file, data, length) FWRITE(file, data, length) + +#define CHUNK_END(file, start, endian_func) \ + do { \ + long end = ftell(out); \ + uint32_t size = endian_func(end - (start)-8); \ + fseek(out, (start) + 4, SEEK_SET); \ + FWRITE(out, &size, 4); \ + fseek(out, end, SEEK_SET); \ + } while (0) + +__attribute__((format(printf, 1, 2))) NORETURN void +error(const char *fmt, ...); + +__attribute__((format(printf, 1, 2))) void +warning(const char *fmt, ...); + +#endif