Add --set-agbl option to wav2agb

This commit is contained in:
Marcus Huderle 2025-12-28 08:50:34 -06:00
parent 8d1f3ccd6a
commit 163c8a4321
4 changed files with 180 additions and 27 deletions

View File

@ -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] <input.wav> [<output>]
@ -24,6 +27,29 @@ Options:
--tune <cents> | override tuning (float)
--key <key> | override midi key (int)
--rate <rate> | override base samplerate (int)
--set-agbl <loop-end> | 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
```

View File

@ -7,6 +7,7 @@
#include <string>
#include "converter.h"
#include "wav_file.h"
static void usage() {
fprintf(stderr, "wav2agb\n");
@ -25,6 +26,7 @@ static void usage() {
fprintf(stderr, "--tune <cents> | override tuning (float)\n");
fprintf(stderr, "--key <key> | override midi key (int)\n");
fprintf(stderr, "--rate <rate> | override base samplerate (int)\n");
fprintf(stderr, "--set-agbl <loop-end> | 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<uint32_t>(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<int64_t>(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<uint32_t>(calculated);
} else {
// Positive value: use directly
loop_end_value = static_cast<uint32_t>(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) {

View File

@ -4,6 +4,7 @@
#include <cerrno>
#include <cstring>
#include <algorithm>
#include <fstream>
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<char *>(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<uint8_t> 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<uint8_t> 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<uint8_t> smplChunk = read_arr(ifs, chunkLen);
uint32_t midiUnityNote = arr_u32(smplChunk, 12);
uint32_t midiUnityNote = arr_u32(chunkData, 12);
this->midiKey = static_cast<uint8_t>(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<double>(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<uint8_t> 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<uint8_t> 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<uint32_t>(dataChunkEndPos - dataChunkPos) / fmt_size();
this->loopEnd = std::min(this->loopEnd, numSamples);
this->numSamples = static_cast<uint32_t>(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<WavChunk>& 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<const char*>(chunk.data.data()), chunk.data.size());
}
if (chunk.data.size() % 2 == 1) {
ofs.put(0);
}
}
ofs.close();
}

View File

@ -8,6 +8,16 @@
#define WAV_INVALID_VAL 0xFFFFFFFFu
// Structure for WAV chunk utilities
struct WavChunk {
std::string id;
std::vector<uint8_t> data;
};
void write_wav_with_agbl_chunk(const std::string& output_path,
std::vector<WavChunk>& 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<WavChunk> chunks; // raw chunks from the WAV file (for re-writing with modifications)
};