summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeod <47716344+JeodC@users.noreply.github.com>2026-08-23 20:57:04 -0400
committerGitHub <noreply@github.com>2026-08-23 18:57:04 -0600
commita93519e66eabb62005d278cce46b4d593bceed83 (patch)
tree34f25bc20de8d330c6ed5a4b8ef72618b0f7789a
parente9faa26c74a839d3c7995dfba7cad0fe6cd4af29 (diff)
Fully parse bk64 soundfont for modding (#256)
* Fully parse bk64 soundfont for modding * Match N64MidiTool loop name convention
-rw-r--r--CMakeLists.txt17
-rw-r--r--run-clang-format.ps16
-rw-r--r--src/Companion.cpp11
-rw-r--r--src/Companion.h4
-rw-r--r--src/factories/ResourceType.h3
-rw-r--r--src/factories/bk64/BKAssetFactory.cpp2
-rw-r--r--src/factories/bk64/MusicFactory.cpp910
-rw-r--r--src/factories/bk64/MusicFactory.h68
-rw-r--r--src/factories/bk64/SoundfontFactory.cpp1367
-rw-r--r--src/factories/bk64/SoundfontFactory.h145
-rw-r--r--src/factories/bk64/SoundfontTblFactory.cpp210
-rw-r--r--src/factories/bk64/SoundfontTblFactory.h27
-rw-r--r--src/factories/bk64/VadpcmEncode.cpp303
-rw-r--r--src/factories/bk64/VadpcmEncode.h21
14 files changed, 2842 insertions, 252 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8c8f227..877c77f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -80,6 +80,16 @@ if(USE_STANDALONE)
endif()
endif()
+# Audio decoding for the soundfont importer; header-only
+FetchContent_Declare(
+ dr_libs
+ GIT_REPOSITORY https://github.com/mackron/dr_libs.git
+ GIT_TAG da35f9d6c7374a95353fd1df1d394d44ab66cf01
+ GIT_SUBMODULES ""
+)
+FetchContent_MakeAvailable(dr_libs)
+include_directories(${dr_libs_SOURCE_DIR})
+
# Interactive viewer renderer: libultraship + Fast3D (true N64 rendering). LUS
# bundles its own ImGui, so it fully replaces the old raylib/rlImGui backend.
if(BUILD_UI)
@@ -100,13 +110,6 @@ if(BUILD_UI)
)
FetchContent_MakeAvailable(tinyxml2)
- FetchContent_Declare(
- dr_libs
- GIT_REPOSITORY https://github.com/mackron/dr_libs.git
- GIT_TAG da35f9d6c7374a95353fd1df1d394d44ab66cf01
- )
- FetchContent_MakeAvailable(dr_libs)
-
# Kenix3 upstream for now. NOTE: the HM64 ports build against a fork
# (KiritoDv/better-mipmaps @ 5959b97) with fixes not on upstream; re-pin here
# if upstream proves incompatible.
diff --git a/run-clang-format.ps1 b/run-clang-format.ps1
index af9ba4a..ce8aa20 100644
--- a/run-clang-format.ps1
+++ b/run-clang-format.ps1
@@ -25,7 +25,7 @@ if (-not (Test-Path $clangFormatFilePath) -or ($currentVersion -ne $requiredVers
}
$wc = New-Object net.webclient
- $wc.Downloadfile($url, $PSScriptRoot + $llvmInstallerPath)
+ $wc.Downloadfile($url, (Join-Path $PSScriptRoot "LLVM-14.0.6-win64.exe"))
$sevenZipPath = "C:\Program Files\7-Zip\7z.exe"
$specificFileInArchive = "bin\clang-format.exe"
@@ -36,9 +36,7 @@ if (-not (Test-Path $clangFormatFilePath) -or ($currentVersion -ne $requiredVers
$basePath = Join-Path (Get-Location).Path "src"
$files = Get-ChildItem -Path $basePath -Recurse -File `
- | Where-Object { ($_.Extension -eq '.c' -or $_.Extension -eq '.cpp' -or `
- (($_.Extension -eq '.h' -or $_.Extension -eq '.hpp') -and `
- (-not ($_.FullName -like "*\src\*" -or $_.FullName -like "*\include\*")))) -and `
+ | Where-Object { ($_.Extension -in '.c', '.cpp', '.h', '.hpp') -and `
(-not ($_.FullName -like "*\assets\*" -or $_.FullName -like "*\build\*")) }
for ($i = 0; $i -lt $files.Length; $i++) {
diff --git a/src/Companion.cpp b/src/Companion.cpp
index 04ae28f..f30bc3c 100644
--- a/src/Companion.cpp
+++ b/src/Companion.cpp
@@ -112,7 +112,8 @@
#include "factories/bk64/SpriteFactory.h"
#include "factories/bk64/ModelFactory.h"
#include "factories/bk64/MapFactory.h"
-#include "factories/bk64/SoundfontTblFactory.h"
+#include "factories/bk64/MusicFactory.h"
+#include "factories/bk64/SoundfontFactory.h"
#endif
#ifdef MARIO_ARTIST_SUPPORT
@@ -285,8 +286,8 @@ void Companion::Init(const ExportType type, std::atomic<size_t>& assetCount, boo
this->RegisterFactory("BK64:MAP", std::make_shared<BK64::MapFactory>());
this->RegisterFactory("BK64:QUIZQ", std::make_shared<BK64::QuizQuestionFactory>());
this->RegisterFactory("BK64:MODEL", std::make_shared<BK64::ModelFactory>());
- this->RegisterFactory("BK64:SOUNDFONT_CTL", std::make_shared<BK64::SoundfontCtlFactory>());
- this->RegisterFactory("BK64:SOUNDFONT_TBL", std::make_shared<BK64::SoundfontTblFactory>());
+ this->RegisterFactory("BK64:MUSIC", std::make_shared<BK64::MusicFactory>());
+ this->RegisterFactory("BK64:SOUNDFONT", std::make_shared<BK64::SoundfontFactory>());
this->RegisterFactory("BK64:SPRITE", std::make_shared<BK64::SpriteFactory>());
#endif
@@ -483,7 +484,9 @@ std::optional<ParseResultData> Companion::ParseNode(YAML::Node& node, std::strin
std::vector<uint8_t> data = std::vector<uint8_t>(std::istreambuf_iterator(input), {});
input.close();
+ this->gCurrentModdingSource = path.filename().string();
result = impl->parse_modding(data, node);
+ this->gCurrentModdingSource.clear();
executeDef = !result.has_value();
}
}
@@ -529,6 +532,8 @@ void Companion::ParseModdingConfig() {
}
void Companion::ParseCurrentFileConfig(YAML::Node node, std::atomic<size_t>& assetCount) {
+ this->gCurrentFileConfig = node;
+
if (node["external_files"]) {
auto externalFiles = node["external_files"];
if (externalFiles.IsSequence() && externalFiles.size()) {
diff --git a/src/Companion.h b/src/Companion.h
index a435482..80366ba 100644
--- a/src/Companion.h
+++ b/src/Companion.h
@@ -243,6 +243,8 @@ public:
void SetProcess(bool shouldProcess);
TorchConfig& GetConfig() { return this->gConfig; }
+ const YAML::Node& GetCurrentFileConfig() const { return this->gCurrentFileConfig; }
+ const std::string& GetCurrentModdingSource() const { return this->gCurrentModdingSource; }
BinaryWrapper* GetCurrentWrapper() { return this->gCurrentWrapper; }
const std::unordered_map<std::string, std::string>& GetModdedAssetPaths() const { return this->gModdedAssetPaths; }
@@ -264,6 +266,8 @@ public:
private:
TorchConfig gConfig;
YAML::Node gModdingConfig;
+ YAML::Node gCurrentFileConfig;
+ std::string gCurrentModdingSource;
fs::path gSourceDirectory;
fs::path gDestinationDirectory;
fs::path gCurrentDirectory;
diff --git a/src/factories/ResourceType.h b/src/factories/ResourceType.h
index 17d5ae9..926335e 100644
--- a/src/factories/ResourceType.h
+++ b/src/factories/ResourceType.h
@@ -91,6 +91,9 @@ enum class ResourceType {
BKMap = 0x424B4D50, // BKMP
BKGruntyQuestion = 0x424B4751, // BKGQ
BKQuizQuestion = 0x424B5151, // BKQQ
+ BKSound = 0x424B534E, // BKSN
+ BKSoundBank = 0x424B5342, // BKSB
+ BKMusic = 0x424B4D55, // BKMU
// NAudio v0
Bank = 0x42414E4B, // BANK
diff --git a/src/factories/bk64/BKAssetFactory.cpp b/src/factories/bk64/BKAssetFactory.cpp
index 2830d5f..db366c2 100644
--- a/src/factories/bk64/BKAssetFactory.cpp
+++ b/src/factories/bk64/BKAssetFactory.cpp
@@ -576,7 +576,7 @@ std::optional<std::shared_ptr<IParsedData>> BKAssetFactory::parse(std::vector<ui
Companion::Instance->AddSubFileAsset(bkAssetNode, assetSymbol, compressionType, assetSize);
break;
case BKAssetType::Midi:
- bkAssetNode["type"] = "BLOB";
+ bkAssetNode["type"] = "BK64:MUSIC";
Companion::Instance->AddSubFileAsset(bkAssetNode, assetSymbol, compressionType, assetSize);
break;
case BKAssetType::Model:
diff --git a/src/factories/bk64/MusicFactory.cpp b/src/factories/bk64/MusicFactory.cpp
new file mode 100644
index 0000000..04c291d
--- /dev/null
+++ b/src/factories/bk64/MusicFactory.cpp
@@ -0,0 +1,910 @@
+#include "MusicFactory.h"
+
+#include "Companion.h"
+#include "spdlog/spdlog.h"
+#include "utils/Decompressor.h"
+
+#include <algorithm>
+#include <cctype>
+#include <cstring>
+#include <map>
+
+namespace BK64 {
+namespace {
+
+constexpr uint8_t kMetaPrefix = 0xFF;
+constexpr uint8_t kEndOfTrack = 0x2F;
+constexpr uint8_t kSetTempo = 0x51;
+constexpr uint8_t kLoopStart = 0x2E;
+constexpr uint8_t kLoopEnd = 0x2D;
+constexpr uint8_t kBlockCode = 0xFE;
+constexpr uint32_t kLoopEndBytes = 6;
+
+class TrackReader {
+ public:
+ TrackReader(const uint8_t* data, size_t size, uint32_t start, uint32_t end)
+ : mData(data), mSize(size), mLoc(start), mEnd(end) {
+ }
+ bool Failed() const {
+ return mFailed;
+ }
+ bool AtEnd() const {
+ return mBackupLen == 0 && mLoc >= mEnd;
+ }
+ uint32_t Loc() const {
+ return mLoc;
+ }
+ bool InBlock() const {
+ return mBackupLen != 0;
+ }
+
+ uint8_t Byte() {
+ if (mBackupLen != 0) {
+ const uint8_t byte = At(mBackup);
+ mBackup++;
+ mBackupLen--;
+ return byte;
+ }
+ uint8_t byte = At(mLoc);
+ mLoc++;
+ if (byte != kBlockCode) {
+ return byte;
+ }
+ const uint8_t next = At(mLoc);
+ mLoc++;
+ if (next == kBlockCode) {
+ return kBlockCode;
+ }
+ const uint32_t high = next;
+ const uint32_t low = At(mLoc);
+ mLoc++;
+ const uint32_t len = At(mLoc);
+ mLoc++;
+ const uint32_t back = (high << 8) | low;
+ if (len == 0 || back + 4 > mLoc) {
+ mFailed = true;
+ return 0;
+ }
+ mBackup = mLoc - (back + 4);
+ mBackupLen = len;
+ byte = At(mBackup);
+ mBackup++;
+ mBackupLen--;
+ return byte;
+ }
+
+ uint32_t VarLen() {
+ uint32_t value = Byte();
+ if (value & 0x80) {
+ value &= 0x7F;
+ uint8_t next;
+ do {
+ next = Byte();
+ value = (value << 7) + (next & 0x7F);
+ } while ((next & 0x80) && !mFailed);
+ }
+ return value;
+ }
+
+ const uint8_t* TakeRaw(uint32_t count) {
+ if (mBackupLen != 0 || mLoc + count > mSize) {
+ mFailed = true;
+ return nullptr;
+ }
+ const uint8_t* at = mData + mLoc;
+ mLoc += count;
+ return at;
+ }
+
+ private:
+ uint8_t At(uint32_t at) {
+ if (at >= mSize) {
+ mFailed = true;
+ return 0;
+ }
+ return mData[at];
+ }
+
+ const uint8_t* mData;
+ size_t mSize;
+ uint32_t mLoc;
+ uint32_t mEnd;
+ uint32_t mBackup = 0;
+ uint32_t mBackupLen = 0;
+ bool mFailed = false;
+};
+
+bool DecodeTrack(const uint8_t* data, size_t size, uint32_t start, uint32_t end, std::vector<MusicEvent>& out) {
+ TrackReader reader(data, size, start, end);
+ uint8_t running = 0;
+
+ while (!reader.AtEnd() && !reader.Failed()) {
+ MusicEvent event;
+ event.delta = reader.VarLen();
+ const uint8_t status = reader.Byte();
+
+ if (status == kMetaPrefix) {
+ const uint8_t type = reader.Byte();
+ if (type == kSetTempo) {
+ const uint32_t high = reader.Byte(), middle = reader.Byte(), low = reader.Byte();
+ event.kind = static_cast<uint8_t>(MusicEventKind::Tempo);
+ event.aux = (high << 16) | (middle << 8) | low;
+ running = 0;
+ } else if (type == kEndOfTrack) {
+ event.kind = static_cast<uint8_t>(MusicEventKind::End);
+ out.push_back(event);
+ return !reader.Failed();
+ } else if (type == kLoopStart) {
+ event.byte1 = reader.Byte();
+ event.byte2 = reader.Byte();
+ event.kind = static_cast<uint8_t>(MusicEventKind::LoopStart);
+ running = 0;
+ } else if (type == kLoopEnd) {
+ const uint8_t* raw = reader.TakeRaw(kLoopEndBytes);
+ if (raw == nullptr) {
+ return false;
+ }
+ event.byte1 = raw[0];
+ event.byte2 = raw[1];
+ event.aux = (static_cast<uint32_t>(raw[2]) << 24) | (static_cast<uint32_t>(raw[3]) << 16) |
+ (static_cast<uint32_t>(raw[4]) << 8) | raw[5];
+ event.kind = static_cast<uint8_t>(MusicEventKind::LoopEnd);
+ running = 0;
+ } else {
+ return false;
+ }
+ } else {
+ event.kind = static_cast<uint8_t>(MusicEventKind::Midi);
+ if (status & 0x80) {
+ event.status = status;
+ event.byte1 = reader.Byte();
+ running = status;
+ } else {
+ if (running == 0) {
+ return false;
+ }
+ event.status = running;
+ event.byte1 = status;
+ }
+ const uint8_t kind = event.status & 0xF0;
+ if (kind != 0xC0 && kind != 0xD0) {
+ event.byte2 = reader.Byte();
+ if (kind == 0x90) {
+ event.aux = reader.VarLen();
+ }
+ }
+ }
+ out.push_back(event);
+ }
+ return !reader.Failed();
+}
+
+std::optional<uint32_t> LoopPointFromName(const std::string& name) {
+ const size_t at = name.rfind("LP ");
+ if (at == std::string::npos) {
+ return std::nullopt;
+ }
+ uint32_t tick = 0;
+ size_t digits = 0;
+ for (size_t i = at + 3; i < name.size() && std::isdigit(static_cast<unsigned char>(name[i])); i++, digits++) {
+ tick = tick * 10 + static_cast<uint32_t>(name[i] - '0');
+ }
+ return digits != 0 ? std::optional<uint32_t>(tick) : std::nullopt;
+}
+
+std::optional<uint32_t> TrackIdFromSymbol(const std::string& symbol) {
+ const size_t at = symbol.rfind("COMUSIC_");
+ if (at == std::string::npos) {
+ return std::nullopt;
+ }
+ size_t pos = at + 8;
+ uint32_t id = 0;
+ size_t digits = 0;
+ for (; pos < symbol.size() && std::isxdigit(static_cast<unsigned char>(symbol[pos])); pos++, digits++) {
+ const char digit = symbol[pos];
+ const uint32_t value =
+ (digit <= '9') ? static_cast<uint32_t>(digit - '0') : static_cast<uint32_t>(std::tolower(digit) - 'a' + 10);
+ id = id * 16 + value;
+ }
+ if (digits == 0) {
+ return std::nullopt;
+ }
+ return id;
+}
+
+uint32_t VanillaVolume(const YAML::Node& node) {
+ const YAML::Node& config = Companion::Instance->GetCurrentFileConfig();
+ if (!node["symbol"] || !config["music_volumes"]) {
+ return kDefaultMusicVolume;
+ }
+ const auto id = TrackIdFromSymbol(node["symbol"].as<std::string>());
+ if (!id.has_value()) {
+ return kDefaultMusicVolume;
+ }
+ const YAML::Node& entry = config["music_volumes"][static_cast<int>(*id)];
+ return entry ? entry.as<uint32_t>() : kDefaultMusicVolume;
+}
+
+void PushVarLen(std::vector<uint8_t>& out, uint32_t value) {
+ uint8_t buffer[5];
+ int count = 0;
+ buffer[count++] = static_cast<uint8_t>(value & 0x7F);
+ while ((value >>= 7) != 0) {
+ buffer[count++] = static_cast<uint8_t>((value & 0x7F) | 0x80);
+ }
+ while (count > 0) {
+ out.push_back(buffer[--count]);
+ }
+}
+
+void PushBE(std::vector<uint8_t>& out, uint32_t value, int bytes) {
+ for (int i = bytes - 1; i >= 0; i--) {
+ out.push_back(static_cast<uint8_t>(value >> (i * 8)));
+ }
+}
+
+void PushTag(std::vector<uint8_t>& out, const char* tag) {
+ out.insert(out.end(), tag, tag + 4);
+}
+
+struct TimedEvent {
+ uint32_t tick = 0;
+ int order = 0;
+ std::vector<uint8_t> bytes;
+};
+
+std::vector<uint8_t> BuildSmfTrack(const MusicTrack& track, const std::string& lead = std::string()) {
+ std::vector<TimedEvent> timed;
+ uint32_t tick = 0;
+ int sequence = 0;
+
+ if (!lead.empty()) {
+ TimedEvent meta;
+ meta.tick = 0;
+ meta.order = -2;
+ meta.bytes = { 0xFF, 0x06, static_cast<uint8_t>(lead.size()) };
+ meta.bytes.insert(meta.bytes.end(), lead.begin(), lead.end());
+ timed.push_back(meta);
+ }
+
+ for (const MusicEvent& event : track.events) {
+ tick += event.delta;
+ const MusicEventKind kind = static_cast<MusicEventKind>(event.kind);
+
+ if (kind == MusicEventKind::Midi) {
+ TimedEvent noteOn;
+ noteOn.tick = tick;
+ noteOn.order = 1 + sequence++;
+ noteOn.bytes = { event.status, event.byte1 };
+ const uint8_t type = event.status & 0xF0;
+ if (type != 0xC0 && type != 0xD0) {
+ noteOn.bytes.push_back(event.byte2);
+ }
+ timed.push_back(noteOn);
+
+ if (type == 0x90) {
+ TimedEvent off;
+ off.tick = tick + event.aux;
+ off.order = -1;
+ off.bytes = { static_cast<uint8_t>(0x80 | (event.status & 0x0F)), event.byte1, 0 };
+ timed.push_back(off);
+ }
+ } else if (kind == MusicEventKind::Tempo) {
+ TimedEvent meta;
+ meta.tick = tick;
+ meta.order = 1 + sequence++;
+ meta.bytes = { 0xFF,
+ 0x51,
+ 0x03,
+ static_cast<uint8_t>(event.aux >> 16),
+ static_cast<uint8_t>(event.aux >> 8),
+ static_cast<uint8_t>(event.aux) };
+ timed.push_back(meta);
+ } else if (kind == MusicEventKind::LoopStart || kind == MusicEventKind::LoopEnd ||
+ kind == MusicEventKind::End) {
+ const char* text = kind == MusicEventKind::LoopStart ? "loopStart"
+ : kind == MusicEventKind::LoopEnd ? "loopEnd"
+ : "track_end";
+ TimedEvent meta;
+ meta.tick = tick;
+ meta.order = 1 + sequence++;
+ meta.bytes = { 0xFF, 0x06 };
+ const size_t len = std::strlen(text);
+ meta.bytes.push_back(static_cast<uint8_t>(len));
+ meta.bytes.insert(meta.bytes.end(), text, text + len);
+ timed.push_back(meta);
+ }
+ }
+
+ std::stable_sort(timed.begin(), timed.end(), [](const TimedEvent& left, const TimedEvent& right) {
+ return left.tick != right.tick ? left.tick < right.tick : left.order < right.order;
+ });
+
+ std::vector<uint8_t> body;
+ uint32_t last = 0;
+ for (const TimedEvent& event : timed) {
+ PushVarLen(body, event.tick - last);
+ last = event.tick;
+ body.insert(body.end(), event.bytes.begin(), event.bytes.end());
+ }
+ PushVarLen(body, 0);
+ body.push_back(0xFF);
+ body.push_back(0x2F);
+ body.push_back(0x00);
+
+ std::vector<uint8_t> chunk;
+ PushTag(chunk, "MTrk");
+ PushBE(chunk, static_cast<uint32_t>(body.size()), 4);
+ chunk.insert(chunk.end(), body.begin(), body.end());
+ return chunk;
+}
+
+} // namespace
+
+std::optional<std::shared_ptr<IParsedData>> MusicFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) {
+ auto [_, segment] = Decompressor::AutoDecode(node, buffer);
+ const uint8_t* data = segment.data;
+ const size_t size = segment.size;
+
+ if (size < 68) {
+ SPDLOG_ERROR("Music: only {} bytes, too small for a sequence header", size);
+ return std::nullopt;
+ }
+
+ auto music = std::make_shared<MusicData>(std::vector<uint8_t>(data, data + size));
+ music->mVolume = VanillaVolume(node);
+
+ uint32_t offsets[16];
+ for (int i = 0; i < 16; i++) {
+ offsets[i] = (static_cast<uint32_t>(data[i * 4]) << 24) | (static_cast<uint32_t>(data[i * 4 + 1]) << 16) |
+ (static_cast<uint32_t>(data[i * 4 + 2]) << 8) | data[i * 4 + 3];
+ }
+ music->mDivision = (static_cast<uint32_t>(data[64]) << 24) | (static_cast<uint32_t>(data[65]) << 16) |
+ (static_cast<uint32_t>(data[66]) << 8) | data[67];
+
+ for (int i = 0; i < 16; i++) {
+ MusicTrack track;
+ track.index = static_cast<uint32_t>(i);
+ track.present = offsets[i] != 0;
+ if (track.present) {
+ uint32_t end = static_cast<uint32_t>(size);
+ for (int j = i + 1; j < 16; j++) {
+ if (offsets[j] != 0) {
+ end = offsets[j];
+ break;
+ }
+ }
+ if (end < offsets[i] || end > size) {
+ SPDLOG_WARN("Music: track {} spans 0x{:X}..0x{:X} in a 0x{:X}-byte sequence", i, offsets[i], end, size);
+ track.present = false;
+ } else if (!DecodeTrack(data, size, offsets[i], end, track.events)) {
+ SPDLOG_ERROR("Music: track {} does not decode", i);
+ return std::nullopt;
+ }
+ }
+ music->mTracks.push_back(std::move(track));
+ }
+
+ return music;
+}
+
+ExportResult MusicBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName,
+ YAML::Node& node, std::string* replacement) {
+ auto writer = LUS::BinaryWriter();
+ auto music = std::static_pointer_cast<MusicData>(raw);
+
+ WriteHeader(writer, Torch::ResourceType::BKMusic, 0);
+ writer.Write(music->mVolume);
+ writer.Write(static_cast<uint32_t>(music->mBuffer.size()));
+ writer.Write(reinterpret_cast<char*>(music->mBuffer.data()), music->mBuffer.size());
+ writer.Finish(write);
+ return std::nullopt;
+}
+
+ExportResult MusicModdingExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName,
+ YAML::Node& node, std::string* replacement) {
+ auto music = std::static_pointer_cast<MusicData>(raw);
+ *replacement += ".mid";
+
+ std::vector<std::vector<uint8_t>> chunks;
+ for (const MusicTrack& track : music->mTracks) {
+ if (track.present) {
+ const std::string lead = chunks.empty() ? "volume " + std::to_string(music->mVolume) : std::string();
+ chunks.push_back(BuildSmfTrack(track, lead));
+ }
+ }
+
+ std::vector<uint8_t> out;
+ PushTag(out, "MThd");
+ PushBE(out, 6, 4);
+ PushBE(out, 1, 2);
+ PushBE(out, static_cast<uint32_t>(chunks.size()), 2);
+ PushBE(out, music->mDivision, 2);
+ for (const std::vector<uint8_t>& chunk : chunks) {
+ out.insert(out.end(), chunk.begin(), chunk.end());
+ }
+
+ write.write(reinterpret_cast<const char*>(out.data()), static_cast<std::streamsize>(out.size()));
+ return std::nullopt;
+}
+
+namespace {
+
+constexpr uint8_t kLoopForever = 0xFF;
+
+struct SmfEvent {
+ uint32_t tick = 0;
+ int order = 0;
+ int track = -1;
+ uint8_t kind = 0;
+ uint8_t status = 0;
+ uint8_t byte1 = 0;
+ uint8_t byte2 = 0;
+ uint32_t aux = 0;
+};
+
+class SmfReader {
+ public:
+ SmfReader(const uint8_t* data, size_t size) : mData(data), mSize(size) {
+ }
+
+ bool Ok() const {
+ return !mFailed;
+ }
+ size_t At() const {
+ return mAt;
+ }
+ void Seek(size_t at) {
+ mAt = at;
+ }
+
+ uint8_t U8() {
+ if (mAt >= mSize) {
+ mFailed = true;
+ return 0;
+ }
+ return mData[mAt++];
+ }
+ uint16_t U16() {
+ const uint16_t high = U8();
+ return static_cast<uint16_t>((high << 8) | U8());
+ }
+ uint32_t U32() {
+ const uint32_t high = U16();
+ return (high << 16) | U16();
+ }
+ uint32_t VarLen() {
+ uint32_t value = 0;
+ for (int i = 0; i < 4; i++) {
+ const uint8_t b = U8();
+ value = (value << 7) | (b & 0x7F);
+ if (!(b & 0x80)) {
+ break;
+ }
+ }
+ return value;
+ }
+ bool Tag(const char* tag) {
+ if (mAt + 4 > mSize) {
+ mFailed = true;
+ return false;
+ }
+ const bool match = std::memcmp(mData + mAt, tag, 4) == 0;
+ mAt += 4;
+ return match;
+ }
+
+ private:
+ const uint8_t* mData;
+ size_t mSize;
+ size_t mAt = 0;
+ bool mFailed = false;
+};
+
+bool TextIs(const std::vector<uint8_t>& text, const char* want) {
+ const size_t n = std::strlen(want);
+ if (text.size() < n) {
+ return false;
+ }
+ for (size_t i = 0; i < n; i++) {
+ if (std::tolower(text[i]) != want[i]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool ReadSmfTrack(SmfReader& reader, size_t end, std::vector<SmfEvent>& out, int& sequence, uint32_t endTicks[16],
+ std::optional<uint32_t>& volume) {
+ uint32_t tick = 0;
+ uint8_t running = 0;
+ int chunkChannel = -1;
+ bool touched[16] = { false };
+ bool haveEndMarker = false;
+ uint32_t endMarkerTick = 0;
+ std::vector<SmfEvent> pending;
+
+ while (reader.At() < end && reader.Ok()) {
+ tick += reader.VarLen();
+ uint8_t status = reader.U8();
+
+ if (status == 0xFF) {
+ const uint8_t meta = reader.U8();
+ const uint32_t length = reader.VarLen();
+ std::vector<uint8_t> text;
+ for (uint32_t i = 0; i < length; i++) {
+ text.push_back(reader.U8());
+ }
+ running = 0;
+ if (meta == 0x2F) {
+ const uint32_t stop = haveEndMarker ? endMarkerTick : tick;
+ bool any = false;
+ for (int c = 0; c < 16; c++) {
+ if (touched[c]) {
+ endTicks[c] = std::max(endTicks[c], stop);
+ any = true;
+ }
+ }
+ if (!any) {
+ endTicks[0] = std::max(endTicks[0], stop);
+ }
+ break;
+ }
+ SmfEvent event;
+ event.tick = tick;
+ event.order = sequence++;
+ if (meta == 0x51 && text.size() >= 3) {
+ event.kind = static_cast<uint8_t>(MusicEventKind::Tempo);
+ event.aux = (static_cast<uint32_t>(text[0]) << 16) | (static_cast<uint32_t>(text[1]) << 8) | text[2];
+ event.track = 0;
+ pending.push_back(event);
+ } else if ((meta == 0x06 || meta == 0x01) && TextIs(text, "loopstart")) {
+ event.kind = static_cast<uint8_t>(MusicEventKind::LoopStart);
+ pending.push_back(event);
+ } else if ((meta == 0x06 || meta == 0x01) && TextIs(text, "volume")) {
+ uint32_t value = 0;
+ bool any = false;
+ for (size_t i = 6; i < text.size(); i++) {
+ if (std::isdigit(text[i])) {
+ value = value * 10 + static_cast<uint32_t>(text[i] - '0');
+ any = true;
+ } else if (any) {
+ break;
+ }
+ }
+ if (any) {
+ volume = std::min<uint32_t>(value, kDefaultMusicVolume);
+ }
+ } else if ((meta == 0x06 || meta == 0x01) && TextIs(text, "track_end")) {
+ haveEndMarker = true;
+ endMarkerTick = tick;
+ } else if ((meta == 0x06 || meta == 0x01) && TextIs(text, "loopend")) {
+ event.kind = static_cast<uint8_t>(MusicEventKind::LoopEnd);
+ event.byte1 = kLoopForever;
+ event.byte2 = kLoopForever;
+ pending.push_back(event);
+ }
+ continue;
+ }
+
+ if (status == 0xF0 || status == 0xF7) {
+ const uint32_t length = reader.VarLen();
+ for (uint32_t i = 0; i < length; i++) {
+ reader.U8();
+ }
+ running = 0;
+ continue;
+ }
+
+ uint8_t first;
+ if (status & 0x80) {
+ running = status;
+ first = reader.U8();
+ } else {
+ if (running == 0) {
+ return false;
+ }
+ first = status;
+ status = running;
+ }
+
+ SmfEvent event;
+ event.tick = tick;
+ event.order = sequence++;
+ event.kind = static_cast<uint8_t>(MusicEventKind::Midi);
+ event.status = status;
+ event.byte1 = first;
+ event.track = status & 0x0F;
+ const uint8_t kind = status & 0xF0;
+ if (kind != 0xC0 && kind != 0xD0) {
+ event.byte2 = reader.U8();
+ }
+ touched[event.track] = true;
+ if (chunkChannel < 0) {
+ chunkChannel = event.track;
+ } else if (chunkChannel != event.track) {
+ chunkChannel = 0x7F;
+ }
+ pending.push_back(event);
+ }
+
+ const int markerTrack = (chunkChannel >= 0 && chunkChannel < 16) ? chunkChannel : 0;
+ for (SmfEvent& event : pending) {
+ if (event.track < 0) {
+ event.track = markerTrack;
+ }
+ out.push_back(event);
+ }
+ return reader.Ok();
+}
+
+void FoldNoteOffs(std::vector<SmfEvent>& events) {
+ std::map<uint32_t, std::vector<size_t>> open;
+ uint32_t lastTick = 0;
+ for (const SmfEvent& event : events) {
+ lastTick = std::max(lastTick, event.tick);
+ }
+
+ std::vector<bool> drop(events.size(), false);
+ for (size_t i = 0; i < events.size(); i++) {
+ SmfEvent& event = events[i];
+ if (event.kind != static_cast<uint8_t>(MusicEventKind::Midi)) {
+ continue;
+ }
+ const uint8_t kind = event.status & 0xF0;
+ const uint32_t key = (static_cast<uint32_t>(event.track) << 8) | event.byte1;
+
+ if (kind == 0x90 && event.byte2 != 0) {
+ open[key].push_back(i);
+ } else if (kind == 0x80 || (kind == 0x90 && event.byte2 == 0)) {
+ auto it = open.find(key);
+ if (it != open.end() && !it->second.empty()) {
+ const size_t start = it->second.back();
+ it->second.pop_back();
+ events[start].aux = event.tick - events[start].tick;
+ }
+ drop[i] = true;
+ }
+ }
+ for (auto& [key, indices] : open) {
+ for (size_t start : indices) {
+ events[start].aux = lastTick > events[start].tick ? lastTick - events[start].tick : 1;
+ }
+ }
+
+ std::vector<SmfEvent> kept;
+ kept.reserve(events.size());
+ for (size_t i = 0; i < events.size(); i++) {
+ if (!drop[i]) {
+ kept.push_back(events[i]);
+ }
+ }
+ events.swap(kept);
+}
+
+void PushByte(std::vector<uint8_t>& out, uint8_t byte) {
+ out.push_back(byte);
+ if (byte == kBlockCode) {
+ out.push_back(kBlockCode);
+ }
+}
+
+void PushSeqVarLen(std::vector<uint8_t>& out, uint32_t value) {
+ uint8_t buffer[5];
+ int count = 0;
+ buffer[count++] = static_cast<uint8_t>(value & 0x7F);
+ while ((value >>= 7) != 0) {
+ buffer[count++] = static_cast<uint8_t>((value & 0x7F) | 0x80);
+ }
+ while (count > 0) {
+ PushByte(out, buffer[--count]);
+ }
+}
+
+} // namespace
+
+std::optional<std::shared_ptr<IParsedData>> MusicFactory::parse_modding(std::vector<uint8_t>& buffer,
+ YAML::Node& node) {
+ SmfReader reader(buffer.data(), buffer.size());
+ if (!reader.Tag("MThd")) {
+ SPDLOG_ERROR("Music: not a MIDI file");
+ return std::nullopt;
+ }
+ const uint32_t headerLength = reader.U32();
+ const uint16_t format = reader.U16();
+ const uint16_t trackCount = reader.U16();
+ const uint16_t division = reader.U16();
+ reader.Seek(8 + headerLength);
+
+ if (format > 1) {
+ SPDLOG_ERROR("Music: format {} is not supported; save as format 0 or 1", format);
+ return std::nullopt;
+ }
+ if ((division & 0x8000) != 0) {
+ SPDLOG_ERROR("Music: SMPTE timing is not supported; use ticks per quarter note");
+ return std::nullopt;
+ }
+
+ std::vector<SmfEvent> events;
+ uint32_t endTicks[16] = { 0 };
+ std::optional<uint32_t> volume;
+ int sequence = 0;
+ for (uint16_t i = 0; i < trackCount && reader.Ok(); i++) {
+ if (!reader.Tag("MTrk")) {
+ SPDLOG_ERROR("Music: track {} is not an MTrk chunk", i);
+ return std::nullopt;
+ }
+ const uint32_t size = reader.U32();
+ const size_t end = reader.At() + size;
+ if (!ReadSmfTrack(reader, end, events, sequence, endTicks, volume)) {
+ SPDLOG_ERROR("Music: track {} does not parse", i);
+ return std::nullopt;
+ }
+ reader.Seek(end);
+ }
+ if (!reader.Ok()) {
+ SPDLOG_ERROR("Music: file ended early");
+ return std::nullopt;
+ }
+
+ int muting = 0;
+ int callbacks = 0;
+ for (const SmfEvent& event : events) {
+ if (event.kind != static_cast<uint8_t>(MusicEventKind::Midi) || (event.status & 0xF0) != 0xB0) {
+ continue;
+ }
+ if (event.byte1 == 0x7E || event.byte1 == 0x7F) {
+ muting++;
+ } else if (event.byte1 >= 0x6A && event.byte1 <= 0x77) {
+ callbacks++;
+ }
+ }
+ if (muting != 0) {
+ SPDLOG_WARN("Music: {} controller 126/127 event(s) will mute or unmute a channel, not set mono or poly mode",
+ muting);
+ }
+ if (callbacks != 0) {
+ SPDLOG_WARN("Music: {} controller event(s) in 106-119 signal the game rather than the synth", callbacks);
+ }
+
+ const bool hasLoop = std::any_of(events.begin(), events.end(), [](const SmfEvent& event) {
+ return event.kind == static_cast<uint8_t>(MusicEventKind::LoopStart) ||
+ event.kind == static_cast<uint8_t>(MusicEventKind::LoopEnd);
+ });
+ if (!hasLoop) {
+ if (const auto loopPoint = LoopPointFromName(Companion::Instance->GetCurrentModdingSource())) {
+ bool used[16] = { false };
+ for (const SmfEvent& event : events) {
+ if (event.kind == static_cast<uint8_t>(MusicEventKind::Midi)) {
+ used[event.track & 0x0F] = true;
+ }
+ }
+ int added = 0;
+ for (int track = 0; track < 16; track++) {
+ if (!used[track] || endTicks[track] <= *loopPoint) {
+ continue;
+ }
+ SmfEvent start;
+ start.tick = *loopPoint;
+ start.order = -1;
+ start.kind = static_cast<uint8_t>(MusicEventKind::LoopStart);
+ start.track = static_cast<uint8_t>(track);
+ events.push_back(start);
+
+ SmfEvent finish;
+ finish.tick = endTicks[track];
+ finish.order = sequence++;
+ finish.kind = static_cast<uint8_t>(MusicEventKind::LoopEnd);
+ finish.byte1 = kLoopForever;
+ finish.byte2 = kLoopForever;
+ finish.track = static_cast<uint8_t>(track);
+ events.push_back(finish);
+ added++;
+ }
+ SPDLOG_INFO("Music: looping {} track(s) back to tick {} from the file name", added, *loopPoint);
+ }
+ }
+
+ FoldNoteOffs(events);
+ std::stable_sort(events.begin(), events.end(), [](const SmfEvent& a, const SmfEvent& b) {
+ return a.tick != b.tick ? a.tick < b.tick : a.order < b.order;
+ });
+
+ std::vector<uint8_t> out(68, 0);
+ uint32_t offsets[16] = { 0 };
+ size_t written = 0;
+
+ for (int track = 0; track < 16; track++) {
+ std::vector<const SmfEvent*> mine;
+ for (const SmfEvent& event : events) {
+ if (event.track == track) {
+ mine.push_back(&event);
+ }
+ }
+ if (mine.empty()) {
+ continue;
+ }
+ offsets[track] = static_cast<uint32_t>(out.size());
+ written++;
+
+ uint32_t last = 0;
+ std::vector<size_t> loopTargets;
+ for (const SmfEvent* event : mine) {
+ PushSeqVarLen(out, event->tick - last);
+ last = event->tick;
+
+ switch (static_cast<MusicEventKind>(event->kind)) {
+ case MusicEventKind::Midi: {
+ PushByte(out, event->status);
+ PushByte(out, event->byte1);
+ const uint8_t kind = event->status & 0xF0;
+ if (kind != 0xC0 && kind != 0xD0) {
+ PushByte(out, event->byte2);
+ if (kind == 0x90) {
+ PushSeqVarLen(out, event->aux);
+ }
+ }
+ break;
+ }
+ case MusicEventKind::Tempo:
+ PushByte(out, kMetaPrefix);
+ PushByte(out, kSetTempo);
+ PushByte(out, static_cast<uint8_t>(event->aux >> 16));
+ PushByte(out, static_cast<uint8_t>(event->aux >> 8));
+ PushByte(out, static_cast<uint8_t>(event->aux));
+ break;
+ case MusicEventKind::LoopStart:
+ PushByte(out, kMetaPrefix);
+ PushByte(out, kLoopStart);
+ PushByte(out, 0);
+ PushByte(out, 0);
+ loopTargets.push_back(out.size());
+ break;
+ case MusicEventKind::LoopEnd: {
+ PushByte(out, kMetaPrefix);
+ PushByte(out, kLoopEnd);
+ const size_t target = loopTargets.empty() ? offsets[track] : loopTargets.back();
+ if (!loopTargets.empty()) {
+ loopTargets.pop_back();
+ }
+ out.push_back(event->byte1);
+ out.push_back(event->byte2);
+ const uint32_t back = static_cast<uint32_t>(out.size() + 4 - target);
+ out.push_back(static_cast<uint8_t>(back >> 24));
+ out.push_back(static_cast<uint8_t>(back >> 16));
+ out.push_back(static_cast<uint8_t>(back >> 8));
+ out.push_back(static_cast<uint8_t>(back));
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ PushSeqVarLen(out, endTicks[track] > last ? endTicks[track] - last : 0);
+ PushByte(out, kMetaPrefix);
+ PushByte(out, kEndOfTrack);
+ }
+
+ for (int i = 0; i < 16; i++) {
+ out[i * 4 + 0] = static_cast<uint8_t>(offsets[i] >> 24);
+ out[i * 4 + 1] = static_cast<uint8_t>(offsets[i] >> 16);
+ out[i * 4 + 2] = static_cast<uint8_t>(offsets[i] >> 8);
+ out[i * 4 + 3] = static_cast<uint8_t>(offsets[i]);
+ }
+ out[64] = static_cast<uint8_t>(division >> 24);
+ out[65] = static_cast<uint8_t>(division >> 16);
+ out[66] = static_cast<uint8_t>(division >> 8);
+ out[67] = static_cast<uint8_t>(division);
+
+ auto music = std::make_shared<MusicData>(std::move(out));
+ music->mDivision = division;
+ music->mVolume = volume.value_or(VanillaVolume(node));
+ if (volume.has_value() && *volume != VanillaVolume(node)) {
+ SPDLOG_INFO("Music: slot volume set to {} of {}", *volume, kDefaultMusicVolume);
+ }
+ SPDLOG_INFO("Music: built a sequence from MIDI -- {} track(s), division {}, {} bytes", written, division,
+ music->mBuffer.size());
+ return music;
+}
+
+} // namespace BK64
diff --git a/src/factories/bk64/MusicFactory.h b/src/factories/bk64/MusicFactory.h
new file mode 100644
index 0000000..3e0fbc7
--- /dev/null
+++ b/src/factories/bk64/MusicFactory.h
@@ -0,0 +1,68 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+#include "types/RawBuffer.h"
+
+#include <cstdint>
+#include <vector>
+
+namespace BK64 {
+
+constexpr uint32_t kDefaultMusicVolume = 32767;
+
+enum class MusicEventKind : uint8_t {
+ Midi = 0,
+ Tempo = 1,
+ LoopStart = 2,
+ LoopEnd = 3,
+ End = 4,
+};
+
+struct MusicEvent {
+ uint32_t delta = 0;
+ uint8_t kind = 0;
+ uint8_t status = 0;
+ uint8_t byte1 = 0;
+ uint8_t byte2 = 0;
+ uint32_t aux = 0;
+};
+
+struct MusicTrack {
+ uint32_t index = 0;
+ bool present = false;
+ std::vector<MusicEvent> events;
+};
+
+class MusicData : public RawBuffer {
+ public:
+ uint32_t mDivision = 0;
+ uint32_t mVolume = kDefaultMusicVolume;
+ std::vector<MusicTrack> mTracks;
+
+ explicit MusicData(std::vector<uint8_t> bytes) : RawBuffer(bytes) {
+ }
+};
+
+class MusicBinaryExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName,
+ YAML::Node& node, std::string* replacement) override;
+};
+
+class MusicModdingExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName,
+ YAML::Node& node, std::string* replacement) override;
+};
+
+class MusicFactory : public BaseFactory {
+ public:
+ std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ std::optional<std::shared_ptr<IParsedData>> parse_modding(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ inline std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
+ return { REGISTER(Binary, MusicBinaryExporter) REGISTER(Modding, MusicModdingExporter) };
+ }
+ bool SupportModdedAssets() override {
+ return true;
+ }
+};
+
+} // namespace BK64
diff --git a/src/factories/bk64/SoundfontFactory.cpp b/src/factories/bk64/SoundfontFactory.cpp
new file mode 100644
index 0000000..e75ca3e
--- /dev/null
+++ b/src/factories/bk64/SoundfontFactory.cpp
@@ -0,0 +1,1367 @@
+#include "SoundfontFactory.h"
+#include "BKByteUtils.h"
+#include "VadpcmEncode.h"
+
+#include <filesystem>
+#include <optional>
+#include <fstream>
+
+#include "Companion.h"
+#include "spdlog/spdlog.h"
+#include "types/RawBuffer.h"
+#include "utils/Decompressor.h"
+
+#include <cstdint>
+#include <cmath>
+#include <cstring>
+#include <iomanip>
+#include <map>
+#include <sstream>
+#include <stdexcept>
+#include <string>
+
+namespace BK64 {
+
+namespace {
+
+// AL_BANK_VERSION magic
+constexpr uint16_t kAlBankRevision = 0x4231;
+
+// Record layouts, all big-endian, all offsets relative to the ctl start:
+//
+// ALBankFile: [0] s16 revision, [2] s16 bankCount, [4+] u32 bankOffsets[]
+// ALBank: [0] s16 instCount, [2] u8 flags, [3] u8 pad, [4] s32 sampleRate,
+// [8] u32 percussion, [12+] u32 instOffsets[]
+// ALInstrument:[0] u8 volume..vibDelay (12 bytes), [12] s16 bendRange,
+// [14] s16 soundCount, [16+] u32 soundOffsets[]
+// ALSound: [0] u32 envelope, [4] u32 keyMap, [8] u32 wavetable,
+// [12] u8 samplePan, [13] u8 sampleVolume, [14] u8 flags
+// ALEnvelope: [0] s32 attackTime, [4] s32 decayTime, [8] s32 releaseTime,
+// [12] u8 attackVolume, [13] u8 decayVolume
+// ALKeyMap: [0] u8 velocityMin, [1] u8 velocityMax, [2] u8 keyMin,
+// [3] u8 keyMax, [4] u8 keyBase, [5] s8 detune
+// ALWaveTable: [0] u32 base, [4] s32 len, [8] u8 type, [9] u8 flags,
+// [12] u32 loop, [16] u32 book
+// ALADPCMBook: [0] s32 order, [4] s32 npredictors, [8+] s16 book[]
+// ALADPCMloop: [0] u32 start, [4] u32 end, [8] u32 count, [12] s16 state[16]
+
+constexpr uint8_t kAdpcmWave = 0;
+
+class CtlReader {
+ public:
+ CtlReader(const uint8_t* data, size_t size) : mData(data), mSize(size) {
+ }
+
+ void Require(uint32_t off, uint64_t need, const char* what) const {
+ if (static_cast<uint64_t>(off) + need > mSize) {
+ throw std::runtime_error(std::string("Soundfont: ctl walk ran past the end reading ") + what);
+ }
+ }
+
+ uint8_t U8(uint32_t offset) const {
+ return mData[offset];
+ }
+ int8_t S8(uint32_t offset) const {
+ return static_cast<int8_t>(mData[offset]);
+ }
+ int16_t S16(uint32_t offset) const {
+ return ReadS16BE(mData + offset);
+ }
+ uint32_t U32(uint32_t offset) const {
+ return ReadU32BE(mData + offset);
+ }
+ int32_t S32(uint32_t offset) const {
+ return ReadS32BE(mData + offset);
+ }
+
+ private:
+ const uint8_t* mData;
+ size_t mSize;
+};
+
+class ImageWriter {
+ public:
+ explicit ImageWriter(SoundfontImage& image) : mImage(image) {
+ }
+
+ void U8(uint32_t off, uint8_t value) {
+ if (off >= mImage.bytes.size()) {
+ throw std::runtime_error("Soundfont: serialized record runs past the end of the ctl");
+ }
+ mImage.bytes[off] = value;
+ mImage.covered[off] = 1;
+ }
+ void S8(uint32_t off, int8_t value) {
+ U8(off, static_cast<uint8_t>(value));
+ }
+ void U16(uint32_t off, uint16_t value) {
+ U8(off, static_cast<uint8_t>(value >> 8));
+ U8(off + 1, static_cast<uint8_t>(value));
+ }
+ void S16(uint32_t off, int16_t value) {
+ U16(off, static_cast<uint16_t>(value));
+ }
+ void U32(uint32_t off, uint32_t value) {
+ U16(off, static_cast<uint16_t>(value >> 16));
+ U16(off + 2, static_cast<uint16_t>(value));
+ }
+ void S32(uint32_t off, int32_t value) {
+ U32(off, static_cast<uint32_t>(value));
+ }
+
+ private:
+ SoundfontImage& mImage;
+};
+
+void ParseBook(const CtlReader& reader, SoundfontData& font, uint32_t off) {
+ if (off == 0 || font.mBooks.count(off) != 0) {
+ return;
+ }
+ reader.Require(off, 8, "ALADPCMBook header");
+
+ SoundfontBook book;
+ book.order = reader.S32(off);
+ book.npredictors = reader.S32(off + 4);
+ if (book.order <= 0 || book.npredictors <= 0) {
+ throw std::runtime_error("Soundfont: ALADPCMBook order/npredictors not positive");
+ }
+
+ const uint64_t count = static_cast<uint64_t>(book.order) * static_cast<uint64_t>(book.npredictors) * 8;
+ reader.Require(off + 8, count * 2, "ALADPCMBook table");
+
+ book.book.reserve(static_cast<size_t>(count));
+ for (uint64_t i = 0; i < count; i++) {
+ book.book.push_back(reader.S16(off + 8 + static_cast<uint32_t>(i * 2)));
+ }
+ font.mBooks.emplace(off, std::move(book));
+}
+
+void ParseLoop(const CtlReader& reader, SoundfontData& font, uint32_t off) {
+ if (off == 0 || font.mLoops.count(off) != 0) {
+ return;
+ }
+ reader.Require(off, 44, "ALADPCMloop");
+
+ SoundfontLoop loop;
+ loop.start = reader.U32(off);
+ loop.end = reader.U32(off + 4);
+ loop.count = reader.U32(off + 8);
+ loop.state.reserve(16);
+ for (uint32_t i = 0; i < 16; i++) {
+ loop.state.push_back(reader.S16(off + 12 + i * 2));
+ }
+ font.mLoops.emplace(off, std::move(loop));
+}
+
+void ParseWave(const CtlReader& reader, SoundfontData& font, uint32_t off) {
+ if (off == 0 || font.mWaves.count(off) != 0) {
+ return;
+ }
+ reader.Require(off, 12, "ALWaveTable header");
+
+ SoundfontWave wave;
+ wave.base = reader.U32(off);
+ wave.len = reader.S32(off + 4);
+ wave.type = reader.U8(off + 8);
+ wave.flags = reader.U8(off + 9);
+ if (wave.len < 0) {
+ throw std::runtime_error("Soundfont: negative wavetable len");
+ }
+
+ if (wave.type != kAdpcmWave) {
+ throw std::runtime_error("Soundfont: only ADPCM wavetables are supported");
+ }
+ reader.Require(off + 12, 8, "ALWaveTable adpcmWave");
+ wave.loopOffset = reader.U32(off + 12);
+ wave.bookOffset = reader.U32(off + 16);
+
+ // Insert before recursing so a self-referential offset cannot loop forever.
+ const uint32_t loopOffset = wave.loopOffset;
+ const uint32_t bookOffset = wave.bookOffset;
+ font.mWaves.emplace(off, std::move(wave));
+
+ ParseLoop(reader, font, loopOffset);
+ ParseBook(reader, font, bookOffset);
+}
+
+void ParseEnvelope(const CtlReader& reader, SoundfontData& font, uint32_t off) {
+ if (off == 0 || font.mEnvelopes.count(off) != 0) {
+ return;
+ }
+ reader.Require(off, 14, "ALEnvelope");
+
+ SoundfontEnvelope env;
+ env.attackTime = reader.S32(off);
+ env.decayTime = reader.S32(off + 4);
+ env.releaseTime = reader.S32(off + 8);
+ env.attackVolume = reader.U8(off + 12);
+ env.decayVolume = reader.U8(off + 13);
+ font.mEnvelopes.emplace(off, env);
+}
+
+void ParseKeyMap(const CtlReader& reader, SoundfontData& font, uint32_t off) {
+ if (off == 0 || font.mKeyMaps.count(off) != 0) {
+ return;
+ }
+ reader.Require(off, 6, "ALKeyMap");
+
+ SoundfontKeyMap keymap;
+ keymap.velocityMin = reader.U8(off);
+ keymap.velocityMax = reader.U8(off + 1);
+ keymap.keyMin = reader.U8(off + 2);
+ keymap.keyMax = reader.U8(off + 3);
+ keymap.keyBase = reader.U8(off + 4);
+ keymap.detune = reader.S8(off + 5);
+ font.mKeyMaps.emplace(off, keymap);
+}
+
+void ParseSound(const CtlReader& reader, SoundfontData& font, uint32_t off) {
+ if (off == 0 || font.mSounds.count(off) != 0) {
+ return;
+ }
+ reader.Require(off, 15, "ALSound");
+
+ SoundfontSound sound;
+ sound.envelopeOffset = reader.U32(off);
+ sound.keyMapOffset = reader.U32(off + 4);
+ sound.waveOffset = reader.U32(off + 8);
+ sound.samplePan = reader.U8(off + 12);
+ sound.sampleVolume = reader.U8(off + 13);
+ sound.flags = reader.U8(off + 14);
+ font.mSounds.emplace(off, sound);
+
+ ParseEnvelope(reader, font, sound.envelopeOffset);
+ ParseKeyMap(reader, font, sound.keyMapOffset);
+ ParseWave(reader, font, sound.waveOffset);
+}
+
+void ParseInstrument(const CtlReader& reader, SoundfontData& font, uint32_t off) {
+ if (off == 0 || font.mInstruments.count(off) != 0) {
+ return;
+ }
+ reader.Require(off, 16, "ALInstrument header");
+
+ const int16_t soundCount = reader.S16(off + 14);
+ if (soundCount < 0) {
+ throw std::runtime_error("Soundfont: negative instrument soundCount");
+ }
+ reader.Require(off + 16, static_cast<uint64_t>(soundCount) * 4, "ALInstrument soundArray");
+
+ SoundfontInstrument inst;
+ inst.volume = reader.U8(off);
+ inst.pan = reader.U8(off + 1);
+ inst.priority = reader.U8(off + 2);
+ inst.flags = reader.U8(off + 3);
+ inst.tremType = reader.U8(off + 4);
+ inst.tremRate = reader.U8(off + 5);
+ inst.tremDepth = reader.U8(off + 6);
+ inst.tremDelay = reader.U8(off + 7);
+ inst.vibType = reader.U8(off + 8);
+ inst.vibRate = reader.U8(off + 9);
+ inst.vibDepth = reader.U8(off + 10);
+ inst.vibDelay = reader.U8(off + 11);
+ inst.bendRange = reader.S16(off + 12);
+ inst.soundOffsets.reserve(static_cast<size_t>(soundCount));
+ for (int16_t i = 0; i < soundCount; i++) {
+ inst.soundOffsets.push_back(reader.U32(off + 16 + static_cast<uint32_t>(i) * 4));
+ }
+
+ const std::vector<uint32_t> sounds = inst.soundOffsets;
+ font.mInstruments.emplace(off, std::move(inst));
+
+ for (uint32_t sndOff : sounds) {
+ ParseSound(reader, font, sndOff);
+ }
+}
+
+void ParseBank(const CtlReader& reader, SoundfontData& font, uint32_t off) {
+ if (off == 0 || font.mBanks.count(off) != 0) {
+ return;
+ }
+ reader.Require(off, 12, "ALBank header");
+
+ const int16_t instCount = reader.S16(off);
+ if (instCount < 0) {
+ throw std::runtime_error("Soundfont: negative bank instCount");
+ }
+ reader.Require(off + 12, static_cast<uint64_t>(instCount) * 4, "ALBank instArray");
+
+ SoundfontBank bank;
+ bank.flags = reader.U8(off + 2);
+ bank.pad = reader.U8(off + 3);
+ bank.sampleRate = reader.S32(off + 4);
+ bank.percussionOffset = reader.U32(off + 8);
+ bank.instrumentOffsets.reserve(static_cast<size_t>(instCount));
+ for (int16_t i = 0; i < instCount; i++) {
+ bank.instrumentOffsets.push_back(reader.U32(off + 12 + static_cast<uint32_t>(i) * 4));
+ }
+
+ // Neither bank has one, so there is no way to test a path that handled it.
+ if (bank.percussionOffset != 0) {
+ throw std::runtime_error("Soundfont: percussion instruments are not supported");
+ }
+
+ const std::vector<uint32_t> instruments = bank.instrumentOffsets;
+ font.mBanks.emplace(off, std::move(bank));
+
+ for (uint32_t instOff : instruments) {
+ ParseInstrument(reader, font, instOff);
+ }
+}
+
+bool ValidateCtl(const uint8_t* ctl, size_t ctlSize) {
+ if (ctlSize < 8) {
+ return false;
+ }
+ if (ReadU16BE(ctl) != kAlBankRevision) {
+ return false;
+ }
+ int16_t bankCount = ReadS16BE(ctl + 2);
+ if (bankCount <= 0 || bankCount > 16) {
+ return false;
+ }
+ uint32_t bank0 = ReadU32BE(ctl + 4);
+ if (bank0 != 0 && (bank0 < 4u + 4u * bankCount || bank0 >= ctlSize)) {
+ return false;
+ }
+ try {
+ return ParseSoundfont(ctl, ctlSize).SampleDataEnd() != 0;
+ } catch (...) { return false; }
+}
+
+void VerifySoundfont(const SoundfontData& font, uint32_t ctlOffset) {
+ SoundfontImage image;
+ try {
+ image = font.Serialize();
+ } catch (const std::exception& envelope) {
+ SPDLOG_ERROR("Soundfont ctl@0x{:X}: could not rebuild for verification: {}", ctlOffset, envelope.what());
+ return;
+ }
+
+ size_t mismatches = 0;
+ size_t firstMismatch = 0;
+ size_t unclaimed = 0;
+ size_t unclaimedNonZero = 0;
+ size_t firstNonZero = 0;
+
+ for (size_t i = 0; i < font.mBuffer.size(); i++) {
+ if (image.covered[i]) {
+ if (image.bytes[i] != font.mBuffer[i]) {
+ if (mismatches++ == 0) {
+ firstMismatch = i;
+ }
+ }
+ } else {
+ unclaimed++;
+ if (font.mBuffer[i] != 0 && unclaimedNonZero++ == 0) {
+ firstNonZero = i;
+ }
+ }
+ }
+
+ SPDLOG_INFO("Soundfont ctl@0x{:X}: {} bank(s), {} instruments, {} sounds, {} wavetables, {} books, {} loops, "
+ "{} envelopes, {} keymaps; sample data ends at 0x{:X}",
+ ctlOffset, font.mBankOffsets.size(), font.mInstruments.size(), font.mSounds.size(), font.mWaves.size(),
+ font.mBooks.size(), font.mLoops.size(), font.mEnvelopes.size(), font.mKeyMaps.size(),
+ font.SampleDataEnd());
+
+ if (mismatches != 0) {
+ SPDLOG_ERROR("Soundfont ctl@0x{:X}: rebuild differs from the ROM in {} byte(s), first at 0x{:X}", ctlOffset,
+ mismatches, firstMismatch);
+ }
+ if (unclaimedNonZero != 0) {
+ SPDLOG_ERROR("Soundfont ctl@0x{:X}: {} non-zero byte(s) belong to no record, first at 0x{:X}", ctlOffset,
+ unclaimedNonZero, firstNonZero);
+ }
+ if (mismatches == 0 && unclaimedNonZero == 0) {
+ SPDLOG_DEBUG("Soundfont ctl@0x{:X}: round-trip clean ({} padding bytes)", ctlOffset, unclaimed);
+ }
+}
+
+} // namespace
+
+SoundfontData ParseSoundfont(const uint8_t* ctl, size_t ctlSize) {
+ const CtlReader reader(ctl, ctlSize);
+ SoundfontData font(std::vector<uint8_t>(ctl, ctl + ctlSize));
+
+ reader.Require(0, 4, "ALBankFile header");
+ font.mRevision = reader.S16(0);
+
+ const int16_t bankCount = reader.S16(2);
+ if (bankCount <= 0) {
+ throw std::runtime_error("Soundfont: bankCount <= 0");
+ }
+ reader.Require(4, static_cast<uint64_t>(bankCount) * 4, "ALBankFile bankArray");
+
+ font.mBankOffsets.reserve(static_cast<size_t>(bankCount));
+ for (int16_t i = 0; i < bankCount; i++) {
+ font.mBankOffsets.push_back(reader.U32(4 + static_cast<uint32_t>(i) * 4));
+ }
+ for (uint32_t bankOff : font.mBankOffsets) {
+ ParseBank(reader, font, bankOff);
+ }
+
+ return font;
+}
+
+uint64_t SoundfontData::SampleDataEnd() const {
+ uint64_t end = 0;
+ for (const auto& [off, wave] : mWaves) {
+ const uint64_t waveEnd = static_cast<uint64_t>(wave.base) + static_cast<uint64_t>(wave.len);
+ if (waveEnd > end) {
+ end = waveEnd;
+ }
+ }
+ return end;
+}
+
+std::vector<uint8_t> SoundfontData::SampleBytes(const SoundfontWave& wave) const {
+ const uint64_t end = static_cast<uint64_t>(wave.base) + static_cast<uint64_t>(wave.len);
+ if (wave.len <= 0 || end > mSampleData.size()) {
+ return {};
+ }
+ return std::vector<uint8_t>(mSampleData.begin() + wave.base, mSampleData.begin() + static_cast<size_t>(end));
+}
+
+SoundfontImage SoundfontData::Serialize() const {
+ SoundfontImage image;
+ image.bytes.assign(mBuffer.size(), 0);
+ image.covered.assign(mBuffer.size(), 0);
+ ImageWriter writer(image);
+
+ writer.S16(0, mRevision);
+ writer.S16(2, static_cast<int16_t>(mBankOffsets.size()));
+ for (size_t i = 0; i < mBankOffsets.size(); i++) {
+ writer.U32(static_cast<uint32_t>(4 + i * 4), mBankOffsets[i]);
+ }
+
+ for (const auto& [off, bank] : mBanks) {
+ writer.S16(off, static_cast<int16_t>(bank.instrumentOffsets.size()));
+ writer.U8(off + 2, bank.flags);
+ writer.U8(off + 3, bank.pad);
+ writer.S32(off + 4, bank.sampleRate);
+ writer.U32(off + 8, bank.percussionOffset);
+ for (size_t i = 0; i < bank.instrumentOffsets.size(); i++) {
+ writer.U32(off + 12 + static_cast<uint32_t>(i * 4), bank.instrumentOffsets[i]);
+ }
+ }
+
+ for (const auto& [off, inst] : mInstruments) {
+ writer.U8(off, inst.volume);
+ writer.U8(off + 1, inst.pan);
+ writer.U8(off + 2, inst.priority);
+ writer.U8(off + 3, inst.flags);
+ writer.U8(off + 4, inst.tremType);
+ writer.U8(off + 5, inst.tremRate);
+ writer.U8(off + 6, inst.tremDepth);
+ writer.U8(off + 7, inst.tremDelay);
+ writer.U8(off + 8, inst.vibType);
+ writer.U8(off + 9, inst.vibRate);
+ writer.U8(off + 10, inst.vibDepth);
+ writer.U8(off + 11, inst.vibDelay);
+ writer.S16(off + 12, inst.bendRange);
+ writer.S16(off + 14, static_cast<int16_t>(inst.soundOffsets.size()));
+ for (size_t i = 0; i < inst.soundOffsets.size(); i++) {
+ writer.U32(off + 16 + static_cast<uint32_t>(i * 4), inst.soundOffsets[i]);
+ }
+ }
+
+ for (const auto& [off, sound] : mSounds) {
+ writer.U32(off, sound.envelopeOffset);
+ writer.U32(off + 4, sound.keyMapOffset);
+ writer.U32(off + 8, sound.waveOffset);
+ writer.U8(off + 12, sound.samplePan);
+ writer.U8(off + 13, sound.sampleVolume);
+ writer.U8(off + 14, sound.flags);
+ }
+
+ for (const auto& [off, env] : mEnvelopes) {
+ writer.S32(off, env.attackTime);
+ writer.S32(off + 4, env.decayTime);
+ writer.S32(off + 8, env.releaseTime);
+ writer.U8(off + 12, env.attackVolume);
+ writer.U8(off + 13, env.decayVolume);
+ }
+
+ for (const auto& [off, keymap] : mKeyMaps) {
+ writer.U8(off, keymap.velocityMin);
+ writer.U8(off + 1, keymap.velocityMax);
+ writer.U8(off + 2, keymap.keyMin);
+ writer.U8(off + 3, keymap.keyMax);
+ writer.U8(off + 4, keymap.keyBase);
+ writer.S8(off + 5, keymap.detune);
+ }
+
+ for (const auto& [off, wave] : mWaves) {
+ writer.U32(off, wave.base);
+ writer.S32(off + 4, wave.len);
+ writer.U8(off + 8, wave.type);
+ writer.U8(off + 9, wave.flags);
+ writer.U32(off + 12, wave.loopOffset);
+ if (wave.type == kAdpcmWave) {
+ writer.U32(off + 16, wave.bookOffset);
+ }
+ }
+
+ for (const auto& [off, loop] : mLoops) {
+ writer.U32(off, loop.start);
+ writer.U32(off + 4, loop.end);
+ writer.U32(off + 8, loop.count);
+ for (size_t i = 0; i < loop.state.size(); i++) {
+ writer.S16(off + 12 + static_cast<uint32_t>(i * 2), loop.state[i]);
+ }
+ }
+
+ for (const auto& [off, book] : mBooks) {
+ writer.S32(off, book.order);
+ writer.S32(off + 4, book.npredictors);
+ for (size_t i = 0; i < book.book.size(); i++) {
+ writer.S16(off + 8 + static_cast<uint32_t>(i * 2), book.book[i]);
+ }
+ }
+
+ return image;
+}
+
+uint32_t LocateSoundfontCtl(const std::vector<uint8_t>& rom, uint32_t ctlOffset, uint32_t ctlSize) {
+ if ((size_t)ctlOffset + ctlSize <= rom.size() && ValidateCtl(rom.data() + ctlOffset, ctlSize)) {
+ return ctlOffset;
+ }
+
+ // Romhacks shift the whole audio region, so hunt for the real header.
+ std::vector<uint32_t> candidates;
+ for (size_t i = 0; i + 8 <= rom.size(); i += 8) {
+ if (rom[i] != 0x42 || rom[i + 1] != 0x31) {
+ continue;
+ }
+ size_t avail = std::min<size_t>(ctlSize, rom.size() - i);
+ if (ValidateCtl(rom.data() + i, avail)) {
+ candidates.push_back((uint32_t)i);
+ }
+ }
+
+ if (candidates.empty()) {
+ SPDLOG_ERROR("SoundfontCtl: no valid ALBankFile found anywhere in ROM (vanilla ctl@0x{:X} size=0x{:X})",
+ ctlOffset, ctlSize);
+ throw std::runtime_error("SoundfontCtl: soundfont ctl not found in ROM");
+ }
+
+ uint32_t best = candidates[0];
+ for (uint32_t c : candidates) {
+ auto dist = [&](uint32_t off) { return off > ctlOffset ? off - ctlOffset : ctlOffset - off; };
+ if (dist(c) < dist(best)) {
+ best = c;
+ }
+ }
+
+ SPDLOG_WARN("SoundfontCtl: ctl not at vanilla offset 0x{:X}; relocated to 0x{:X} (delta 0x{:X}, {} candidates)",
+ ctlOffset, best, (uint32_t)(best - ctlOffset), candidates.size());
+ return best;
+}
+
+std::optional<std::shared_ptr<IParsedData>> SoundfontFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) {
+ const auto ctlOffset = GetSafeNode<uint32_t>(node, "offset");
+ const auto ctlSize = GetSafeNode<uint32_t>(node, "size");
+ const auto tblOffset = GetSafeNode<uint32_t>(node, "tbl_offset");
+
+ // The tbl follows the ctl, so it shifts by the same delta.
+ const uint32_t realCtlOffset = LocateSoundfontCtl(buffer, ctlOffset, ctlSize);
+ const uint32_t realTblOffset = tblOffset + (realCtlOffset - ctlOffset);
+
+ if ((size_t)realCtlOffset + ctlSize > buffer.size()) {
+ throw std::runtime_error("SoundfontFactory: ctl exceeds ROM size");
+ }
+
+ auto soundfont = std::make_shared<SoundfontData>(ParseSoundfont(buffer.data() + realCtlOffset, ctlSize));
+
+ // No size of its own: it runs to the end of the sample data the wavetables
+ // point at, rounded to BK's 16-byte alignment.
+ const uint64_t sampleEnd = soundfont->SampleDataEnd();
+ if (sampleEnd == 0) {
+ throw std::runtime_error("SoundfontFactory: ctl referenced no wavetables");
+ }
+ const size_t tblSize = static_cast<size_t>((sampleEnd + 0xF) & ~static_cast<uint64_t>(0xF));
+ if ((size_t)realTblOffset + tblSize > buffer.size()) {
+ throw std::runtime_error("SoundfontFactory: computed tbl size exceeds ROM bounds");
+ }
+ soundfont->mSampleData.assign(buffer.begin() + realTblOffset, buffer.begin() + realTblOffset + tblSize);
+
+ VerifySoundfont(*soundfont, realCtlOffset);
+ SPDLOG_INFO("Soundfont ctl@0x{:X}: tbl@0x{:X} size 0x{:X}", realCtlOffset, realTblOffset, tblSize);
+
+ return soundfont;
+}
+
+namespace {
+
+struct SoundNaming {
+ std::string sfxPath = "sfx";
+ std::string instPath = "music";
+ int64_t sfxBase = -1;
+ std::map<uint32_t, std::string> names;
+ std::map<uint32_t, uint32_t> users;
+ std::map<uint32_t, std::string> instNames;
+};
+
+SoundNaming ReadNaming(YAML::Node& node) {
+ SoundNaming naming;
+ naming.sfxPath = GetSafeNode<std::string>(node, "sfx_path", naming.sfxPath);
+ naming.instPath = GetSafeNode<std::string>(node, "inst_path", naming.instPath);
+ if (node["sfx_base"]) {
+ naming.sfxBase = node["sfx_base"].as<int64_t>();
+ }
+
+ const YAML::Node& config = Companion::Instance->GetCurrentFileConfig();
+ if (config && config["sfx_names"]) {
+ for (auto it = config["sfx_names"].begin(); it != config["sfx_names"].end(); ++it) {
+ naming.names[it->first.as<uint32_t>()] = it->second.as<std::string>();
+ }
+ }
+ if (config && config["instrument_names"]) {
+ for (auto it = config["instrument_names"].begin(); it != config["instrument_names"].end(); ++it) {
+ naming.instNames[it->first.as<uint32_t>()] = it->second.as<std::string>();
+ }
+ }
+ if (config && config["sfx_users"]) {
+ for (auto it = config["sfx_users"].begin(); it != config["sfx_users"].end(); ++it) {
+ naming.users[it->first.as<uint32_t>()] = it->second.as<uint32_t>();
+ }
+ }
+ return naming;
+}
+
+std::string Hex(uint64_t value, int width) {
+ std::stringstream stream;
+ stream << std::uppercase << std::hex << std::setw(width) << std::setfill('0') << value;
+ return stream.str();
+}
+
+// Instruments have no names anywhere in the game, so the number a composer types
+// is the name: decimal, matching the program change. A multisampled instrument
+// splits by key range, and no two splits of one program share a range, so the
+// range names the file and says which notes it covers.
+std::string PathForSound(const SoundNaming& naming, const SoundfontData& font, size_t instIndex, size_t soundIndex,
+ size_t soundCount, uint32_t soundOffset) {
+ if (instIndex == 0 && naming.sfxBase >= 0) {
+ const uint32_t id = static_cast<uint32_t>(naming.sfxBase) + static_cast<uint32_t>(soundIndex);
+ const auto it = naming.names.find(id);
+ return naming.sfxPath + "/" + Hex(id, 3) + "_" + (it != naming.names.end() ? it->second : "UNNAMED");
+ }
+
+ std::string path = naming.instPath + "/program" + std::to_string(instIndex);
+ const auto named = naming.instNames.find(static_cast<uint32_t>(instIndex));
+ if (named != naming.instNames.end() && !named->second.empty()) {
+ path += "_" + named->second;
+ }
+ if (soundCount <= 1) {
+ return path;
+ }
+ const auto sound = font.mSounds.find(soundOffset);
+ if (sound != font.mSounds.end()) {
+ const auto keymap = font.mKeyMaps.find(sound->second.keyMapOffset);
+ if (keymap != font.mKeyMaps.end()) {
+ return path + "_keys" + std::to_string(static_cast<int>(keymap->second.keyMin)) + "-" +
+ std::to_string(static_cast<int>(keymap->second.keyMax));
+ }
+ }
+ return path + "_" + std::to_string(soundIndex);
+}
+
+void WriteSoundResource(const SoundfontData& font, const SoundfontSound& sound, const std::string& path,
+ uint32_t userCount) {
+ LUS::BinaryWriter writer;
+ BaseExporter::WriteHeader(writer, Torch::ResourceType::BKSound, 0);
+
+ writer.Write(sound.samplePan);
+ writer.Write(sound.sampleVolume);
+ writer.Write(sound.flags);
+
+ const auto env = font.mEnvelopes.find(sound.envelopeOffset);
+ const bool hasEnv = env != font.mEnvelopes.end();
+ writer.Write(static_cast<uint8_t>(hasEnv ? 1 : 0));
+ if (hasEnv) {
+ writer.Write(env->second.attackTime);
+ writer.Write(env->second.decayTime);
+ writer.Write(env->second.releaseTime);
+ writer.Write(env->second.attackVolume);
+ writer.Write(env->second.decayVolume);
+ }
+
+ const auto keymap = font.mKeyMaps.find(sound.keyMapOffset);
+ const bool hasKm = keymap != font.mKeyMaps.end();
+ writer.Write(static_cast<uint8_t>(hasKm ? 1 : 0));
+ if (hasKm) {
+ writer.Write(keymap->second.velocityMin);
+ writer.Write(keymap->second.velocityMax);
+ writer.Write(keymap->second.keyMin);
+ writer.Write(keymap->second.keyMax);
+ writer.Write(keymap->second.keyBase);
+ writer.Write(static_cast<uint8_t>(keymap->second.detune));
+ }
+
+ const auto wave = font.mWaves.find(sound.waveOffset);
+ const bool hasWave = wave != font.mWaves.end();
+ writer.Write(static_cast<uint8_t>(hasWave ? 1 : 0));
+ if (hasWave) {
+ writer.Write(wave->second.type);
+ writer.Write(wave->second.flags);
+
+ const auto loop = font.mLoops.find(wave->second.loopOffset);
+ const bool hasLoop = wave->second.loopOffset != 0 && loop != font.mLoops.end();
+ writer.Write(static_cast<uint8_t>(hasLoop ? 1 : 0));
+ if (hasLoop) {
+ writer.Write(loop->second.start);
+ writer.Write(loop->second.end);
+ writer.Write(loop->second.count);
+ writer.Write(static_cast<uint32_t>(loop->second.state.size()));
+ for (int16_t value : loop->second.state) {
+ writer.Write(value);
+ }
+ }
+
+ const auto book = font.mBooks.find(wave->second.bookOffset);
+ const bool hasBook = wave->second.bookOffset != 0 && book != font.mBooks.end();
+ writer.Write(static_cast<uint8_t>(hasBook ? 1 : 0));
+ if (hasBook) {
+ writer.Write(book->second.order);
+ writer.Write(book->second.npredictors);
+ writer.Write(static_cast<uint32_t>(book->second.book.size()));
+ for (int16_t value : book->second.book) {
+ writer.Write(value);
+ }
+ }
+
+ const std::vector<uint8_t> bytes = font.SampleBytes(wave->second);
+ writer.Write(static_cast<uint32_t>(bytes.size()));
+ writer.Write(reinterpret_cast<char*>(const_cast<uint8_t*>(bytes.data())), bytes.size());
+ }
+
+ // Appended last so anything reading only the sound itself stays valid.
+ writer.Write(userCount);
+
+ std::stringstream stream;
+ writer.Finish(stream);
+ const std::string blob = stream.str();
+ Companion::Instance->RegisterCompanionFile(path, std::vector<char>(blob.begin(), blob.end()));
+}
+
+void WriteInstrument(LUS::BinaryWriter& writer, const SoundfontInstrument& inst,
+ const std::vector<std::string>& paths) {
+ writer.Write(inst.volume);
+ writer.Write(inst.pan);
+ writer.Write(inst.priority);
+ writer.Write(inst.flags);
+ writer.Write(inst.tremType);
+ writer.Write(inst.tremRate);
+ writer.Write(inst.tremDepth);
+ writer.Write(inst.tremDelay);
+ writer.Write(inst.vibType);
+ writer.Write(inst.vibRate);
+ writer.Write(inst.vibDepth);
+ writer.Write(inst.vibDelay);
+ writer.Write(inst.bendRange);
+ writer.Write(static_cast<uint32_t>(paths.size()));
+ for (const std::string& p : paths) {
+ writer.Write(p);
+ }
+}
+
+} // namespace
+
+ExportResult SoundfontBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw,
+ std::string& entryName, YAML::Node& node, std::string* replacement) {
+ auto font = std::static_pointer_cast<SoundfontData>(raw);
+ const SoundNaming naming = ReadNaming(node);
+ const std::string root = Companion::Instance->GetCurrentDirectory();
+
+ std::map<uint32_t, std::string> pathBySound;
+ size_t written = 0;
+
+ auto collect = [&](const SoundfontInstrument& inst, size_t instIndex) {
+ std::vector<std::string> paths;
+ paths.reserve(inst.soundOffsets.size());
+ for (size_t k = 0; k < inst.soundOffsets.size(); k++) {
+ const uint32_t soundOffset = inst.soundOffsets[k];
+ if (soundOffset == 0) {
+ paths.emplace_back();
+ continue;
+ }
+ auto it = pathBySound.find(soundOffset);
+ if (it == pathBySound.end()) {
+ const std::string rel =
+ PathForSound(naming, *font, instIndex, k, inst.soundOffsets.size(), soundOffset);
+ const auto sound = font->mSounds.find(soundOffset);
+ if (sound == font->mSounds.end()) {
+ paths.emplace_back();
+ continue;
+ }
+ uint32_t userCount = 0;
+ if (instIndex == 0 && naming.sfxBase >= 0) {
+ const auto found =
+ naming.users.find(static_cast<uint32_t>(naming.sfxBase) + static_cast<uint32_t>(k));
+ if (found != naming.users.end()) {
+ userCount = found->second;
+ }
+ }
+ WriteSoundResource(*font, sound->second, rel, userCount);
+ written++;
+ it = pathBySound.emplace(soundOffset, root.empty() ? rel : root + "/" + rel).first;
+ }
+ paths.push_back(it->second);
+ }
+ return paths;
+ };
+
+ LUS::BinaryWriter writer;
+ WriteHeader(writer, Torch::ResourceType::BKSoundBank, 0);
+ writer.Write(static_cast<uint32_t>(font->mBankOffsets.size()));
+
+ for (uint32_t bankOffset : font->mBankOffsets) {
+ const auto bank = font->mBanks.find(bankOffset);
+ if (bank == font->mBanks.end()) {
+ writer.Write(static_cast<int32_t>(0)); // sampleRate
+ writer.Write(static_cast<uint8_t>(0)); // flags
+ writer.Write(static_cast<uint8_t>(0)); // pad
+ writer.Write(static_cast<uint32_t>(0));
+ continue;
+ }
+
+ writer.Write(bank->second.sampleRate);
+ writer.Write(bank->second.flags);
+ writer.Write(bank->second.pad);
+
+ writer.Write(static_cast<uint32_t>(bank->second.instrumentOffsets.size()));
+ for (size_t i = 0; i < bank->second.instrumentOffsets.size(); i++) {
+ const auto inst = font->mInstruments.find(bank->second.instrumentOffsets[i]);
+ const bool present = bank->second.instrumentOffsets[i] != 0 && inst != font->mInstruments.end();
+ writer.Write(static_cast<uint8_t>(present ? 1 : 0));
+ if (present) {
+ WriteInstrument(writer, inst->second, collect(inst->second, i));
+ }
+ }
+ }
+
+ SPDLOG_INFO("Soundfont '{}': wrote {} sound resources under '{}'", entryName, written, root);
+
+ writer.Finish(write);
+ return std::nullopt;
+}
+
+namespace {
+
+int16_t Clamp16(int32_t value) {
+ return static_cast<int16_t>(value < -32768 ? -32768 : (value > 32767 ? 32767 : value));
+}
+
+std::vector<int16_t> DecodeAdpcm(const std::vector<uint8_t>& in, const SoundfontBook& book) {
+ const size_t frames = in.size() / 9;
+ std::vector<int16_t> out(frames * 16 + 16, 0);
+ int16_t* dst = out.data() + 16;
+ size_t at = 0;
+
+ for (size_t f = 0; f < frames; f++) {
+ const int shift = in[at] >> 4;
+ int predictor = in[at++] & 0xF;
+ if (predictor >= book.npredictors) {
+ predictor = 0;
+ }
+ const int16_t* tbl0 = book.book.data() + predictor * 16;
+ const int16_t* tbl1 = tbl0 + 8;
+
+ for (int half = 0; half < 2; half++) {
+ int16_t ins[8];
+ const int16_t prev1 = dst[-1];
+ const int16_t prev2 = dst[-2];
+ for (int j = 0; j < 4; j++) {
+ ins[j * 2] = static_cast<int16_t>((((in[at] >> 4) << 28) >> 28) << shift);
+ ins[j * 2 + 1] = static_cast<int16_t>((((in[at++] & 0xF) << 28) >> 28) << shift);
+ }
+ for (int j = 0; j < 8; j++) {
+ int32_t acc = tbl0[j] * prev2 + tbl1[j] * prev1 + (ins[j] << 11);
+ for (int k = 0; k < j; k++) {
+ acc += tbl1[(j - k) - 1] * ins[k];
+ }
+ *dst++ = Clamp16(acc >> 11);
+ }
+ }
+ }
+ out.erase(out.begin(), out.begin() + 16);
+ return out;
+}
+
+void PushLE(std::vector<uint8_t>& out, uint32_t value, int bytes) {
+ for (int i = 0; i < bytes; i++) {
+ out.push_back(static_cast<uint8_t>(value >> (i * 8)));
+ }
+}
+
+std::vector<uint8_t> BuildWav(const std::vector<int16_t>& pcm, int rate) {
+ std::vector<uint8_t> out;
+ const uint32_t dataBytes = static_cast<uint32_t>(pcm.size() * 2);
+ const char* riff = "RIFF";
+ out.insert(out.end(), riff, riff + 4);
+ PushLE(out, 36 + dataBytes, 4);
+ const char* wave = "WAVEfmt ";
+ out.insert(out.end(), wave, wave + 8);
+ PushLE(out, 16, 4);
+ PushLE(out, 1, 2); // PCM
+ PushLE(out, 1, 2); // mono
+ PushLE(out, static_cast<uint32_t>(rate), 4);
+ PushLE(out, static_cast<uint32_t>(rate) * 2, 4);
+ PushLE(out, 2, 2); // block align
+ PushLE(out, 16, 2); // bits
+ const char* data = "data";
+ out.insert(out.end(), data, data + 4);
+ PushLE(out, dataBytes, 4);
+ for (int16_t sample : pcm) {
+ PushLE(out, static_cast<uint16_t>(sample), 2);
+ }
+ return out;
+}
+
+std::vector<uint8_t> BuildRaw(const SoundfontBook* book, const SoundfontLoop* loop, const std::vector<uint8_t>& adpcm) {
+ std::vector<uint8_t> out;
+ PushLE(out, book ? static_cast<uint32_t>(book->order) : 0, 4);
+ PushLE(out, book ? static_cast<uint32_t>(book->npredictors) : 0, 4);
+ PushLE(out, book ? static_cast<uint32_t>(book->book.size()) : 0, 4);
+ if (book) {
+ for (int16_t value : book->book) {
+ PushLE(out, static_cast<uint16_t>(value), 2);
+ }
+ }
+ PushLE(out, loop ? static_cast<uint32_t>(loop->state.size()) : 0, 4);
+ if (loop) {
+ for (int16_t value : loop->state) {
+ PushLE(out, static_cast<uint16_t>(value), 2);
+ }
+ }
+ PushLE(out, static_cast<uint32_t>(adpcm.size()), 4);
+ out.insert(out.end(), adpcm.begin(), adpcm.end());
+ return out;
+}
+
+int PlaybackRate(int bankRate, const SoundfontKeyMap* keymap) {
+ if (keymap == nullptr) {
+ return bankRate;
+ }
+ const double cents = static_cast<double>(keymap->keyBase) * 100.0 + static_cast<double>(keymap->detune) - 6000.0;
+ const double rate = static_cast<double>(bankRate) * std::pow(2.0, cents / 1200.0);
+ return static_cast<int>(std::lround(rate));
+}
+
+void WriteSoundModFiles(const SoundfontData& font, const SoundfontSound& sound, const std::string& rel) {
+ const auto env = font.mEnvelopes.find(sound.envelopeOffset);
+ const auto keymap = font.mKeyMaps.find(sound.keyMapOffset);
+ const auto wave = font.mWaves.find(sound.waveOffset);
+ const SoundfontBook* book = nullptr;
+ const SoundfontLoop* loop = nullptr;
+ std::vector<uint8_t> adpcm;
+ if (wave != font.mWaves.end()) {
+ const auto b = font.mBooks.find(wave->second.bookOffset);
+ const auto l = font.mLoops.find(wave->second.loopOffset);
+ book = b != font.mBooks.end() ? &b->second : nullptr;
+ loop = l != font.mLoops.end() ? &l->second : nullptr;
+ adpcm = font.SampleBytes(wave->second);
+ }
+
+ YAML::Emitter out;
+ out << YAML::BeginMap;
+ out << YAML::Key << "Sample" << YAML::Value << "raw";
+ out << YAML::Key << "SamplePan" << YAML::Value << (int)sound.samplePan;
+ out << YAML::Key << "SampleVolume" << YAML::Value << (int)sound.sampleVolume;
+ out << YAML::Key << "Flags" << YAML::Value << (int)sound.flags;
+ if (env != font.mEnvelopes.end()) {
+ out << YAML::Key << "Envelope" << YAML::Value << YAML::BeginMap;
+ out << YAML::Key << "AttackTime" << YAML::Value << env->second.attackTime;
+ out << YAML::Key << "AttackVolume" << YAML::Value << (int)env->second.attackVolume;
+ out << YAML::Key << "DecayTime" << YAML::Value << env->second.decayTime;
+ out << YAML::Key << "DecayVolume" << YAML::Value << (int)env->second.decayVolume;
+ out << YAML::Key << "ReleaseTime" << YAML::Value << env->second.releaseTime;
+ out << YAML::EndMap;
+ }
+ if (keymap != font.mKeyMaps.end()) {
+ out << YAML::Key << "KeyBase" << YAML::Value << (int)keymap->second.keyBase;
+ out << YAML::Key << "Detune" << YAML::Value << (int)keymap->second.detune;
+ out << YAML::Key << "ChainNext" << YAML::Value
+ << (int)(keymap->second.velocityMin + ((keymap->second.keyMin & 0xC0) * 4));
+ out << YAML::Key << "ChainDelayFrames" << YAML::Value << (int)keymap->second.velocityMax;
+ out << YAML::Key << "VolumeGroup" << YAML::Value << (int)(keymap->second.keyMin & 0x3F);
+ out << YAML::Key << "ReverbSend" << YAML::Value << (int)(keymap->second.keyMax & 0x0F);
+ out << YAML::Key << "KeyMaxFlags" << YAML::Value << (int)(keymap->second.keyMax & 0xF0);
+ }
+ if (loop != nullptr) {
+ out << YAML::Key << "Loop" << YAML::Value << YAML::BeginMap;
+ out << YAML::Key << "Start" << YAML::Value << loop->start;
+ out << YAML::Key << "End" << YAML::Value << loop->end;
+ out << YAML::Key << "Count" << YAML::Value << loop->count;
+ out << YAML::EndMap;
+ }
+ out << YAML::EndMap;
+
+ const std::string yaml = out.c_str();
+ Companion::Instance->RegisterCompanionFile(rel + ".yaml", std::vector<char>(yaml.begin(), yaml.end()));
+
+ const std::vector<uint8_t> bin = BuildRaw(book, loop, adpcm);
+ Companion::Instance->RegisterCompanionFile(rel + ".bin", std::vector<char>(bin.begin(), bin.end()));
+
+ if (book != nullptr && !adpcm.empty()) {
+ int32_t bankRate = 22050;
+ for (const auto& [off, bank] : font.mBanks) {
+ bankRate = bank.sampleRate;
+ break;
+ }
+ const int rate = PlaybackRate(bankRate, keymap != font.mKeyMaps.end() ? &keymap->second : nullptr);
+ const std::vector<uint8_t> wav = BuildWav(DecodeAdpcm(adpcm, *book), rate);
+ Companion::Instance->RegisterCompanionFile(rel + ".wav", std::vector<char>(wav.begin(), wav.end()));
+ }
+}
+
+} // namespace
+
+ExportResult SoundfontModdingExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw,
+ std::string& entryName, YAML::Node& node, std::string* replacement) {
+ auto font = std::static_pointer_cast<SoundfontData>(raw);
+ const SoundNaming naming = ReadNaming(node);
+ *replacement += ".yaml";
+
+ YAML::Emitter bank;
+ bank << YAML::BeginMap;
+ std::map<uint32_t, std::string> seen;
+
+ for (uint32_t bankOffset : font->mBankOffsets) {
+ const auto bankIt = font->mBanks.find(bankOffset);
+ if (bankIt == font->mBanks.end()) {
+ continue;
+ }
+ bank << YAML::Key << "SampleRate" << YAML::Value << bankIt->second.sampleRate;
+ bank << YAML::Key << "Instruments" << YAML::Value << YAML::BeginSeq;
+
+ for (size_t i = 0; i < bankIt->second.instrumentOffsets.size(); i++) {
+ const auto inst = font->mInstruments.find(bankIt->second.instrumentOffsets[i]);
+ bank << YAML::BeginMap;
+ if (inst == font->mInstruments.end()) {
+ bank << YAML::Key << "Present" << YAML::Value << false << YAML::EndMap;
+ continue;
+ }
+ bank << YAML::Key << "Volume" << YAML::Value << (int)inst->second.volume;
+ bank << YAML::Key << "Pan" << YAML::Value << (int)inst->second.pan;
+ bank << YAML::Key << "Priority" << YAML::Value << (int)inst->second.priority;
+ bank << YAML::Key << "BendRange" << YAML::Value << inst->second.bendRange;
+ bank << YAML::Key << "Sounds" << YAML::Value << YAML::BeginSeq;
+
+ for (size_t k = 0; k < inst->second.soundOffsets.size(); k++) {
+ const uint32_t soundOffset = inst->second.soundOffsets[k];
+ if (soundOffset == 0) {
+ bank << "";
+ continue;
+ }
+ auto known = seen.find(soundOffset);
+ if (known == seen.end()) {
+ known = seen.emplace(soundOffset, PathForSound(naming, *font, i, k,
+ inst->second.soundOffsets.size(), soundOffset))
+ .first;
+ const auto sound = font->mSounds.find(soundOffset);
+ if (sound != font->mSounds.end()) {
+ WriteSoundModFiles(*font, sound->second, known->second);
+ }
+ }
+ bank << known->second;
+ }
+ bank << YAML::EndSeq << YAML::EndMap;
+ }
+ bank << YAML::EndSeq;
+ }
+ bank << YAML::EndMap;
+
+ write << bank.c_str();
+ SPDLOG_INFO("Soundfont '{}': wrote {} editable sounds", entryName, seen.size());
+ return std::nullopt;
+}
+
+namespace {
+
+std::vector<uint8_t> ReadFile(const std::filesystem::path& path) {
+ std::ifstream in(path, std::ios::binary);
+ if (!in) {
+ return {};
+ }
+ return std::vector<uint8_t>(std::istreambuf_iterator<char>(in), {});
+}
+
+uint32_t ReadLE(const std::vector<uint8_t>& d, size_t& at, int bytes) {
+ uint32_t value = 0;
+ for (int i = 0; i < bytes; i++) {
+ value |= static_cast<uint32_t>(at < d.size() ? d[at] : 0) << (i * 8);
+ at++;
+ }
+ return value;
+}
+
+bool ParseRawSample(const std::vector<uint8_t>& d, SoundfontBook& book, std::vector<int16_t>& loopState,
+ std::vector<uint8_t>& adpcm) {
+ if (d.size() < 12) {
+ return false;
+ }
+ size_t at = 0;
+ book.order = static_cast<int32_t>(ReadLE(d, at, 4));
+ book.npredictors = static_cast<int32_t>(ReadLE(d, at, 4));
+ const uint32_t bookCount = ReadLE(d, at, 4);
+ book.book.clear();
+ for (uint32_t i = 0; i < bookCount; i++) {
+ book.book.push_back(static_cast<int16_t>(ReadLE(d, at, 2)));
+ }
+ const uint32_t stateCount = ReadLE(d, at, 4);
+ loopState.clear();
+ for (uint32_t i = 0; i < stateCount; i++) {
+ loopState.push_back(static_cast<int16_t>(ReadLE(d, at, 2)));
+ }
+ const uint32_t dataSize = ReadLE(d, at, 4);
+ if (at + dataSize > d.size()) {
+ return false;
+ }
+ adpcm.assign(d.begin() + at, d.begin() + at + dataSize);
+ return true;
+}
+
+int Field(const YAML::Node& n, const char* key, int def) {
+ return n[key] ? n[key].as<int>() : def;
+}
+
+void TrimSilence(std::vector<int16_t>& pcm, int rate, double& leadSeconds, double& tailSeconds) {
+ constexpr int16_t kFloor = 512;
+ size_t first = 0;
+ while (first < pcm.size() && std::abs(static_cast<int>(pcm[first])) <= kFloor) {
+ first++;
+ }
+ if (first == pcm.size()) {
+ leadSeconds = 0.0;
+ tailSeconds = 0.0;
+ return;
+ }
+ size_t last = pcm.size();
+ while (last > first && std::abs(static_cast<int>(pcm[last - 1])) <= kFloor) {
+ last--;
+ }
+
+ const size_t guard = static_cast<size_t>(rate / 500);
+ first = first > guard ? first - guard : 0;
+ last = std::min(pcm.size(), last + guard);
+
+ leadSeconds = static_cast<double>(first) / static_cast<double>(rate);
+ tailSeconds = static_cast<double>(pcm.size() - last) / static_cast<double>(rate);
+ pcm = std::vector<int16_t>(pcm.begin() + first, pcm.begin() + last);
+}
+
+} // namespace
+
+std::optional<std::shared_ptr<IParsedData>> SoundfontFactory::parse_modding(std::vector<uint8_t>& buffer,
+ YAML::Node& node) {
+ YAML::Node bankNode;
+ try {
+ bankNode = YAML::Load(std::string(reinterpret_cast<char*>(buffer.data()), buffer.size()));
+ } catch (const YAML::ParserException& envelope) {
+ SPDLOG_ERROR("Soundfont: modding yaml is malformed: {}", envelope.what());
+ return std::nullopt;
+ }
+
+ const std::filesystem::path root = std::filesystem::path(Companion::Instance->GetConfig().moddingPath) /
+ Companion::Instance->GetCurrentDirectory();
+ const int32_t sampleRate = bankNode["SampleRate"] ? bankNode["SampleRate"].as<int32_t>() : 22050;
+
+ auto font = std::make_shared<SoundfontData>(std::vector<uint8_t>());
+ uint32_t nextKey = 1;
+ std::map<std::string, uint32_t> soundByPath;
+ size_t encoded = 0;
+ size_t verbatim = 0;
+
+ SoundfontBank bank;
+ bank.sampleRate = sampleRate;
+
+ for (const auto& instNode : bankNode["Instruments"]) {
+ if (instNode["Present"] && !instNode["Present"].as<bool>()) {
+ bank.instrumentOffsets.push_back(0);
+ continue;
+ }
+ SoundfontInstrument inst;
+ inst.volume = static_cast<uint8_t>(Field(instNode, "Volume", 127));
+ inst.pan = static_cast<uint8_t>(Field(instNode, "Pan", 64));
+ inst.priority = static_cast<uint8_t>(Field(instNode, "Priority", 5));
+ inst.bendRange = static_cast<int16_t>(Field(instNode, "BendRange", 200));
+
+ for (const auto& entry : instNode["Sounds"]) {
+ const std::string rel = entry.as<std::string>();
+ if (rel.empty()) {
+ inst.soundOffsets.push_back(0);
+ continue;
+ }
+ const auto known = soundByPath.find(rel);
+ if (known != soundByPath.end()) {
+ inst.soundOffsets.push_back(known->second);
+ continue;
+ }
+
+ YAML::Node sound;
+ try {
+ sound = YAML::LoadFile((root / (rel + ".yaml")).string());
+ } catch (const std::exception& envelope) {
+ SPDLOG_ERROR("Soundfont: cannot read {}.yaml: {}", rel, envelope.what());
+ return std::nullopt;
+ }
+
+ SoundfontBook book;
+ std::vector<int16_t> loopState;
+ std::vector<uint8_t> adpcm;
+ std::optional<int32_t> fittedDecay;
+ const std::string mode = sound["Sample"] ? sound["Sample"].as<std::string>() : "raw";
+
+ if (mode == "raw") {
+ const std::vector<uint8_t> bin = ReadFile(root / (rel + ".bin"));
+ if (bin.empty() || !ParseRawSample(bin, book, loopState, adpcm)) {
+ SPDLOG_ERROR("Soundfont: {}.bin is missing or malformed", rel);
+ return std::nullopt;
+ }
+ verbatim++;
+ } else {
+ const std::filesystem::path audio = root / std::filesystem::path(rel).parent_path() / mode;
+ if (!std::filesystem::exists(audio)) {
+ SPDLOG_ERROR("Soundfont: {} names sample '{}', which is not next to it", rel, mode);
+ return std::nullopt;
+ }
+ SoundfontKeyMap slot;
+ slot.keyBase = static_cast<uint8_t>(Field(sound, "KeyBase", 60));
+ slot.detune = static_cast<int8_t>(Field(sound, "Detune", 0));
+
+ const int playbackRate = PlaybackRate(sampleRate, &slot);
+ std::vector<int16_t> pcm;
+ std::string error;
+ if (!DecodeAudioFile(audio.string(), playbackRate, pcm, error)) {
+ SPDLOG_ERROR("Soundfont: {}", error);
+ return std::nullopt;
+ }
+
+ const bool trim = sound["TrimSilence"] ? sound["TrimSilence"].as<bool>() : !sound["Loop"];
+ if (trim) {
+ double lead = 0.0, tail = 0.0;
+ TrimSilence(pcm, playbackRate, lead, tail);
+ if (lead > 0.001 || tail > 0.001) {
+ SPDLOG_INFO("Soundfont: {} trimmed {:.3f}s of silence from the start and {:.3f}s "
+ "from the end",
+ rel, lead, tail);
+ }
+ }
+
+ const int64_t loopStart =
+ sound["Loop"] ? static_cast<int64_t>(sound["Loop"]["Start"].as<uint32_t>()) : -1;
+ const VadpcmSample enc = EncodeVadpcm(pcm, 1, loopStart);
+ book.order = enc.order;
+ book.npredictors = enc.npredictors;
+ book.book = enc.book;
+ loopState = enc.loopState;
+ adpcm = enc.frames;
+ encoded++;
+ SPDLOG_INFO("Soundfont: encoded {} from {} -- {} samples, {:.1f} dB", rel, audio.filename().string(),
+ pcm.size(), enc.snrDb);
+
+ const bool fit = sound["FitEnvelope"] ? sound["FitEnvelope"].as<bool>() : true;
+ if (fit && sound["Envelope"]) {
+ const auto envelope = sound["Envelope"];
+ const int64_t attack = envelope["AttackTime"].as<int64_t>();
+ const int64_t release = envelope["ReleaseTime"].as<int64_t>();
+ const int64_t was = envelope["DecayTime"].as<int64_t>();
+ const int64_t needed = static_cast<int64_t>(1000000.0 * static_cast<double>(pcm.size()) /
+ static_cast<double>(sampleRate));
+ const int64_t decay = std::max<int64_t>(0, needed - attack - release);
+ if (decay != was) {
+ fittedDecay = static_cast<int32_t>(decay);
+ SPDLOG_INFO("Soundfont: {} envelope fitted to the new sample -- DecayTime {} -> {} "
+ "({:.3f}s)",
+ rel, was, decay, static_cast<double>(needed) / 1e6);
+ }
+ if (needed < attack + release) {
+ SPDLOG_WARN("Soundfont: {} is shorter than its attack and release together; it will be "
+ "clipped whatever the decay",
+ rel);
+ }
+ }
+ }
+
+ const uint32_t base = static_cast<uint32_t>((font->mSampleData.size() + 7) & ~static_cast<size_t>(7));
+ font->mSampleData.resize(base);
+ font->mSampleData.insert(font->mSampleData.end(), adpcm.begin(), adpcm.end());
+
+ const uint32_t bookKey = nextKey++;
+ font->mBooks.emplace(bookKey, book);
+
+ uint32_t loopKey = 0;
+ if (sound["Loop"]) {
+ SoundfontLoop loop;
+ loop.start = sound["Loop"]["Start"].as<uint32_t>();
+ loop.end = sound["Loop"]["End"].as<uint32_t>();
+ loop.count = sound["Loop"]["Count"].as<uint32_t>();
+ loop.state = loopState.size() == 16 ? loopState : std::vector<int16_t>(16, 0);
+ loopKey = nextKey++;
+ font->mLoops.emplace(loopKey, loop);
+ }
+
+ SoundfontWave wave;
+ wave.base = base;
+ wave.len = static_cast<int32_t>(adpcm.size());
+ wave.type = 0;
+ wave.bookOffset = bookKey;
+ wave.loopOffset = loopKey;
+ const uint32_t waveKey = nextKey++;
+ font->mWaves.emplace(waveKey, wave);
+
+ SoundfontEnvelope env;
+ if (sound["Envelope"]) {
+ const auto envelope = sound["Envelope"];
+ env.attackTime = envelope["AttackTime"].as<int32_t>();
+ env.attackVolume = static_cast<uint8_t>(envelope["AttackVolume"].as<int>());
+ env.decayTime = fittedDecay.value_or(envelope["DecayTime"].as<int32_t>());
+ env.decayVolume = static_cast<uint8_t>(envelope["DecayVolume"].as<int>());
+ env.releaseTime = envelope["ReleaseTime"].as<int32_t>();
+ }
+ const uint32_t envKey = nextKey++;
+ font->mEnvelopes.emplace(envKey, env);
+ SoundfontKeyMap keymap;
+ const int chain = Field(sound, "ChainNext", 0);
+ keymap.velocityMin = static_cast<uint8_t>(chain & 0xFF);
+ keymap.velocityMax = static_cast<uint8_t>(Field(sound, "ChainDelayFrames", 0));
+ keymap.keyMin =
+ static_cast<uint8_t>((Field(sound, "VolumeGroup", 0) & 0x3F) | (((chain >> 8) & 0x03) << 6));
+ keymap.keyMax =
+ static_cast<uint8_t>((Field(sound, "ReverbSend", 0) & 0x0F) | (Field(sound, "KeyMaxFlags", 0) & 0xF0));
+ keymap.keyBase = static_cast<uint8_t>(Field(sound, "KeyBase", 60));
+ keymap.detune = static_cast<int8_t>(Field(sound, "Detune", 0));
+ const uint32_t kmKey = nextKey++;
+ font->mKeyMaps.emplace(kmKey, keymap);
+ SoundfontSound record;
+ record.envelopeOffset = envKey;
+ record.keyMapOffset = kmKey;
+ record.waveOffset = waveKey;
+ record.samplePan = static_cast<uint8_t>(Field(sound, "SamplePan", 64));
+ record.sampleVolume = static_cast<uint8_t>(Field(sound, "SampleVolume", 127));
+ record.flags = static_cast<uint8_t>(Field(sound, "Flags", 0));
+ const uint32_t soundKey = nextKey++;
+ font->mSounds.emplace(soundKey, record);
+
+ soundByPath.emplace(rel, soundKey);
+ inst.soundOffsets.push_back(soundKey);
+ }
+
+ const uint32_t instKey = nextKey++;
+ font->mInstruments.emplace(instKey, inst);
+ bank.instrumentOffsets.push_back(instKey);
+ }
+
+ const uint32_t bankKey = nextKey++;
+ font->mBanks.emplace(bankKey, bank);
+ font->mBankOffsets.push_back(bankKey);
+ font->mRevision = 0x4231;
+
+ SPDLOG_INFO("Soundfont: rebuilt {} sounds from modding files -- {} re-encoded, {} verbatim", soundByPath.size(),
+ encoded, verbatim);
+ return font;
+}
+
+} // namespace BK64
diff --git a/src/factories/bk64/SoundfontFactory.h b/src/factories/bk64/SoundfontFactory.h
new file mode 100644
index 0000000..7808478
--- /dev/null
+++ b/src/factories/bk64/SoundfontFactory.h
@@ -0,0 +1,145 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+#include "types/RawBuffer.h"
+
+#include <cstdint>
+#include <map>
+#include <string>
+#include <vector>
+
+namespace BK64 {
+
+struct SoundfontBook {
+ int32_t order = 0;
+ int32_t npredictors = 0;
+ std::vector<int16_t> book;
+};
+
+struct SoundfontLoop {
+ uint32_t start = 0;
+ uint32_t end = 0;
+ uint32_t count = 0;
+ std::vector<int16_t> state;
+};
+
+struct SoundfontEnvelope {
+ int32_t attackTime = 0;
+ int32_t decayTime = 0;
+ int32_t releaseTime = 0;
+ uint8_t attackVolume = 0;
+ uint8_t decayVolume = 0;
+};
+
+struct SoundfontKeyMap {
+ uint8_t velocityMin = 0;
+ uint8_t velocityMax = 0;
+ uint8_t keyMin = 0;
+ uint8_t keyMax = 0;
+ uint8_t keyBase = 0;
+ int8_t detune = 0;
+};
+
+struct SoundfontWave {
+ uint32_t base = 0;
+ int32_t len = 0;
+ uint8_t type = 0;
+ uint8_t flags = 0;
+ uint32_t loopOffset = 0;
+ uint32_t bookOffset = 0;
+};
+
+struct SoundfontSound {
+ uint32_t envelopeOffset = 0;
+ uint32_t keyMapOffset = 0;
+ uint32_t waveOffset = 0;
+ uint8_t samplePan = 0;
+ uint8_t sampleVolume = 0;
+ uint8_t flags = 0;
+};
+
+struct SoundfontInstrument {
+ uint8_t volume = 0;
+ uint8_t pan = 0;
+ uint8_t priority = 0;
+ uint8_t flags = 0;
+ uint8_t tremType = 0;
+ uint8_t tremRate = 0;
+ uint8_t tremDepth = 0;
+ uint8_t tremDelay = 0;
+ uint8_t vibType = 0;
+ uint8_t vibRate = 0;
+ uint8_t vibDepth = 0;
+ uint8_t vibDelay = 0;
+ int16_t bendRange = 0;
+ std::vector<uint32_t> soundOffsets;
+};
+
+struct SoundfontBank {
+ uint8_t flags = 0;
+ uint8_t pad = 0;
+ int32_t sampleRate = 0;
+ uint32_t percussionOffset = 0;
+ std::vector<uint32_t> instrumentOffsets;
+};
+
+struct SoundfontImage {
+ std::vector<uint8_t> bytes;
+ std::vector<uint8_t> covered;
+};
+
+class SoundfontData : public RawBuffer {
+ public:
+ int16_t mRevision = 0;
+ std::vector<uint32_t> mBankOffsets;
+ std::map<uint32_t, SoundfontBank> mBanks;
+ std::map<uint32_t, SoundfontInstrument> mInstruments;
+ std::map<uint32_t, SoundfontSound> mSounds;
+ std::map<uint32_t, SoundfontEnvelope> mEnvelopes;
+ std::map<uint32_t, SoundfontKeyMap> mKeyMaps;
+ std::map<uint32_t, SoundfontWave> mWaves;
+ std::map<uint32_t, SoundfontBook> mBooks;
+ std::map<uint32_t, SoundfontLoop> mLoops;
+ std::vector<uint8_t> mSampleData;
+
+ explicit SoundfontData(std::vector<uint8_t> bytes) : RawBuffer(bytes) {
+ }
+
+ uint64_t SampleDataEnd() const;
+ SoundfontImage Serialize() const;
+ std::vector<uint8_t> SampleBytes(const SoundfontWave& wave) const;
+};
+
+struct SoundfontModFile {
+ std::string yaml;
+ std::vector<uint8_t> raw;
+ std::vector<uint8_t> wav;
+};
+
+SoundfontData ParseSoundfont(const uint8_t* ctl, size_t ctlSize);
+
+class SoundfontBinaryExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName,
+ YAML::Node& node, std::string* replacement) override;
+};
+
+class SoundfontModdingExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName,
+ YAML::Node& node, std::string* replacement) override;
+};
+
+class SoundfontFactory : public BaseFactory {
+ public:
+ std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ std::optional<std::shared_ptr<IParsedData>> parse_modding(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ inline std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
+ return { REGISTER(Binary, SoundfontBinaryExporter) REGISTER(Modding, SoundfontModdingExporter) };
+ }
+ bool SupportModdedAssets() override {
+ return true;
+ }
+};
+
+uint32_t LocateSoundfontCtl(const std::vector<uint8_t>& rom, uint32_t ctlOffset, uint32_t ctlSize);
+
+} // namespace BK64
diff --git a/src/factories/bk64/SoundfontTblFactory.cpp b/src/factories/bk64/SoundfontTblFactory.cpp
deleted file mode 100644
index 9b4d08e..0000000
--- a/src/factories/bk64/SoundfontTblFactory.cpp
+++ /dev/null
@@ -1,210 +0,0 @@
-#include "SoundfontTblFactory.h"
-#include "BKByteUtils.h"
-
-#include "Companion.h"
-#include "spdlog/spdlog.h"
-#include "types/RawBuffer.h"
-#include "utils/Decompressor.h"
-
-#include <cstdint>
-#include <cstring>
-#include <stdexcept>
-#include <unordered_set>
-
-namespace BK64 {
-
-namespace {
-
-// AL_BANK_VERSION magic
-constexpr uint16_t kAlBankRevision = 0x4231;
-
-// Walk an N64 ALBankFile binary (big-endian, 32-bit offsets relative to ctl
-// start) and return max(wavetable.base + wavetable.len) over every wavetable
-// reachable from the file's banks. Throws on any structural inconsistency —
-// silent fallbacks would just produce a wrong-size tbl and reproduce the bug
-// we're fixing.
-//
-// ALBankFile: [0] s16 revision, [2] s16 bankCount, [4+] u32 bankOffsets[]
-// ALBank: [0] s16 instCount, [4] s32 sampleRate, [8] u32 percussion,
-// [12+] u32 instOffsets[]
-// ALInstrument:[14] s16 soundCount, [16+] u32 soundOffsets[]
-// ALSound: [8] u32 wavetable
-// ALWaveTable: [0] u32 base, [4] s32 len
-size_t ComputeTblSize(const uint8_t* ctl, size_t ctlSize, uint32_t ctlRomOffset, bool quiet = false) {
- auto check = [&](uint32_t off, size_t need, const char* what) {
- if ((size_t)off + need > ctlSize) {
- if (!quiet) {
- SPDLOG_ERROR(
- "SoundfontTblFactory: ctl walk OOB reading {} (ctl@0x{:X} size=0x{:X} off=0x{:X} need=0x{:X})",
- what, ctlRomOffset, ctlSize, off, need);
- }
- throw std::runtime_error("SoundfontTblFactory: ctl walk OOB");
- }
- };
-
- check(0, 4, "header");
- int16_t bankCount = ReadS16BE(ctl + 2);
- if (bankCount <= 0) {
- throw std::runtime_error("SoundfontTblFactory: bankCount <= 0");
- }
-
- uint64_t maxEnd = 0;
- std::unordered_set<uint32_t> seenWt;
-
- for (int b = 0; b < bankCount; b++) {
- check(4 + b * 4, 4, "bankArray entry");
- uint32_t bankOff = ReadU32BE(ctl + 4 + b * 4);
- if (bankOff == 0) {
- continue;
- }
-
- check(bankOff, 12, "ALBank header");
- int16_t instCount = ReadS16BE(ctl + bankOff + 0);
- uint32_t percOff = ReadU32BE(ctl + bankOff + 8);
-
- auto visitInst = [&](uint32_t instOff) {
- if (instOff == 0) {
- return;
- }
- check(instOff, 16, "ALInstrument header");
- int16_t soundCount = ReadS16BE(ctl + instOff + 14);
- for (int s = 0; s < soundCount; s++) {
- check(instOff + 16 + s * 4, 4, "ALInstrument soundArray entry");
- uint32_t sndOff = ReadU32BE(ctl + instOff + 16 + s * 4);
- if (sndOff == 0) {
- continue;
- }
- check(sndOff, 12, "ALSound header");
- uint32_t wtOff = ReadU32BE(ctl + sndOff + 8);
- if (wtOff == 0 || !seenWt.insert(wtOff).second) {
- continue;
- }
- check(wtOff, 8, "ALWaveTable header");
- uint32_t base = ReadU32BE(ctl + wtOff + 0);
- int32_t len = ReadS32BE(ctl + wtOff + 4);
- if (len < 0) {
- throw std::runtime_error("SoundfontTblFactory: negative wavetable len");
- }
- uint64_t end = (uint64_t)base + (uint64_t)len;
- if (end > maxEnd) {
- maxEnd = end;
- }
- }
- };
-
- visitInst(percOff);
- for (int i = 0; i < instCount; i++) {
- check(bankOff + 12 + i * 4, 4, "ALBank instArray entry");
- visitInst(ReadU32BE(ctl + bankOff + 12 + i * 4));
- }
- }
-
- if (maxEnd == 0) {
- throw std::runtime_error("SoundfontTblFactory: ctl referenced no wavetables");
- }
-
- // Round up to BK's 16-byte ROM alignment.
- return (size_t)((maxEnd + 0xF) & ~(uint64_t)0xF);
-}
-
-bool ValidateCtl(const uint8_t* ctl, size_t ctlSize) {
- if (ctlSize < 8) {
- return false;
- }
- if (ReadU16BE(ctl) != kAlBankRevision) {
- return false;
- }
- int16_t bankCount = ReadS16BE(ctl + 2);
- if (bankCount <= 0 || bankCount > 16) {
- return false;
- }
- uint32_t bank0 = ReadU32BE(ctl + 4);
- if (bank0 != 0 && (bank0 < 4u + 4u * bankCount || bank0 >= ctlSize)) {
- return false;
- }
- try {
- ComputeTblSize(ctl, ctlSize, 0, /*quiet=*/true);
- } catch (...) {
- return false;
- }
- return true;
-}
-
-} // namespace
-
-uint32_t LocateSoundfontCtl(const std::vector<uint8_t>& rom, uint32_t ctlOffset, uint32_t ctlSize) {
- if ((size_t)ctlOffset + ctlSize <= rom.size() && ValidateCtl(rom.data() + ctlOffset, ctlSize)) {
- return ctlOffset;
- }
-
- // Romhacks can shift the whole audio region; hunt
- // for the real ALBankFile header instead of failing.
- std::vector<uint32_t> candidates;
- for (size_t i = 0; i + 8 <= rom.size(); i += 8) {
- if (rom[i] != 0x42 || rom[i + 1] != 0x31) {
- continue;
- }
- size_t avail = std::min<size_t>(ctlSize, rom.size() - i);
- if (ValidateCtl(rom.data() + i, avail)) {
- candidates.push_back((uint32_t)i);
- }
- }
-
- if (candidates.empty()) {
- SPDLOG_ERROR("SoundfontCtl: no valid ALBankFile found anywhere in ROM (vanilla ctl@0x{:X} size=0x{:X})",
- ctlOffset, ctlSize);
- throw std::runtime_error("SoundfontCtl: soundfont ctl not found in ROM");
- }
-
- uint32_t best = candidates[0];
- for (uint32_t c : candidates) {
- auto dist = [&](uint32_t off) { return off > ctlOffset ? off - ctlOffset : ctlOffset - off; };
- if (dist(c) < dist(best)) {
- best = c;
- }
- }
-
- SPDLOG_WARN("SoundfontCtl: ctl not at vanilla offset 0x{:X}; relocated to 0x{:X} (delta 0x{:X}, {} candidates)",
- ctlOffset, best, (uint32_t)(best - ctlOffset), candidates.size());
- return best;
-}
-
-std::optional<std::shared_ptr<IParsedData>> SoundfontCtlFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) {
- const auto offset = GetSafeNode<uint32_t>(node, "offset");
- const auto size = GetSafeNode<uint32_t>(node, "size");
-
- const uint32_t realOffset = LocateSoundfontCtl(buffer, offset, size);
- if ((size_t)realOffset + size > buffer.size()) {
- throw std::runtime_error("SoundfontCtlFactory: ctl exceeds ROM size");
- }
-
- return std::make_shared<RawBuffer>(buffer.data() + realOffset, size);
-}
-
-std::optional<std::shared_ptr<IParsedData>> SoundfontTblFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) {
- const auto tblOffset = GetSafeNode<uint32_t>(node, "offset");
- const auto ctlOffset = GetSafeNode<uint32_t>(node, "ctl_offset");
- const auto ctlSize = GetSafeNode<uint32_t>(node, "ctl_size");
-
- // The tbl sits right after the ctl, so it shifts by
- // the same delta when a romhack relocates the audio region.
- const uint32_t realCtlOffset = LocateSoundfontCtl(buffer, ctlOffset, ctlSize);
- const uint32_t realTblOffset = tblOffset + (realCtlOffset - ctlOffset);
-
- if ((size_t)realCtlOffset + ctlSize > buffer.size()) {
- throw std::runtime_error("SoundfontTblFactory: ctl_offset + ctl_size exceeds ROM size");
- }
-
- const size_t tblSize = ComputeTblSize(buffer.data() + realCtlOffset, ctlSize, realCtlOffset);
-
- if ((size_t)realTblOffset + tblSize > buffer.size()) {
- throw std::runtime_error("SoundfontTblFactory: computed tbl size exceeds ROM bounds");
- }
-
- SPDLOG_INFO("SoundfontTbl: tbl@0x{:X} size 0x{:X} (computed from ctl@0x{:X})", realTblOffset, tblSize,
- realCtlOffset);
-
- return std::make_shared<RawBuffer>(buffer.data() + realTblOffset, tblSize);
-}
-
-} // namespace BK64
diff --git a/src/factories/bk64/SoundfontTblFactory.h b/src/factories/bk64/SoundfontTblFactory.h
deleted file mode 100644
index b1979a2..0000000
--- a/src/factories/bk64/SoundfontTblFactory.h
+++ /dev/null
@@ -1,27 +0,0 @@
-#pragma once
-
-#include "factories/BaseFactory.h"
-#include "factories/BlobFactory.h"
-
-namespace BK64 {
-class SoundfontTblFactory : public BaseFactory {
- public:
- std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
- inline std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
- return { REGISTER(Header, BlobHeaderExporter) REGISTER(Binary, BlobBinaryExporter)
- REGISTER(Code, BlobCodeExporter) };
- }
-};
-
-class SoundfontCtlFactory : public BaseFactory {
- public:
- std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
- inline std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
- return { REGISTER(Header, BlobHeaderExporter) REGISTER(Binary, BlobBinaryExporter)
- REGISTER(Code, BlobCodeExporter) };
- }
-};
-
-uint32_t LocateSoundfontCtl(const std::vector<uint8_t>& rom, uint32_t ctlOffset, uint32_t ctlSize);
-
-} // namespace BK64
diff --git a/src/factories/bk64/VadpcmEncode.cpp b/src/factories/bk64/VadpcmEncode.cpp
new file mode 100644
index 0000000..608af5f
--- /dev/null
+++ b/src/factories/bk64/VadpcmEncode.cpp
@@ -0,0 +1,303 @@
+#include "VadpcmEncode.h"
+
+#define DR_WAV_IMPLEMENTATION
+#define DR_MP3_IMPLEMENTATION
+#define DR_FLAC_IMPLEMENTATION
+#include "dr_flac.h"
+#include "dr_mp3.h"
+#include "dr_wav.h"
+
+#include <algorithm>
+#include <cctype>
+#include <cmath>
+#include <cstring>
+
+namespace BK64 {
+namespace {
+
+constexpr int kFrameSamples = 16;
+constexpr int kOrder = 2;
+
+int16_t Clamp16(int32_t value) {
+ return static_cast<int16_t>(value < -32768 ? -32768 : (value > 32767 ? 32767 : value));
+}
+
+bool EndsWith(const std::string& text, const char* suffix) {
+ const size_t length = std::strlen(suffix);
+ if (text.size() < length) {
+ return false;
+ }
+ const char* at = text.c_str() + text.size() - length;
+ for (size_t i = 0; i < length; i++) {
+ const char left = static_cast<char>(std::tolower(static_cast<unsigned char>(at[i])));
+ const char right = static_cast<char>(std::tolower(static_cast<unsigned char>(suffix[i])));
+ if (left != right) {
+ return false;
+ }
+ }
+ return true;
+}
+
+std::vector<int16_t> ToMonoAtRate(const float* interleaved, uint64_t frames, uint32_t channels, uint32_t sourceRate,
+ int rate) {
+ std::vector<float> mono(static_cast<size_t>(frames));
+ for (uint64_t i = 0; i < frames; i++) {
+ float sum = 0.0f;
+ for (uint32_t c = 0; c < channels; c++) {
+ sum += interleaved[i * channels + c];
+ }
+ mono[static_cast<size_t>(i)] = sum / static_cast<float>(channels);
+ }
+
+ if (sourceRate == static_cast<uint32_t>(rate) || mono.empty()) {
+ std::vector<int16_t> out(mono.size());
+ for (size_t i = 0; i < mono.size(); i++) {
+ out[i] = Clamp16(static_cast<int32_t>(mono[i] * 32767.0f));
+ }
+ return out;
+ }
+
+ const double ratio = static_cast<double>(sourceRate) / static_cast<double>(rate);
+ const size_t count = static_cast<size_t>(static_cast<double>(mono.size()) / ratio);
+ std::vector<int16_t> out(count);
+ for (size_t i = 0; i < count; i++) {
+ const double at = static_cast<double>(i) * ratio;
+ const size_t idx = static_cast<size_t>(at);
+ const double frac = at - static_cast<double>(idx);
+ const float first = mono[std::min(idx, mono.size() - 1)];
+ const float second = mono[std::min(idx + 1, mono.size() - 1)];
+ out[i] = Clamp16(static_cast<int32_t>((first + (second - first) * static_cast<float>(frac)) * 32767.0f));
+ }
+ return out;
+}
+
+void FitOrder2(const std::vector<int16_t>& pcm, double& a1, double& a2) {
+ double r[3] = { 0.0, 0.0, 0.0 };
+ for (int lag = 0; lag <= kOrder; lag++) {
+ double sum = 0.0;
+ for (size_t i = lag; i < pcm.size(); i++) {
+ sum += static_cast<double>(pcm[i]) * static_cast<double>(pcm[i - lag]);
+ }
+ r[lag] = sum;
+ }
+ if (r[0] <= 0.0) {
+ a1 = 0.0;
+ a2 = 0.0;
+ return;
+ }
+ r[0] *= 1.0001;
+
+ const double k1 = r[1] / r[0];
+ double err = r[0] * (1.0 - k1 * k1);
+ if (err <= 0.0) {
+ a1 = k1;
+ a2 = 0.0;
+ return;
+ }
+ const double k2 = (r[2] - k1 * r[1]) / err;
+ a1 = k1 - k2 * k1;
+ a2 = k2;
+}
+
+void BuildBookRows(double a1, double a2, int16_t* row0, int16_t* row1) {
+ double p0 = 1.0, p1 = 0.0;
+ double q0 = 0.0, q1 = 1.0;
+ for (int i = 0; i < 8; i++) {
+ const double n0 = a1 * p1 + a2 * p0;
+ const double n1 = a1 * q1 + a2 * q0;
+ p0 = p1;
+ p1 = n0;
+ q0 = q1;
+ q1 = n1;
+ row0[i] = Clamp16(static_cast<int32_t>(std::lround(p1 * 2048.0)));
+ row1[i] = Clamp16(static_cast<int32_t>(std::lround(q1 * 2048.0)));
+ }
+}
+
+void DecodeFrame(const uint8_t* frame, const int16_t* tbl0, const int16_t* tbl1, int16_t* out) {
+ const int shift = frame[0] >> 4;
+ const uint8_t* nibbles = frame + 1;
+ for (int half = 0; half < 2; half++) {
+ int16_t ins[8];
+ const int16_t prev1 = out[-1];
+ const int16_t prev2 = out[-2];
+ for (int j = 0; j < 4; j++) {
+ ins[j * 2] = static_cast<int16_t>((((*nibbles >> 4) << 28) >> 28) << shift);
+ ins[j * 2 + 1] = static_cast<int16_t>((((*nibbles++ & 0xF) << 28) >> 28) << shift);
+ }
+ for (int j = 0; j < 8; j++) {
+ int32_t acc = tbl0[j] * prev2 + tbl1[j] * prev1 + (ins[j] << 11);
+ for (int k = 0; k < j; k++) {
+ acc += tbl1[(j - k) - 1] * ins[k];
+ }
+ *out++ = Clamp16(acc >> 11);
+ }
+ }
+}
+
+int64_t TryShift(const int16_t* want, const int16_t* history, const int16_t* tbl0, const int16_t* tbl1, int shift,
+ int predictor, uint8_t* frame, int16_t* got) {
+ std::memset(frame, 0, 9);
+ frame[0] = static_cast<uint8_t>((shift << 4) | (predictor & 0xF));
+
+ int16_t work[18];
+ work[0] = history[0];
+ work[1] = history[1];
+
+ for (int half = 0; half < 2; half++) {
+ int16_t* out = work + 2 + half * 8;
+ const int16_t prev1 = out[-1];
+ const int16_t prev2 = out[-2];
+ int16_t ins[8] = { 0 };
+
+ for (int j = 0; j < 8; j++) {
+ int32_t predicted = tbl0[j] * prev2 + tbl1[j] * prev1;
+ for (int k = 0; k < j; k++) {
+ predicted += tbl1[(j - k) - 1] * ins[k];
+ }
+ const int32_t target = (static_cast<int32_t>(want[half * 8 + j]) << 11) - predicted;
+ const double step = 2048.0 * static_cast<double>(1 << shift);
+ int nibble = static_cast<int>(std::lround(static_cast<double>(target) / step));
+ nibble = std::max(-8, std::min(7, nibble));
+
+ ins[j] = static_cast<int16_t>(nibble << shift);
+ out[j] = Clamp16((predicted + (static_cast<int32_t>(ins[j]) << 11)) >> 11);
+
+ uint8_t& byte = frame[1 + half * 4 + j / 2];
+ if (j % 2 == 0) {
+ byte = static_cast<uint8_t>((byte & 0x0F) | ((nibble & 0xF) << 4));
+ } else {
+ byte = static_cast<uint8_t>((byte & 0xF0) | (nibble & 0xF));
+ }
+ }
+ }
+
+ int64_t error = 0;
+ for (int i = 0; i < kFrameSamples; i++) {
+ const int64_t diff = static_cast<int64_t>(work[2 + i]) - static_cast<int64_t>(want[i]);
+ error += diff * diff;
+ got[i] = work[2 + i];
+ }
+ return error;
+}
+
+} // namespace
+
+bool DecodeAudioFile(const std::string& path, int rate, std::vector<int16_t>& out, std::string& error) {
+ unsigned int channels = 0;
+ unsigned int sourceRate = 0;
+ drwav_uint64 frames = 0;
+ float* samples = nullptr;
+
+ if (EndsWith(path, ".wav")) {
+ samples = drwav_open_file_and_read_pcm_frames_f32(path.c_str(), &channels, &sourceRate, &frames, nullptr);
+ } else if (EndsWith(path, ".mp3")) {
+ drmp3_config cfg;
+ std::memset(&cfg, 0, sizeof(cfg));
+ samples = drmp3_open_file_and_read_pcm_frames_f32(path.c_str(), &cfg, &frames, nullptr);
+ channels = cfg.channels;
+ sourceRate = cfg.sampleRate;
+ } else if (EndsWith(path, ".flac")) {
+ samples = drflac_open_file_and_read_pcm_frames_f32(path.c_str(), &channels, &sourceRate, &frames, nullptr);
+ } else {
+ error = "unsupported audio format (use .wav, .mp3 or .flac)";
+ return false;
+ }
+
+ if (samples == nullptr || frames == 0 || channels == 0 || sourceRate == 0) {
+ error = "could not decode " + path;
+ return false;
+ }
+
+ out = ToMonoAtRate(samples, frames, channels, sourceRate, rate);
+ drwav_free(samples, nullptr);
+ return !out.empty();
+}
+
+VadpcmSample EncodeVadpcm(const std::vector<int16_t>& pcm, int npredictors, int64_t loopStart) {
+ VadpcmSample result;
+ result.order = kOrder;
+ result.npredictors = std::max(1, npredictors);
+
+ double a1 = 0.0, a2 = 0.0;
+ FitOrder2(pcm, a1, a2);
+
+ result.book.assign(static_cast<size_t>(result.npredictors) * 16, 0);
+ for (int p = 0; p < result.npredictors; p++) {
+ const double damp = 1.0 - static_cast<double>(p) / static_cast<double>(result.npredictors * 2);
+ BuildBookRows(a1 * damp, a2 * damp, result.book.data() + p * 16, result.book.data() + p * 16 + 8);
+ }
+
+ const size_t frameCount = (pcm.size() + kFrameSamples - 1) / kFrameSamples;
+ result.frames.assign(frameCount * 9, 0);
+
+ int16_t history[2] = { 0, 0 };
+ for (size_t f = 0; f < frameCount; f++) {
+ int16_t want[kFrameSamples] = { 0 };
+ for (int i = 0; i < kFrameSamples; i++) {
+ const size_t at = f * kFrameSamples + i;
+ want[i] = at < pcm.size() ? pcm[at] : 0;
+ }
+
+ if (loopStart >= 0 && static_cast<size_t>(loopStart) == f * kFrameSamples) {
+ result.loopState.assign(16, 0);
+ result.loopState[14] = history[0];
+ result.loopState[15] = history[1];
+ }
+
+ uint8_t best[9] = { 0 };
+ int16_t bestGot[kFrameSamples] = { 0 };
+ int64_t bestError = INT64_MAX;
+
+ for (int p = 0; p < result.npredictors; p++) {
+ const int16_t* tbl0 = result.book.data() + p * 16;
+ const int16_t* tbl1 = tbl0 + 8;
+ for (int shift = 0; shift <= 12; shift++) {
+ uint8_t frame[9] = { 0 };
+ int16_t got[kFrameSamples] = { 0 };
+ const int64_t err = TryShift(want, history, tbl0, tbl1, shift, p, frame, got);
+ if (err < bestError) {
+ bestError = err;
+ std::memcpy(best, frame, 9);
+ std::memcpy(bestGot, got, sizeof(got));
+ }
+ }
+ }
+
+ std::memcpy(result.frames.data() + f * 9, best, 9);
+ history[0] = bestGot[kFrameSamples - 2];
+ history[1] = bestGot[kFrameSamples - 1];
+ }
+
+ if (loopStart >= 0 && result.loopState.empty()) {
+ result.loopState.assign(16, 0);
+ }
+
+ double signal = 0.0;
+ double noise = 0.0;
+ {
+ int16_t history[2] = { 0, 0 };
+ std::vector<int16_t> decoded(kFrameSamples + 2, 0);
+ for (size_t f = 0; f < frameCount; f++) {
+ const uint8_t* frame = result.frames.data() + f * 9;
+ const int predictor = frame[0] & 0xF;
+ const int16_t* tbl0 = result.book.data() + predictor * 16;
+ decoded[0] = history[0];
+ decoded[1] = history[1];
+ DecodeFrame(frame, tbl0, tbl0 + 8, decoded.data() + 2);
+ for (int i = 0; i < kFrameSamples; i++) {
+ const size_t at = f * kFrameSamples + i;
+ const double want = at < pcm.size() ? static_cast<double>(pcm[at]) : 0.0;
+ const double got = static_cast<double>(decoded[2 + i]);
+ signal += want * want;
+ noise += (want - got) * (want - got);
+ }
+ history[0] = decoded[kFrameSamples];
+ history[1] = decoded[kFrameSamples + 1];
+ }
+ }
+ result.snrDb = (noise > 0.0 && signal > 0.0) ? 10.0 * std::log10(signal / noise) : 99.0;
+ return result;
+}
+
+} // namespace BK64
diff --git a/src/factories/bk64/VadpcmEncode.h b/src/factories/bk64/VadpcmEncode.h
new file mode 100644
index 0000000..213f2b7
--- /dev/null
+++ b/src/factories/bk64/VadpcmEncode.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+namespace BK64 {
+
+struct VadpcmSample {
+ int32_t order = 2;
+ int32_t npredictors = 1;
+ std::vector<int16_t> book;
+ std::vector<uint8_t> frames;
+ std::vector<int16_t> loopState;
+ double snrDb = 0.0;
+};
+
+bool DecodeAudioFile(const std::string& path, int rate, std::vector<int16_t>& out, std::string& error);
+VadpcmSample EncodeVadpcm(const std::vector<int16_t>& pcm, int npredictors, int64_t loopStart);
+
+} // namespace BK64