diff options
| author | Jeod <47716344+JeodC@users.noreply.github.com> | 2026-08-23 20:57:04 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-23 18:57:04 -0600 |
| commit | a93519e66eabb62005d278cce46b4d593bceed83 (patch) | |
| tree | 34f25bc20de8d330c6ed5a4b8ef72618b0f7789a | |
| parent | e9faa26c74a839d3c7995dfba7cad0fe6cd4af29 (diff) | |
Fully parse bk64 soundfont for modding (#256)
* Fully parse bk64 soundfont for modding
* Match N64MidiTool loop name convention
| -rw-r--r-- | CMakeLists.txt | 17 | ||||
| -rw-r--r-- | run-clang-format.ps1 | 6 | ||||
| -rw-r--r-- | src/Companion.cpp | 11 | ||||
| -rw-r--r-- | src/Companion.h | 4 | ||||
| -rw-r--r-- | src/factories/ResourceType.h | 3 | ||||
| -rw-r--r-- | src/factories/bk64/BKAssetFactory.cpp | 2 | ||||
| -rw-r--r-- | src/factories/bk64/MusicFactory.cpp | 910 | ||||
| -rw-r--r-- | src/factories/bk64/MusicFactory.h | 68 | ||||
| -rw-r--r-- | src/factories/bk64/SoundfontFactory.cpp | 1367 | ||||
| -rw-r--r-- | src/factories/bk64/SoundfontFactory.h | 145 | ||||
| -rw-r--r-- | src/factories/bk64/SoundfontTblFactory.cpp | 210 | ||||
| -rw-r--r-- | src/factories/bk64/SoundfontTblFactory.h | 27 | ||||
| -rw-r--r-- | src/factories/bk64/VadpcmEncode.cpp | 303 | ||||
| -rw-r--r-- | src/factories/bk64/VadpcmEncode.h | 21 |
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 |
