Convert .aif files to .wav (#711)

* Migrate .aif files to .wav

* cleanup
This commit is contained in:
Marcus Huderle
2025-12-01 09:01:35 -06:00
committed by GitHub
parent 7299efd381
commit 14b76daff8
971 changed files with 1123 additions and 1158 deletions
+10
View File
@@ -0,0 +1,10 @@
*.o
*.exe
*.s
*.gba
*.sdf
wav2agb
Debug
Release
.vs
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 ipatix
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+33
View File
@@ -0,0 +1,33 @@
GIT_VERSION := $(shell git describe --abbrev=7 --dirty --always --tags)
CXX = g++
STRIP = strip
CXXFLAGS = -Wall -Wextra -Wconversion -std=c++17 -O2 -g -DGIT_VERSION=\"$(GIT_VERSION)\"
EXE :=
ifeq ($(OS),Windows_NT)
EXE := .exe
endif
BINARY = wav2agb$(EXE)
SRC_FILES = $(wildcard *.cpp)
OBJ_FILES = $(SRC_FILES:.cpp=.o)
LDFLAGS :=
ifneq (,$(RELEASE))
LDFLAGS += -static
CXXFLAGS += -flto
endif
.PHONY: clean clean
all: $(BINARY)
clean:
rm -f $(OBJ_FILES) $(BINARY)
$(BINARY): $(OBJ_FILES)
$(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ $^
if [ $(RELEASE)x != x ]; then strip -s $@; fi
%.o: %.cpp
$(CXX) -c -o $@ $< $(CXXFLAGS)
+29
View File
@@ -0,0 +1,29 @@
# wav2agb
"wav2agb" is a tool to convert standard .wav files to GBA compatible .s or .bin files. Intended to convert .wav files for the use with the mp2k/m4a sound driver.
This copy has been slightly modified from [ipatix's original implementation](https://github.com/ipatix/wav2agb) in the following ways:
1. Support outputting `.bin` files with a command line option `-b, --binary`. (The original only supports outputting `.s` assembly files.)
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.
Usage:
```
Usage: wav2agb [options] <input.wav> [<output>]
Options:
-s, --symbol <sym> | symbol name for wave header (default: file name)
-l, --lookahead <amount> | DPCM compression lookahead 1..8 (default: 3)
-c, --compress | compress output with DPCM
-f, --fast-compress | compress output with DPCM fast
--no-pad | omit trailing padding in compressed output
-b, --binary | output raw binary instead of assembly
--loop-start <pos> | override loop start (integer)
--loop-end <pos> | override loop end (integer)
--tune <cents> | override tuning (float)
--key <key> | override midi key (int)
--rate <rate> | override base samplerate (int)
```
Flag -c enables compression (only supported by Pokemon Games)
+462
View File
@@ -0,0 +1,462 @@
#include "converter.h"
#include <stdexcept>
#include <fstream>
#include <algorithm>
#include <vector>
#include <cmath>
#include <cstdarg>
#include <cassert>
#include <cstring>
#include <chrono>
#include "wav_file.h"
static void agb_out(std::ofstream& ofs, const char *msg, ...) {
char buf[256];
va_list args;
va_start(args, msg);
vsnprintf(buf, sizeof(buf), msg, args);
va_end(args);
ofs << buf;
}
static void data_write(std::ofstream& ofs, uint32_t& block_pos, int data, bool hex) {
if (block_pos++ == 0) {
if (hex)
agb_out(ofs, "\n .byte 0x%02X", data);
else
agb_out(ofs, "\n .byte %4d", data);
} else {
if (hex)
agb_out(ofs, ", 0x%02X", data);
else
agb_out(ofs, ", %4d", data);
}
block_pos %= 16;
}
static void bin_write_u8(std::vector<uint8_t>& data, uint8_t value) {
data.push_back(value);
}
static void bin_write_u32_le(std::vector<uint8_t>& data, uint32_t value) {
data.push_back(static_cast<uint8_t>(value & 0xFF));
data.push_back(static_cast<uint8_t>((value >> 8) & 0xFF));
data.push_back(static_cast<uint8_t>((value >> 16) & 0xFF));
data.push_back(static_cast<uint8_t>((value >> 24) & 0xFF));
}
template<typename T>
const T& clamp(const T& v, const T& lo, const T& hi) {
return (v < lo) ? lo : (hi < v) ? hi : v;
}
static void convert_uncompressed(wav_file& wf, std::ofstream& ofs)
{
int loop_sample = 0;
uint32_t block_pos = 0;
for (size_t i = 0; i < wf.loopEnd; i++) {
double ds;
wf.readData(i, &ds, 1);
// TODO apply dither noise
int s = clamp(static_cast<int>(floor(ds * 128.0)), -128, 127);
if (wf.loopEnabled && i == wf.loopStart)
loop_sample = s;
data_write(ofs, block_pos, s, false);
}
data_write(ofs, block_pos, loop_sample, false);
}
static void convert_uncompressed_bin(wav_file& wf, std::vector<uint8_t>& data)
{
for (size_t i = 0; i < wf.loopEnd; i++) {
double ds;
wf.readData(i, &ds, 1);
// TODO apply dither noise
int s = clamp(static_cast<int>(floor(ds * 128.0)), -128, 127);
bin_write_u8(data, static_cast<uint8_t>(s));
}
// Align to 4 bytes.
while (data.size() % 4 != 0) {
bin_write_u8(data, 0);
}
}
static uint32_t wav_loop_start;
static bool wav_loop_start_override = false;
static uint32_t wav_loop_end;
static bool wav_loop_end_override = false;
static double wav_tune;
static bool wav_tune_override = false;
static uint8_t wav_key;
static bool wav_key_override = false;
static uint32_t wav_rate;
static bool wav_rate_override = false;
static bool dpcm_verbose = false;
static bool dpcm_lookahead_fast = false;
static bool dpcm_include_padding = true;
static size_t dpcm_enc_lookahead = 3;
static const size_t DPCM_BLK_SIZE = 0x40;
static const std::vector<int8_t> dpcmLookupTable = {
0, 1, 4, 9, 16, 25, 36, 49, -64, -49, -36, -25, -16, -9, -4, -1
};
static const std::vector<size_t> dpcmIndexTable = {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
};
static const std::vector<std::vector<size_t>> dpcmFastLookupTable = {
{8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8},
{8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8},
{8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8},
{8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8},
{8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8},
{8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8},
{8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8},
{8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8},
{8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8},
{8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8},
{8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8},
{8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8}, {8},
{8, 9}, {8, 9}, {8, 9}, {8, 9}, {8, 9}, {8, 9}, {8, 9}, {8, 9}, {8, 9}, {8, 9}, {8, 9}, {8, 9}, {8, 9}, {8, 9}, {9, 10}, {9, 10},
{9, 10}, {9, 10}, {9, 10}, {9, 10}, {9, 10}, {9, 10}, {9, 10}, {9, 10}, {9, 10}, {9, 10}, {9, 10}, {10, 11}, {10, 11}, {10, 11}, {10, 11}, {10, 11},
{10, 11}, {10, 11}, {10, 11}, {10, 11}, {10, 11}, {10, 11}, {11, 12}, {11, 12}, {11, 12}, {11, 12}, {11, 12}, {11, 12}, {11, 12}, {11, 12}, {11, 12}, {12, 13},
{12, 13}, {12, 13}, {12, 13}, {12, 13}, {12, 13}, {12, 13}, {13, 14}, {13, 14}, {13, 14}, {13, 14}, {13, 14}, {14, 15}, {14, 15}, {14, 15}, {0, 15}, {0, 1, 15},
{1, 0}, {1, 2}, {1, 2}, {2, 1}, {2, 3}, {2, 3}, {2, 3}, {2, 3}, {3, 2}, {3, 4}, {3, 4}, {3, 4}, {3, 4}, {3, 4}, {3, 4}, {4, 3},
{4, 5}, {4, 5}, {4, 5}, {4, 5}, {4, 5}, {4, 5}, {4, 5}, {4, 5}, {5, 4}, {5, 6}, {5, 6}, {5, 6}, {5, 6}, {5, 6}, {5, 6}, {5, 6},
{5, 6}, {5, 6}, {5, 6}, {6, 5}, {6, 7}, {6, 7}, {6, 7}, {6, 7}, {6, 7}, {6, 7}, {6, 7}, {6, 7}, {6, 7}, {6, 7}, {6, 7}, {6, 7},
{7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7},
{7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7},
{7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7},
{7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7},
{7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7},
{7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7},
{7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7},
{7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7},
{7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7},
{7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7},
{7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7},
{7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7},
{7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}, {7}
};
static int squared(int x) { return x * x; }
static void dpcm_lookahead(
int& minimumError, size_t& minimumErrorIndex,
const double *sampleBuf, const size_t lookahead, const int prevLevel)
{
if (lookahead == 0) {
minimumError = 0;
return;
}
minimumError = std::numeric_limits<int>::max();
minimumErrorIndex = dpcmLookupTable.size();
const int s = clamp(static_cast<int>(floor(sampleBuf[0] * 128.0)), -128, 127);
const std::vector<size_t> indexCandicateSet = dpcm_lookahead_fast? dpcmFastLookupTable[s - prevLevel + 255]: dpcmIndexTable;
for (auto i : indexCandicateSet) {
int newLevel = prevLevel + dpcmLookupTable[i];
int recMinimumError;
size_t recMinimumErrorIndex;
// TODO apply dither noise
int errorEstimation = squared(s - newLevel);
if (errorEstimation >= minimumError)
continue;
dpcm_lookahead(recMinimumError, recMinimumErrorIndex,
sampleBuf + 1, lookahead - 1, newLevel);
// TODO weigh the error squared
int error = squared(s - newLevel) + recMinimumError;
if (error < minimumError) {
if (newLevel <= 127 && newLevel >= -128) {
minimumError = error;
minimumErrorIndex = i;
}
}
}
}
static double calculate_snr(const std::vector<double>& uncompressedData, const std::vector<int>& decompressedData)
{
int sum_son = 0;
int sum_mum = 0;
assert(uncompressedData.size() == decompressedData.size());
for (size_t i = 0; i < uncompressedData.size(); i++) {
const int s = clamp(static_cast<int>(floor(uncompressedData[i] * 128.0)), -128, 127) + 128;
sum_son += s * s;
const int sub = decompressedData[i] + 128 - s;
sum_mum += sub * sub;
}
if (sum_mum == 0) {
return 100;
}
return 10 * std::log10((double)sum_son / sum_mum);
}
template<typename InitialSampleWriter, typename CompressedDataWriter>
static void convert_dpcm_impl(wav_file& wf, InitialSampleWriter writeInitialSample, CompressedDataWriter writeCompressedData)
{
int minimumError;
size_t minimumErrorIndex;
std::vector<double> uncompressedData;
std::vector<int> decompressedData;
const auto startTime = std::chrono::high_resolution_clock::now();
for (size_t i = 0; i < wf.loopEnd; i += DPCM_BLK_SIZE) {
double ds[DPCM_BLK_SIZE];
size_t samples_in_block = std::min(DPCM_BLK_SIZE, wf.loopEnd - i);
wf.readData(i, ds, samples_in_block);
// Pad remaining samples in block with zeros if needed
for (size_t j = samples_in_block; j < DPCM_BLK_SIZE; j++) {
ds[j] = 0.0;
}
if (dpcm_verbose) {
uncompressedData.insert(uncompressedData.end(), std::begin(ds), std::end(ds));
}
// TODO apply dither noise
int s = clamp(static_cast<int>(floor(ds[0] * 128.0)), -128, 127);
writeInitialSample(s);
if (dpcm_verbose) {
decompressedData.push_back(s);
}
size_t innerLoopCount = 1;
size_t samples_to_process = dpcm_include_padding ? DPCM_BLK_SIZE : samples_in_block;
uint8_t outData = 0;
size_t sampleBufReadLen;
goto initial_loop_enter;
do {
if (innerLoopCount >= samples_to_process)
break;
sampleBufReadLen = std::min(dpcm_enc_lookahead, DPCM_BLK_SIZE - innerLoopCount);
dpcm_lookahead(
minimumError, minimumErrorIndex,
&ds[innerLoopCount], sampleBufReadLen, s);
outData = static_cast<uint8_t>((minimumErrorIndex & 0xF) << 4);
s += dpcmLookupTable[minimumErrorIndex];
if (dpcm_verbose) {
decompressedData.push_back(s);
}
innerLoopCount += 1;
initial_loop_enter:
if (innerLoopCount >= samples_to_process)
break;
sampleBufReadLen = std::min(dpcm_enc_lookahead, DPCM_BLK_SIZE - innerLoopCount);
dpcm_lookahead(
minimumError, minimumErrorIndex,
&ds[innerLoopCount], sampleBufReadLen, s);
outData |= static_cast<uint8_t>(minimumErrorIndex & 0xF);
s += dpcmLookupTable[minimumErrorIndex];
innerLoopCount += 1;
if (dpcm_verbose) {
decompressedData.push_back(s);
}
writeCompressedData(outData);
} while (innerLoopCount < DPCM_BLK_SIZE);
}
const auto endTime = std::chrono::high_resolution_clock::now();
if (dpcm_verbose) {
const auto dur = std::chrono::duration_cast<std::chrono::nanoseconds>(endTime - startTime);
const double durSecs = static_cast<double>(dur.count()) / 1000000000.0;
printf("SNR: %.2fdB, run time: %.2fs\n", calculate_snr(uncompressedData, decompressedData), durSecs);
}
}
static void convert_dpcm(wav_file& wf, std::ofstream& ofs)
{
uint32_t block_pos = 0;
convert_dpcm_impl(wf,
[&](int s) { data_write(ofs, block_pos, s, false); },
[&](uint8_t outData) { data_write(ofs, block_pos, outData, true); });
}
static void convert_dpcm_bin(wav_file& wf, std::vector<uint8_t>& data)
{
convert_dpcm_impl(wf,
[&](int s) { bin_write_u8(data, static_cast<uint8_t>(s)); },
[&](uint8_t outData) { bin_write_u8(data, outData); });
}
void enable_dpcm_verbose()
{
dpcm_verbose = true;
}
void enable_dpcm_lookahead_fast()
{
dpcm_lookahead_fast = true;
}
void disable_dpcm_padding()
{
dpcm_include_padding = false;
}
void set_dpcm_lookahead(size_t lookahead)
{
dpcm_enc_lookahead = clamp<size_t>(lookahead, 1, 8);
}
void set_wav_loop_start(uint32_t start)
{
wav_loop_start = start;
wav_loop_start_override = true;
}
void set_wav_loop_end(uint32_t end)
{
wav_loop_end = end;
wav_loop_end_override = true;
}
void set_wav_tune(double tune)
{
wav_tune = tune;
wav_tune_override = true;
}
void set_wav_key(uint8_t key)
{
wav_key = key;
wav_key_override = true;
}
void set_wav_rate(uint32_t rate)
{
wav_rate = rate;
wav_rate_override = true;
}
void convert(const std::string& wav_file_str, const std::string& out_file_str,
const std::string& sym, cmp_type ct, out_type ot)
{
wav_file wf(wav_file_str);
// check command line overrides
if (wav_loop_start_override) {
wf.loopStart = std::min(wav_loop_start, wf.loopEnd);
wf.loopEnabled = true;
}
if (wav_loop_end_override) {
wf.loopEnd = std::min(wav_loop_end, wf.loopEnd);
}
if (wav_tune_override) {
wf.tuning = wav_tune;
}
if (wav_key_override) {
wf.midiKey = wav_key;
}
if (wav_rate_override) {
wf.sampleRate = wav_rate;
}
uint8_t fmt;
if (ct == cmp_type::none)
fmt = 0;
else if (ct == cmp_type::dpcm)
fmt = 1;
else
throw std::runtime_error("convert: invalid compression type");
double pitch;
if (wf.midiKey == 60 && wf.tuning == 0.0) {
pitch = wf.sampleRate;
} else {
pitch = wf.sampleRate * pow(2.0, (60.0 - wf.midiKey) / 12.0 + wf.tuning / 1200.0);
}
uint32_t pitch_value;
if (wf.agbPitch != 0) {
pitch_value = wf.agbPitch;
} else {
pitch_value = static_cast<uint32_t>(pitch * 1024.0);
}
if (ot == out_type::binary) {
// Binary output mode
std::vector<uint8_t> bin_data;
// Write header (16 bytes)
// Bytes 0-3: flags (format in bit 0, loop in bit 30)
uint32_t flags = fmt;
if (wf.loopEnabled)
flags |= 0x40000000;
bin_write_u32_le(bin_data, flags);
// Bytes 4-7: pitch
bin_write_u32_le(bin_data, pitch_value);
// Bytes 8-11: loop start
bin_write_u32_le(bin_data, wf.loopStart);
// Bytes 12-15: loop end
// wf.loopEnd is the exclusive end position; binary format expects (end - 1)
bin_write_u32_le(bin_data, wf.loopEnd > 0 ? wf.loopEnd - 1 : 0);
// Write sample data
if (ct == cmp_type::none)
convert_uncompressed_bin(wf, bin_data);
else if (ct == cmp_type::dpcm)
convert_dpcm_bin(wf, bin_data);
else
throw std::runtime_error("convert: invalid compression type");
// Write binary file
std::ofstream fout(out_file_str, std::ios::out | std::ios::binary);
if (!fout.is_open()) {
perror("ofstream");
throw std::runtime_error("unable to open output file");
}
fout.write(reinterpret_cast<const char*>(bin_data.data()), bin_data.size());
fout.close();
} else {
// Assembly output mode
std::ofstream fout(out_file_str, std::ios::out);
if (!fout.is_open()) {
perror("ofstream");
throw std::runtime_error("unable to open output file");
}
agb_out(fout, " .section .rodata\n");
agb_out(fout, " .global %s\n", sym.c_str());
agb_out(fout, " .align 2\n\n%s:\n\n", sym.c_str());
agb_out(fout, " .byte 0x%X, 0x0, 0x0, 0x%X\n", fmt, wf.loopEnabled ? 0x40 : 0x0);
agb_out(fout, " .word 0x%08X @ Mid-C ~%f\n", pitch_value, pitch);
agb_out(fout, " .word %u, %u\n", wf.loopStart, wf.loopEnd);
if (ct == cmp_type::none)
convert_uncompressed(wf, fout);
else if (ct == cmp_type::dpcm)
convert_dpcm(wf, fout);
else
throw std::runtime_error("convert: invalid compression type");
agb_out(fout, "\n\n .end\n");
fout.close();
}
}
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <string>
#include <algorithm>
#include <cstdint>
enum class cmp_type {
none, dpcm
};
enum class out_type {
assembly, binary
};
void enable_dpcm_verbose();
void enable_dpcm_lookahead_fast();
void disable_dpcm_padding();
void set_dpcm_lookahead(size_t lookahead);
void set_wav_loop_start(uint32_t start);
void set_wav_loop_end(uint32_t end);
void set_wav_tune(double tune);
void set_wav_key(uint8_t key);
void set_wav_rate(uint32_t rate);
void convert(const std::string&, const std::string&,
const std::string& sym, cmp_type ct, out_type ot);
+9
View File
@@ -0,0 +1,9 @@
#!/bin/sh
for l in $(seq 1 8)
do
echo lookahead="$l":
wav2agb "$1" -c -l "$l" --verbose
echo lookahead="$l" fast:
wav2agb "$1" -f -l "$l" --verbose
done
+213
View File
@@ -0,0 +1,213 @@
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cstdarg>
#include <cassert>
#include <string>
#include "converter.h"
static void usage() {
fprintf(stderr, "wav2agb\n");
fprintf(stderr, "\n");
fprintf(stderr, "Usage: wav2agb [options] <input.wav> [<output>]\n");
fprintf(stderr, "\n");
fprintf(stderr, "Options:\n");
fprintf(stderr, "-s, --symbol <sym> | symbol name for wave header (default: file name)\n");
fprintf(stderr, "-l, --lookahead <amount> | DPCM compression lookahead 1..8 (default: 3)\n");
fprintf(stderr, "-c, --compress | compress output with DPCM\n");
fprintf(stderr, "-f, --fast-compress | compress output with DPCM fast\n");
fprintf(stderr, "--no-pad | omit trailing padding in compressed output\n");
fprintf(stderr, "-b, --binary | output raw binary instead of assembly\n");
fprintf(stderr, "--loop-start <pos> | override loop start (integer)\n");
fprintf(stderr, "--loop-end <pos> | override loop end (integer)\n");
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");
exit(1);
}
static void version() {
printf("wav2agb v1.1 (c) 2019 ipatix\n");
exit(0);
}
static void die(const char *msg, ...) {
va_list args;
va_start(args, msg);
vfprintf(stderr, msg, args);
va_end(args);
exit(1);
}
static void fix_str(std::string& str) {
// replaces all characters that are not alphanumerical
for (size_t i = 0; i < str.size(); i++) {
if (str[i] >= 'a' && str[i] <= 'z')
continue;
if (str[i] >= 'A' && str[i] <= 'Z')
continue;
if (str[i] >= '0' && str[i] <= '9' && i > 0)
continue;
str[i] = '_';
}
}
static char path_seperators[] = {
'/',
#ifdef _WIN32
'\\',
#endif
'\0'
};
static std::string filename_without_ext(const std::string& str) {
size_t last_path_seperator = 0;
char *sep = path_seperators;
while (*sep) {
size_t pos = str.find_last_of(*sep);
if (pos != std::string::npos)
last_path_seperator = std::max(pos, last_path_seperator);
sep += 1;
}
size_t file_ext_dot_pos = str.find_last_of('.');
if (file_ext_dot_pos == std::string::npos)
return std::string(str);
assert(file_ext_dot_pos != last_path_seperator);
if (file_ext_dot_pos > last_path_seperator)
return str.substr(0, file_ext_dot_pos);
return std::string(str);
}
static std::string filename_without_dir(const std::string& str) {
size_t last_path_seperator = 0;
bool path_seperator_found = false;
char *sep = path_seperators;
while (*sep) {
size_t pos = str.find_last_of(*sep);
if (pos != std::string::npos) {
last_path_seperator = std::max(pos, last_path_seperator);
path_seperator_found = true;
}
sep += 1;
}
if (str.size() > 0 && path_seperator_found) {
return str.substr(last_path_seperator + 1);
} else {
return std::string(str);
}
}
static cmp_type arg_compress = cmp_type::none;
static out_type arg_output_type = out_type::assembly;
static std::string arg_sym;
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;
int main(int argc, char *argv[]) {
try {
if (argc == 1)
usage();
for (int i = 1; i < argc; i++) {
std::string st(argv[i]);
if (st == "-s" || st == "--symbol") {
if (++i >= argc)
die("-s: missing symbol name\n");
arg_sym = argv[i];
fix_str(arg_sym);
} else if (st == "-c" || st == "--compress") {
arg_compress = cmp_type::dpcm;
} else if (st == "-f" || st == "--compress-fast") {
arg_compress = cmp_type::dpcm;
enable_dpcm_lookahead_fast();
} else if (st == "--no-pad") {
disable_dpcm_padding();
} else if (st == "-b" || st == "--binary") {
arg_output_type = out_type::binary;
} else if (st == "--verbose") {
enable_dpcm_verbose();
} else if (st == "-l" || st == "--lookahead") {
if (++i >= argc)
die("-l: missing parameter");
set_dpcm_lookahead(std::stoul(argv[i], nullptr, 10));
} else if (st == "--version") {
version();
} else if (st == "--loop-start") {
if (++i >= argc)
die("--loop-start: missing parameter");
uint32_t start = static_cast<uint32_t>(std::stoul(argv[i], nullptr, 10));
set_wav_loop_start(start);
} else if (st == "--loop-end") {
if (++i >= argc)
die("--loop-end: missing parameter");
uint32_t end = static_cast<uint32_t>(std::stoul(argv[i], nullptr, 10));
set_wav_loop_end(end);
} else if (st == "--tune") {
if (++i >= argc)
die("--tune: missing parameter");
double tune = std::stod(argv[i], nullptr);
set_wav_tune(tune);
} else if (st == "--key") {
if (++i >= argc)
die("--key: missing parameter");
int key = std::stoi(argv[i], nullptr, 10);
if (key < 0) key = 0;
if (key > 127) key = 127;
set_wav_key(static_cast<uint8_t>(key));
} else if (st == "--rate") {
if (++i >= argc)
die("--rate: missing parameter");
uint32_t rate = static_cast<uint32_t>(std::stoul(argv[i], nullptr, 10));
set_wav_rate(rate);
} else {
if (st == "--") {
if (++i >= argc)
die("--: missing file name\n");
}
if (!arg_input_file_read) {
arg_input_file = argv[i];
if (arg_input_file.size() < 1)
die("empty input file name\n");
arg_input_file_read = true;
} else if (!arg_output_file_read) {
arg_output_file = argv[i];
if (arg_output_file.size() < 1)
die("empty output file name\n");
arg_output_file_read = true;
} else {
die("Too many files specified\n");
}
}
}
// check arguments
if (!arg_input_file_read) {
die("No input file specified\n");
}
if (!arg_output_file_read) {
// create output file name if none is provided
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";
}
arg_output_file_read = true;
}
if (arg_sym.size() == 0) {
arg_sym = filename_without_dir(filename_without_ext(arg_output_file));
fix_str(arg_sym);
}
convert(arg_input_file, arg_output_file, arg_sym, arg_compress, arg_output_type);
return 0;
} catch (const std::exception& e) {
fprintf(stderr, "std lib error:\n%s\n", e.what());
}
return 1;
}
+287
View File
@@ -0,0 +1,287 @@
#include "wav_file.h"
#include <stdexcept>
#include <cerrno>
#include <cstring>
#include <algorithm>
static uint32_t read_u32(std::ifstream& ifs)
{
uint8_t lenBytes[4];
ifs.read(reinterpret_cast<char *>(lenBytes), sizeof(lenBytes));
uint32_t retval = lenBytes[0] | (lenBytes[1] << 8) | (lenBytes[2] << 16) | (lenBytes[3] << 24);
return retval;
}
//static uint16_t read_u16(std::ifstream& ifs)
//{
// uint8_t lenBytes[2];
// ifs.read(reinterpret_cast<char *>(lenBytes), sizeof(lenBytes));
// uint16_t retval = uint16_t(lenBytes[0] | (lenBytes[1] << 8));
// return retval;
//}
static std::string read_str(std::ifstream& ifs, size_t len)
{
std::vector<char> buf(len);
ifs.read(buf.data(), buf.size());
return std::string(buf.data(), buf.size());
}
static std::vector<uint8_t> read_arr(std::ifstream& ifs, size_t len)
{
std::vector<uint8_t> buf(len);
ifs.read(reinterpret_cast<char *>(buf.data()), buf.size());
return buf;
}
static uint16_t arr_u16(const std::vector<uint8_t>& arr, size_t pos)
{
uint16_t val = uint16_t(arr.at(pos) | (arr.at(pos + 1) << 8));
return val;
}
static uint32_t arr_u32(const std::vector<uint8_t>& arr, size_t pos)
{
uint32_t val = uint32_t(arr.at(pos) | (arr.at(pos + 1) << 8) |
(arr.at(pos + 2) << 16) | (arr.at(pos + 3) << 24));
return val;
}
static const size_t loadChunkSize = 2048;
uint32_t wav_file::fmt_size() const
{
if (fmt == format_type::u8)
return 1;
else if (fmt == format_type::s16)
return 2;
else if (fmt == format_type::s24)
return 3;
else if (fmt == format_type::s32)
return 4;
else if (fmt == format_type::f32)
return 4;
else if (fmt == format_type::f64)
return 8;
else
throw std::runtime_error("INTERNAL ERROR: invalid format type");
}
wav_file::wav_file(const std::string& path) : loadBuffer(loadChunkSize)
{
ifs.exceptions(std::ios::badbit | std::ios::eofbit);
ifs.open(path, std::ios::binary);
if (!ifs.good())
throw std::runtime_error("failed to open file: " + path + ", reason: " + strerror(errno));
if (!ifs.is_open())
throw std::runtime_error("failed to open file: " + path + ", reason: " + strerror(errno));
ifs.seekg(0, ifs.end);
std::streampos len = ifs.tellg();
ifs.seekg(0, ifs.beg);
std::string chunkId = read_str(ifs, 4);
if (chunkId != "RIFF")
throw std::runtime_error("RIFF ID invalid");
uint32_t mainChunkLen = read_u32(ifs);
if (mainChunkLen + 8 != len)
throw std::runtime_error("RIFF chunk len (=" +
std::to_string(mainChunkLen) +
") doesn't match file len (=" +
std::to_string(len) +
")");
std::string riffType = read_str(ifs, 4);
if (riffType != "WAVE")
throw std::runtime_error("WAVE ID invalid");
bool dataChunkFound = false;
bool fmtChunkFound = false;
// search all chunks
std::streampos curPos;
while ((curPos = ifs.tellg()) + std::streampos(8) <= len) {
chunkId = read_str(ifs, 4);
uint32_t chunkLen = read_u32(ifs);
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));
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);
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);
if (fmtTag == 1) {
// integer
if (block_align == 1 && bits_per_sample == 8)
this->fmt = format_type::u8;
else if (block_align == 2 && bits_per_sample == 16)
this->fmt = format_type::s16;
else if (block_align == 3 && bits_per_sample == 24)
this->fmt = format_type::s24;
else if (block_align == 4 && bits_per_sample == 32)
this->fmt = format_type::s32;
else
throw std::runtime_error("ERROR: unsupported integer format combination");
} else if (fmtTag == 3) {
// float
if (block_align == 4 && bits_per_sample == 32)
this->fmt = format_type::f32;
else if (block_align == 8 && bits_per_sample == 64)
this->fmt = format_type::f64;
else
throw std::runtime_error("ERROR: unsupported float format combination");
} else {
throw std::runtime_error("ERROR: unsupported format code: " + std::to_string(fmtTag));
}
} else if (chunkId == "data") {
dataChunkFound = true;
dataChunkPos = ifs.tellg();
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);
this->midiKey = static_cast<uint8_t>(std::min(midiUnityNote, 127u));
uint32_t midiPitchFraction = arr_u32(smplChunk, 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);
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);
if (loopType != 0)
throw std::runtime_error("ERROR: loop type not supported: " + std::to_string(loopType));
this->loopStart = arr_u32(smplChunk, 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->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);
}
} 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
* If chunk size is odd, skip the pad byte */
if ((chunkLen % 2) == 1)
ifs.seekg(1, ifs.cur);
}
if (!fmtChunkFound)
throw std::runtime_error("ERROR: fmt chunk not found");
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);
}
wav_file::~wav_file()
{
ifs.close();
}
void wav_file::readData(size_t location, double *data, size_t len)
{
while (len-- > 0) {
if (loadedChunk != location - (location % loadChunkSize)) {
loadedChunk = location - (location % loadChunkSize);
std::streampos blockpos = this->dataChunkPos + std::streampos(loadedChunk * fmt_size());
std::streampos endblockpos = this->dataChunkEndPos;
size_t actualChunkSize = std::min(loadChunkSize, static_cast<size_t>(endblockpos - blockpos) / fmt_size());
if (actualChunkSize == 0) {
std::fill(loadBuffer.begin(), loadBuffer.end(), 0.0);
goto load_sample;
}
ifs.seekg(blockpos, ifs.beg);
std::vector<uint8_t> ld(actualChunkSize * fmt_size());
ifs.read(reinterpret_cast<char *>(ld.data()), ld.size());
if (fmt == format_type::u8) {
for (size_t i = 0; i < actualChunkSize; i++) {
loadBuffer[i] = (double(ld[i]) - 128.0) / 128.0;
}
} else if (fmt == format_type::s16) {
for (size_t i = 0; i < actualChunkSize; i++) {
int32_t s =
(ld[i * fmt_size() + 0] << 0) |
(ld[i * fmt_size() + 1] << 8);
s <<= 16;
s >>= 16;
loadBuffer[i] = double(s) / 32768.0;
}
} else if (fmt == format_type::s24) {
for (size_t i = 0; i < actualChunkSize; i++) {
int32_t s =
(ld[i * fmt_size() + 0] << 0) |
(ld[i * fmt_size() + 1] << 8) |
(ld[i * fmt_size() + 2] << 16);
s <<= 8;
s >>= 8;
loadBuffer[i] = double(s) / 8388608.0;
}
} else if (fmt == format_type::s32) {
for (size_t i = 0; i < actualChunkSize; i++) {
int32_t s =
(ld[i * fmt_size() + 0] << 0) |
(ld[i * fmt_size() + 1] << 8) |
(ld[i * fmt_size() + 2] << 16) |
(ld[i * fmt_size() + 3] << 24);
loadBuffer[i] = double(s) / 2147483648.0;
}
} else if (fmt == format_type::f32) {
for (size_t i = 0; i < actualChunkSize; i++) {
union {
uint32_t s;
float f;
} u;
u.s =
(ld[i * fmt_size() + 0] << 0) |
(ld[i * fmt_size() + 1] << 8) |
(ld[i * fmt_size() + 2] << 16) |
(ld[i * fmt_size() + 3] << 24);
loadBuffer[i] = u.f;
}
} else if (fmt == format_type::f64) {
for (size_t i = 0; i < actualChunkSize; i++) {
union {
uint64_t s;
double d;
} u;
u.s =
(uint64_t(ld[i * fmt_size() + 0]) << 0) |
(uint64_t(ld[i * fmt_size() + 1]) << 8) |
(uint64_t(ld[i * fmt_size() + 2]) << 16) |
(uint64_t(ld[i * fmt_size() + 3]) << 24) |
(uint64_t(ld[i * fmt_size() + 4]) << 32) |
(uint64_t(ld[i * fmt_size() + 5]) << 40) |
(uint64_t(ld[i * fmt_size() + 6]) << 48) |
(uint64_t(ld[i * fmt_size() + 7]) << 56);
loadBuffer[i] = u.d;
}
}
for (size_t i = actualChunkSize; i < loadChunkSize; i++) {
loadBuffer[i] = 0.0;
}
}
load_sample:
*data++ = loadBuffer[location % loadChunkSize];
location++;
}
}
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include <string>
#include <fstream>
#include <vector>
#include <limits>
#include <cstdint>
#define WAV_INVALID_VAL 0xFFFFFFFFu
class wav_file {
public:
wav_file(const std::string& path);
~wav_file();
void readData(size_t location, double *data, size_t len);
private:
std::ifstream ifs;
std::streampos dataChunkPos;
std::streampos dataChunkEndPos;
size_t loadedChunk = WAV_INVALID_VAL;
std::vector<double> loadBuffer;
size_t loadBufferBlock = std::numeric_limits<size_t>::max();
enum class format_type {
u8, s16, s24, s32,
f32, f64,
} fmt;
uint32_t fmt_size() const;
public:
uint32_t loopStart = 0; // samples
uint32_t loopEnd = std::numeric_limits<uint32_t>::max(); // samples
bool loopEnabled = false;
double tuning = 0.0; // cents
uint8_t midiKey = 60;
uint32_t sampleRate;
uint32_t agbPitch = 0; // optional: exact GBA pitch value from 'agbp' chunk (0 = not present)
};