Update .wav samples to use 'agbl' chunk to accomodate for vanilla's off-by-one loop end behavior (#712)

This commit is contained in:
Marcus Huderle
2025-12-28 09:10:08 -06:00
committed by GitHub
parent 14b76daff8
commit 338ec9d956
483 changed files with 204 additions and 50 deletions
+109 -22
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,38 +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)
if (chunkLen >= 4) {
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
@@ -185,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()
@@ -285,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();
}