mirror of
https://github.com/pret/pokeemerald.git
synced 2026-03-21 09:45:09 -05:00
Add --set-agbl option to wav2agb
This commit is contained in:
parent
8d1f3ccd6a
commit
163c8a4321
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user