mirror of
https://github.com/pret/pokeemerald.git
synced 2026-03-21 17:54:57 -05:00
467 lines
16 KiB
C++
467 lines
16 KiB
C++
#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);
|
|
}
|
|
|
|
uint32_t loop_end = wf.loopEnd;
|
|
if (wf.agbLoopEnd != 0) {
|
|
loop_end = wf.agbLoopEnd;
|
|
}
|
|
|
|
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
|
|
bin_write_u32_le(bin_data, loop_end);
|
|
|
|
// 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, loop_end);
|
|
|
|
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();
|
|
}
|
|
}
|