From 163c8a43211bb846306e886d58449c71ef3145cb Mon Sep 17 00:00:00 2001 From: Marcus Huderle Date: Sun, 28 Dec 2025 08:50:34 -0600 Subject: [PATCH] Add --set-agbl option to wav2agb --- tools/wav2agb/README.md | 30 ++++++++- tools/wav2agb/wav2agb.cpp | 36 ++++++++++- tools/wav2agb/wav_file.cpp | 129 ++++++++++++++++++++++++++++++------- tools/wav2agb/wav_file.h | 12 ++++ 4 files changed, 180 insertions(+), 27 deletions(-) diff --git a/tools/wav2agb/README.md b/tools/wav2agb/README.md index 86f2660231..3b8b32e9b8 100644 --- a/tools/wav2agb/README.md +++ b/tools/wav2agb/README.md @@ -6,9 +6,12 @@ This copy has been slightly modified from [ipatix's original implementation](htt 2. Support reading an override "pitch" value from a custom `agbp` RIFF chunk. - This is needed to properly match some samples, due to float-point rounding errors when attempting to infer the pitch/sample rate from the .wav file's sample rate. - If the custom `agbp` chunk isn't present, it will simply use the .wav's sample rate to calculate this "pitch" value. -3. Optionally omits trailing padding from compressed output. +3. Support reading an override "loop end" value from a custom `agbl` RIFF chunk. + - This is needed to properly match vanilla samples, due their their inherent off-by-one error (the last sample is mistakenly ignored). + - This `agbl` chunk can be added to existing .wav files with the `--set-agbl` option (described below). +4. Optionally omits trailing padding from compressed output. -Usage: +Usage: ``` Usage: wav2agb [options] [] @@ -24,6 +27,29 @@ Options: --tune | override tuning (float) --key | override midi key (int) --rate | override base samplerate (int) +--set-agbl | adds the custom agbl chunk to the given input .wav file ``` Flag -c enables compression (only supported by Pokemon Games) + +## Adding agbl Chunk to WAV Files + +The `--set-agbl` option allows you to add or update the custom `agbl` chunk in a WAV file. When this option is used, `wav2agb` will output a WAV file with the agbl chunk added, rather than converting to `.s` or `.bin` format. + +The loop-end value can be specified as either: +- **Positive value**: Used as an absolute sample position +- **Negative value**: Treated as an offset from the end of the file + +This is useful for correcting the off-by-one loop-end error in vanilla samples. The typical fix is `--set-agbl -1`, which sets the loop-end to `(total_samples - 1)`. + +Example: +```bash +# Set agbl to (total_samples - 1), the most common case for fixing the off-by-one error +wav2agb --set-agbl -1 input.wav + +# Set agbl chunk to specific sample position 12345 +wav2agb --set-agbl 12345 input.wav output.wav + +# If no output file is specified, the input file is modified in place +wav2agb --set-agbl -1 input.wav +``` diff --git a/tools/wav2agb/wav2agb.cpp b/tools/wav2agb/wav2agb.cpp index be018abad6..2d4a84517e 100644 --- a/tools/wav2agb/wav2agb.cpp +++ b/tools/wav2agb/wav2agb.cpp @@ -7,6 +7,7 @@ #include #include "converter.h" +#include "wav_file.h" static void usage() { fprintf(stderr, "wav2agb\n"); @@ -25,6 +26,7 @@ static void usage() { fprintf(stderr, "--tune | override tuning (float)\n"); fprintf(stderr, "--key | override midi key (int)\n"); fprintf(stderr, "--rate | override base samplerate (int)\n"); + fprintf(stderr, "--set-agbl | adds the custom agbl chunk to the given input .wav file\n"); exit(1); } @@ -106,6 +108,8 @@ static bool arg_input_file_read = false; static bool arg_output_file_read = false; static std::string arg_input_file; static std::string arg_output_file; +static bool arg_set_agbl = false; +static int32_t arg_agbl_value = 0; int main(int argc, char *argv[]) { try { @@ -163,6 +167,11 @@ int main(int argc, char *argv[]) { die("--rate: missing parameter"); uint32_t rate = static_cast(std::stoul(argv[i], nullptr, 10)); set_wav_rate(rate); + } else if (st == "--set-agbl") { + if (++i >= argc) + die("--set-agbl: missing parameter"); + arg_agbl_value = std::stoi(argv[i], nullptr, 10); + arg_set_agbl = true; } else { if (st == "--") { if (++i >= argc) @@ -191,7 +200,9 @@ int main(int argc, char *argv[]) { if (!arg_output_file_read) { // create output file name if none is provided - if (arg_output_type == out_type::binary) { + if (arg_set_agbl) { + arg_output_file = arg_input_file; + } else if (arg_output_type == out_type::binary) { arg_output_file = filename_without_ext(arg_input_file) + ".bin"; } else { arg_output_file = filename_without_ext(arg_input_file) + ".s"; @@ -204,6 +215,29 @@ int main(int argc, char *argv[]) { fix_str(arg_sym); } + if (arg_set_agbl) { + // Parse the WAV file once to get both chunks and metadata + wav_file wav(arg_input_file); + + // Calculate actual loop-end value + uint32_t loop_end_value; + if (arg_agbl_value < 0) { + // Negative value: offset from end of samples + int64_t calculated = static_cast(wav.numSamples) + arg_agbl_value; + if (calculated < 0) { + die("--set-agbl: negative offset %d exceeds total samples %u\n", + arg_agbl_value, wav.numSamples); + } + loop_end_value = static_cast(calculated); + } else { + // Positive value: use directly + loop_end_value = static_cast(arg_agbl_value); + } + + write_wav_with_agbl_chunk(arg_output_file, wav.chunks, loop_end_value); + return 0; + } + convert(arg_input_file, arg_output_file, arg_sym, arg_compress, arg_output_type); return 0; } catch (const std::exception& e) { diff --git a/tools/wav2agb/wav_file.cpp b/tools/wav2agb/wav_file.cpp index 4d21b72eb4..79cfed5d79 100644 --- a/tools/wav2agb/wav_file.cpp +++ b/tools/wav2agb/wav_file.cpp @@ -4,6 +4,7 @@ #include #include #include +#include static uint32_t read_u32(std::ifstream& ifs) { @@ -13,6 +14,16 @@ static uint32_t read_u32(std::ifstream& ifs) return retval; } +static void write_u32(std::ofstream& ofs, uint32_t value) +{ + uint8_t bytes[4]; + bytes[0] = value & 0xFF; + bytes[1] = (value >> 8) & 0xFF; + bytes[2] = (value >> 16) & 0xFF; + bytes[3] = (value >> 24) & 0xFF; + ofs.write(reinterpret_cast(bytes), sizeof(bytes)); +} + //static uint16_t read_u16(std::ifstream& ifs) //{ // uint8_t lenBytes[2]; @@ -105,16 +116,21 @@ wav_file::wav_file(const std::string& path) : loadBuffer(loadChunkSize) if (curPos + std::streampos(8) + std::streampos(chunkLen) > len) throw std::runtime_error("ERROR: chunk goes beyond end of file: offset=" + std::to_string(curPos)); + std::vector chunkData = read_arr(ifs, chunkLen); + WavChunk chunk; + chunk.id = chunkId; + chunk.data = chunkData; + this->chunks.push_back(chunk); + if (chunkId == "fmt ") { fmtChunkFound = true; - std::vector fmtChunk = read_arr(ifs, chunkLen); - uint16_t fmtTag = arr_u16(fmtChunk, 0); - uint16_t numChannels = arr_u16(fmtChunk, 2); + uint16_t fmtTag = arr_u16(chunkData, 0); + uint16_t numChannels = arr_u16(chunkData, 2); if (numChannels != 1) throw std::runtime_error("ERROR: input file is NOT mono"); - this->sampleRate = arr_u32(fmtChunk, 4); - uint16_t block_align = arr_u16(fmtChunk, 12); - uint16_t bits_per_sample = arr_u16(fmtChunk, 14); + this->sampleRate = arr_u32(chunkData, 4); + uint16_t block_align = arr_u16(chunkData, 12); + uint16_t bits_per_sample = arr_u16(chunkData, 14); if (fmtTag == 1) { // integer if (block_align == 1 && bits_per_sample == 8) @@ -140,44 +156,41 @@ wav_file::wav_file(const std::string& path) : loadBuffer(loadChunkSize) } } else if (chunkId == "data") { dataChunkFound = true; - dataChunkPos = ifs.tellg(); + // For data chunk, we need to track position in the file for later reading + // The data was already read into chunkData and saved to chunks vector + // But we need to calculate the position for the readData function + // Since we already read the data, we're now past it in the file + dataChunkPos = curPos + std::streampos(8); // Skip chunk ID and size dataChunkEndPos = dataChunkPos + std::streampos(chunkLen); - ifs.seekg(chunkLen, ifs.cur); } else if (chunkId == "smpl") { - std::vector smplChunk = read_arr(ifs, chunkLen); - uint32_t midiUnityNote = arr_u32(smplChunk, 12); + uint32_t midiUnityNote = arr_u32(chunkData, 12); this->midiKey = static_cast(std::min(midiUnityNote, 127u)); - uint32_t midiPitchFraction = arr_u32(smplChunk, 16); + uint32_t midiPitchFraction = arr_u32(chunkData, 16); // the values below convert the uint32_t range to 0.0 to 100.0 range this->tuning = static_cast(midiPitchFraction) / (4294967296.0 * 100.0); - uint32_t numLoops = arr_u32(smplChunk, 28); + uint32_t numLoops = arr_u32(chunkData, 28); if (numLoops > 1) throw std::runtime_error("ERROR: too many loops in smpl chunk"); if (numLoops == 1) { - uint32_t loopType = arr_u32(smplChunk, 36 + 4); + uint32_t loopType = arr_u32(chunkData, 36 + 4); if (loopType != 0) throw std::runtime_error("ERROR: loop type not supported: " + std::to_string(loopType)); - this->loopStart = arr_u32(smplChunk, 36 + 8); + this->loopStart = arr_u32(chunkData, 36 + 8); // sampler chunks tell the last sample to be played (so including rather than excluding), thus +1 - this->loopEnd = arr_u32(smplChunk, 36 + 12) + 1; + this->loopEnd = arr_u32(chunkData, 36 + 12) + 1; this->loopEnabled = true; } } else if (chunkId == "agbp") { // Custom chunk: exact GBA pitch value (sample_rate * 1024) // This allows perfect round-trip conversion without period-based precision loss - std::vector agbpChunk = read_arr(ifs, chunkLen); if (chunkLen >= 4) { - this->agbPitch = arr_u32(agbpChunk, 0); + this->agbPitch = arr_u32(chunkData, 0); } } else if (chunkId == "agbl") { // Custom chunk: exact loop end override (handles off-by-one from original game) - std::vector agblChunk = read_arr(ifs, chunkLen); if (chunkLen >= 4) { - this->agbLoopEnd = arr_u32(agblChunk, 0); + this->agbLoopEnd = arr_u32(chunkData, 0); } - } else { - //fprintf(stderr, "WARNING: ignoring unknown chunk type: <%s>\n", chunkId.c_str()); - ifs.seekg(chunkLen, ifs.cur); } /* https://en.wikipedia.org/wiki/Resource_Interchange_File_Format#Explanation @@ -191,8 +204,8 @@ wav_file::wav_file(const std::string& path) : loadBuffer(loadChunkSize) if (!dataChunkFound) throw std::runtime_error("ERROR: data chunk not found"); - uint32_t numSamples = static_cast(dataChunkEndPos - dataChunkPos) / fmt_size(); - this->loopEnd = std::min(this->loopEnd, numSamples); + this->numSamples = static_cast(dataChunkEndPos - dataChunkPos) / fmt_size(); + this->loopEnd = std::min(this->loopEnd, this->numSamples); } wav_file::~wav_file() @@ -291,3 +304,71 @@ load_sample: location++; } } + +// In the future, if wav2agb gains the ability to construct .wav files from .bin files, +// this function should be rolled into that flow. +void write_wav_with_agbl_chunk(const std::string& output_path, + std::vector& chunks, + uint32_t loop_end_value) +{ + bool has_agbl = false; + for (auto& chunk : chunks) { + if (chunk.id == "agbl") { + has_agbl = true; + chunk.data.resize(4); + chunk.data[0] = loop_end_value & 0xFF; + chunk.data[1] = (loop_end_value >> 8) & 0xFF; + chunk.data[2] = (loop_end_value >> 16) & 0xFF; + chunk.data[3] = (loop_end_value >> 24) & 0xFF; + break; + } + } + + if (!has_agbl) { + WavChunk agbl_chunk; + agbl_chunk.id = "agbl"; + agbl_chunk.data.resize(4); + agbl_chunk.data[0] = loop_end_value & 0xFF; + agbl_chunk.data[1] = (loop_end_value >> 8) & 0xFF; + agbl_chunk.data[2] = (loop_end_value >> 16) & 0xFF; + agbl_chunk.data[3] = (loop_end_value >> 24) & 0xFF; + for (size_t i = 0; i < chunks.size(); i++) { + if (chunks[i].id == "data") { + chunks.insert(chunks.begin() + i, agbl_chunk); + break; + } + } + } + + // Calculate total RIFF size + uint32_t total_chunk_size = 0; + for (const auto& chunk : chunks) { + total_chunk_size += 8 + chunk.data.size(); + if (chunk.data.size() % 2 == 1) { + total_chunk_size += 1; + } + } + uint32_t riff_size = 4 + total_chunk_size; + + std::ofstream ofs(output_path, std::ios::binary); + if (!ofs.good()) + throw std::runtime_error("Failed to open output file: " + output_path); + + ofs.write("RIFF", 4); + write_u32(ofs, riff_size); + ofs.write("WAVE", 4); + + for (const auto& chunk : chunks) { + ofs.write(chunk.id.c_str(), 4); + write_u32(ofs, chunk.data.size()); + if (!chunk.data.empty()) { + ofs.write(reinterpret_cast(chunk.data.data()), chunk.data.size()); + } + + if (chunk.data.size() % 2 == 1) { + ofs.put(0); + } + } + + ofs.close(); +} diff --git a/tools/wav2agb/wav_file.h b/tools/wav2agb/wav_file.h index c529a0b88a..cea278970d 100644 --- a/tools/wav2agb/wav_file.h +++ b/tools/wav2agb/wav_file.h @@ -8,6 +8,16 @@ #define WAV_INVALID_VAL 0xFFFFFFFFu +// Structure for WAV chunk utilities +struct WavChunk { + std::string id; + std::vector data; +}; + +void write_wav_with_agbl_chunk(const std::string& output_path, + std::vector& chunks, + uint32_t loop_end_value); + class wav_file { public: wav_file(const std::string& path); @@ -34,6 +44,8 @@ public: double tuning = 0.0; // cents uint8_t midiKey = 60; uint32_t sampleRate; + uint32_t numSamples = 0; // total number of samples in the file uint32_t agbPitch = 0; // optional: exact GBA pitch value from 'agbp' chunk (0 = not present) uint32_t agbLoopEnd = 0; // optional: exact loop end from 'agbl' chunk (0 = not present) + std::vector chunks; // raw chunks from the WAV file (for re-writing with modifications) };