summaryrefslogtreecommitdiff
path: root/src/factories/pm64/AudioFactory.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/factories/pm64/AudioFactory.cpp')
-rw-r--r--src/factories/pm64/AudioFactory.cpp614
1 files changed, 614 insertions, 0 deletions
diff --git a/src/factories/pm64/AudioFactory.cpp b/src/factories/pm64/AudioFactory.cpp
new file mode 100644
index 0000000..a4df6bf
--- /dev/null
+++ b/src/factories/pm64/AudioFactory.cpp
@@ -0,0 +1,614 @@
+#include "AudioFactory.h"
+#include "Companion.h"
+#include "spdlog/spdlog.h"
+#include <set>
+
+// SBN file format constants
+#define SBN_SIGNATURE 0x53424E20 // 'SBN '
+#define INIT_SIGNATURE 0x494E4954 // 'INIT'
+#define BGM_SIGNATURE 0x42474D20 // 'BGM '
+#define SEF_SIGNATURE 0x53454620 // 'SEF '
+#define PER_SIGNATURE 0x50455220 // 'PER '
+#define PRG_SIGNATURE 0x50524720 // 'PRG '
+#define BK_SIGNATURE 0x424B // 'BK'
+#define MSEQ_SIGNATURE 0x4D534551 // 'MSEQ'
+
+// Audio file format types (upper byte of SBNFileEntry.data)
+// NOTE: PER and PRG share format 0x40 with MSEQ; distinguished by file signature
+#define AU_FMT_BGM 0x10
+#define AU_FMT_SEF 0x20
+#define AU_FMT_BK 0x30
+#define AU_FMT_MSEQ 0x40
+
+// Structure sizes
+#define SBN_HEADER_SIZE 0x40
+#define SBN_FILE_ENTRY_SIZE 8
+#define INIT_HEADER_SIZE 0x20
+#define INIT_SONG_ENTRY_SIZE 8
+#define INIT_BANK_ENTRY_SIZE 4
+#define BGM_HEADER_SIZE 0x24
+#define BK_HEADER_SIZE 0x40
+#define SEF_HEADER_SIZE 0x22
+#define MSEQ_HEADER_SIZE 0x18
+#define PER_HEADER_SIZE 0x10
+
+// SEF section entry counts (from game code)
+#define SEF_SECTION_0_3_ENTRIES 0xC0 // 192 entries for sections 0-3
+#define SEF_SECTION_4_7_ENTRIES 0x40 // 64 entries for sections 4-7
+#define SEF_EXTRA_ENTRIES 0x140 // 320 entries for extra section
+
+// BGMDrumInfo size
+#define BGM_DRUM_INFO_SIZE 0x0C
+// BGMInstrumentInfo size
+#define BGM_INSTRUMENT_INFO_SIZE 0x08
+// PEREntry size (12 drums)
+#define PER_ENTRY_SIZE (12 * BGM_DRUM_INFO_SIZE) // 0x90
+
+// Helper to check bounds
+#define CHECK_BOUNDS(offset, size, totalSize) ((offset) + (size) <= (totalSize))
+
+static void ByteSwapAudioData(uint8_t* data, size_t size) {
+ if (size < SBN_HEADER_SIZE) {
+ SPDLOG_WARN("Audio data too small for SBN header: {}", size);
+ return;
+ }
+
+ // === SBN Header ===
+ // Offsets: 0x00 signature (s32), 0x04 size (s32), 0x10 fileListOffset (s32),
+ // 0x14 numEntries (s32), 0x18 fullFileSize (s32), 0x1C versionOffset (s32),
+ // 0x24 INIToffset (s32)
+ uint32_t* header32 = reinterpret_cast<uint32_t*>(data);
+
+ // Byte-swap the header fields we need
+ uint32_t signature = BSWAP32(header32[0]);
+ header32[0] = signature;
+ header32[1] = BSWAP32(header32[1]); // size
+
+ uint32_t fileListOffset = BSWAP32(header32[4]); // 0x10
+ uint32_t numEntries = BSWAP32(header32[5]); // 0x14
+ uint32_t fullFileSize = BSWAP32(header32[6]); // 0x18
+ uint32_t versionOffset = BSWAP32(header32[7]); // 0x1C
+ uint32_t initOffset = BSWAP32(header32[9]); // 0x24
+
+ header32[4] = fileListOffset;
+ header32[5] = numEntries;
+ header32[6] = fullFileSize;
+ header32[7] = versionOffset;
+ header32[9] = initOffset;
+
+ SPDLOG_DEBUG("SBN: signature=0x{:08X}, fileListOffset=0x{:X}, numEntries={}, initOffset=0x{:X}",
+ signature, fileListOffset, numEntries, initOffset);
+
+ if (signature != SBN_SIGNATURE) {
+ SPDLOG_ERROR("Invalid SBN signature: 0x{:08X}", signature);
+ return;
+ }
+
+ // === SBN File Entry Array ===
+ // Each entry: s32 offset, u32 data
+ if (CHECK_BOUNDS(fileListOffset, numEntries * SBN_FILE_ENTRY_SIZE, size)) {
+ uint32_t* entries = reinterpret_cast<uint32_t*>(data + fileListOffset);
+ for (uint32_t i = 0; i < numEntries; i++) {
+ uint32_t offset = BSWAP32(entries[i * 2]);
+ uint32_t entryData = BSWAP32(entries[i * 2 + 1]);
+ entries[i * 2] = offset;
+ entries[i * 2 + 1] = entryData;
+
+ // Stop if we hit an invalid entry
+ if ((offset & 0xFFFFFF) == 0) {
+ break;
+ }
+
+ // Get the file type and offset within SBN
+ uint8_t fileType = (entryData >> 24) & 0xFF;
+ uint32_t fileOffset = offset & 0xFFFFFF;
+ uint32_t fileSize = entryData & 0xFFFFFF;
+
+ // Byte-swap the embedded file based on type
+ if (fileOffset > 0 && CHECK_BOUNDS(fileOffset, 8, size)) {
+ uint8_t* fileData = data + fileOffset;
+
+ switch (fileType) {
+ case AU_FMT_BGM: {
+ // BGM Header: s32 signature, s32 size, s32 name, pad[4], BGMFileInfo
+ if (CHECK_BOUNDS(fileOffset, BGM_HEADER_SIZE, size)) {
+ uint32_t* bgm32 = reinterpret_cast<uint32_t*>(fileData);
+ bgm32[0] = BSWAP32(bgm32[0]); // signature
+ uint32_t bgmFileSize = BSWAP32(bgm32[1]);
+ bgm32[1] = bgmFileSize; // size
+ bgm32[2] = BSWAP32(bgm32[2]); // name
+ // pad at 0x0C
+
+ // BGMFileInfo at offset 0x10:
+ // u8 timingPreset, pad[3], u16 compositions[4], u16 drums, u16 drumCount, u16 instruments, u16 instrumentCount
+ uint16_t* bgm16 = reinterpret_cast<uint16_t*>(fileData + 0x14);
+ bgm16[0] = BSWAP16(bgm16[0]); // compositions[0]
+ bgm16[1] = BSWAP16(bgm16[1]); // compositions[1]
+ bgm16[2] = BSWAP16(bgm16[2]); // compositions[2]
+ bgm16[3] = BSWAP16(bgm16[3]); // compositions[3]
+ bgm16[4] = BSWAP16(bgm16[4]); // drums
+ bgm16[5] = BSWAP16(bgm16[5]); // drumCount
+ bgm16[6] = BSWAP16(bgm16[6]); // instruments
+ bgm16[7] = BSWAP16(bgm16[7]); // instrumentCount
+
+ // Swap composition data (SegData/u32 command arrays) and phrase track entries
+ // The BGM player reads these as u32 via SegData* pointers
+ std::set<uint32_t> swappedPhrases;
+ for (int comp = 0; comp < 4; comp++) {
+ uint16_t compOff = bgm16[comp]; // already swapped
+ if (compOff == 0) continue;
+
+ uint32_t compAbsOff = fileOffset + compOff * 4;
+ uint32_t* cmdPtr = reinterpret_cast<uint32_t*>(data + compAbsOff);
+
+ // Walk composition commands until BGM_COMP_END (0x00000000)
+ while (compAbsOff + 4 <= size) {
+ uint32_t raw = *cmdPtr;
+ if (raw == 0) break; // BGM_COMP_END is 0 in both endiannesses
+ uint32_t swapped = BSWAP32(raw);
+ *cmdPtr = swapped;
+
+ // Check for PLAY_PHRASE command (top nibble = 1)
+ if (((swapped >> 28) & 0xF) == 1) {
+ // Phrase offset is relative to compStartPos, in u32 units
+ uint16_t phraseRelOff = swapped & 0xFFFF;
+ uint32_t phraseAbsOff = fileOffset + compOff * 4 + phraseRelOff * 4;
+ if (swappedPhrases.find(phraseAbsOff) == swappedPhrases.end()
+ && CHECK_BOUNDS(phraseAbsOff, 16 * 4, size)) {
+ swappedPhrases.insert(phraseAbsOff);
+ // Swap 16 u32 track info entries
+ uint32_t* phrasePtr = reinterpret_cast<uint32_t*>(data + phraseAbsOff);
+ for (int t = 0; t < 16; t++) {
+ phrasePtr[t] = BSWAP32(phrasePtr[t]);
+ }
+ }
+ }
+ cmdPtr++;
+ compAbsOff += 4;
+ }
+ }
+
+ // Swap BGMDrumInfo entries (u16 bankPatch at +0, u16 keyBase at +2)
+ uint16_t drumsOff = bgm16[4];
+ uint16_t drumCount = bgm16[5];
+ if (drumsOff != 0 && drumCount > 0) {
+ uint32_t drumsAbsOff = fileOffset + drumsOff * 4;
+ for (uint16_t d = 0; d < drumCount; d++) {
+ uint32_t drumEntryOff = drumsAbsOff + d * BGM_DRUM_INFO_SIZE;
+ if (CHECK_BOUNDS(drumEntryOff, BGM_DRUM_INFO_SIZE, size)) {
+ uint16_t* drum16 = reinterpret_cast<uint16_t*>(data + drumEntryOff);
+ drum16[0] = BSWAP16(drum16[0]); // bankPatch
+ drum16[1] = BSWAP16(drum16[1]); // keyBase
+ }
+ }
+ }
+
+ // Swap BGMInstrumentInfo entries (u16 bankPatch at +0)
+ uint16_t instrOff = bgm16[6];
+ uint16_t instrCount = bgm16[7];
+ if (instrOff != 0 && instrCount > 0) {
+ uint32_t instrAbsOff = fileOffset + instrOff * 4;
+ for (uint16_t ins = 0; ins < instrCount; ins++) {
+ uint32_t instrEntryOff = instrAbsOff + ins * BGM_INSTRUMENT_INFO_SIZE;
+ if (CHECK_BOUNDS(instrEntryOff, BGM_INSTRUMENT_INFO_SIZE, size)) {
+ uint16_t* instr16 = reinterpret_cast<uint16_t*>(data + instrEntryOff);
+ instr16[0] = BSWAP16(instr16[0]); // bankPatch
+ }
+ }
+ }
+ }
+ break;
+ }
+
+ case AU_FMT_BK: {
+ // BK Header: u16 signature, pad[2], s32 size, s32 name, u16 format, u8 swizzled, pad[3],
+ // u16 instruments[16], u16 instrumentsLength, u16 loopStatesStart, u16 loopStatesLength,
+ // u16 predictorsStart, u16 predictorsLength, u16 envelopesStart, u16 envelopesLength
+ if (CHECK_BOUNDS(fileOffset, BK_HEADER_SIZE, size)) {
+ uint16_t* bk16 = reinterpret_cast<uint16_t*>(fileData);
+ bk16[0] = BSWAP16(bk16[0]); // signature
+ // pad at 0x02
+
+ uint32_t* bk32 = reinterpret_cast<uint32_t*>(fileData + 0x04);
+ uint32_t bkSize = BSWAP32(bk32[0]); // size
+ bk32[0] = bkSize;
+ bk32[1] = BSWAP32(bk32[1]); // name
+
+ bk16 = reinterpret_cast<uint16_t*>(fileData + 0x0C);
+ bk16[0] = BSWAP16(bk16[0]); // format
+ // swizzled (u8) and pad at 0x0E-0x11
+
+ // instruments[16] at 0x12 - swap and save offsets for instrument data swapping
+ uint16_t instrumentOffsets[16];
+ bk16 = reinterpret_cast<uint16_t*>(fileData + 0x12);
+ for (int j = 0; j < 16; j++) {
+ instrumentOffsets[j] = BSWAP16(bk16[j]);
+ bk16[j] = instrumentOffsets[j];
+ }
+
+ // More u16 fields at 0x32
+ bk16 = reinterpret_cast<uint16_t*>(fileData + 0x32);
+ bk16[0] = BSWAP16(bk16[0]); // instrumentsLength
+ bk16[1] = BSWAP16(bk16[1]); // loopStatesStart
+ bk16[2] = BSWAP16(bk16[2]); // loopStatesLength
+ bk16[3] = BSWAP16(bk16[3]); // predictorsStart
+ bk16[4] = BSWAP16(bk16[4]); // predictorsLength
+ bk16[5] = BSWAP16(bk16[5]); // envelopesStart
+ bk16[6] = BSWAP16(bk16[6]); // envelopesLength
+
+ // Save region offsets/lengths for data swapping below
+ uint16_t loopStatesStart = bk16[1];
+ uint16_t loopStatesLength = bk16[2];
+ uint16_t predictorsStart = bk16[3];
+ uint16_t predictorsLength = bk16[4];
+
+ // Now swap each Instrument structure within the BK file
+ // Instrument structure (0x30 bytes):
+ // 0x00: u32 wavData (offset)
+ // 0x04: u32 wavDataLength
+ // 0x08: u32 loopState (offset)
+ // 0x0C: s32 loopStart
+ // 0x10: s32 loopEnd
+ // 0x14: s32 loopCount
+ // 0x18: u32 predictor (offset)
+ // 0x1C: u16 codebookSize
+ // 0x1E: u16 keyBase
+ // 0x20: s32 sampleRate
+ // 0x24-0x2B: u8 fields (no swap needed)
+ // 0x2C: u32 envelopes (offset)
+
+ // Track which envelope presets we've already swapped (multiple instruments may share one)
+ std::set<uint32_t> swappedEnvelopes;
+
+ for (int j = 0; j < 16; j++) {
+ uint16_t instOffset = instrumentOffsets[j];
+ if (instOffset != 0 && CHECK_BOUNDS(fileOffset + instOffset, 0x30, size)) {
+ uint8_t* instData = fileData + instOffset;
+ uint32_t* inst32 = reinterpret_cast<uint32_t*>(instData);
+
+ inst32[0] = BSWAP32(inst32[0]); // wavData
+ inst32[1] = BSWAP32(inst32[1]); // wavDataLength
+ inst32[2] = BSWAP32(inst32[2]); // loopState
+ inst32[3] = BSWAP32(inst32[3]); // loopStart
+ inst32[4] = BSWAP32(inst32[4]); // loopEnd
+ inst32[5] = BSWAP32(inst32[5]); // loopCount
+ inst32[6] = BSWAP32(inst32[6]); // predictor
+
+ uint16_t* inst16 = reinterpret_cast<uint16_t*>(instData + 0x1C);
+ inst16[0] = BSWAP16(inst16[0]); // codebookSize
+ inst16[1] = BSWAP16(inst16[1]); // keyBase
+
+ inst32 = reinterpret_cast<uint32_t*>(instData + 0x20);
+ inst32[0] = BSWAP32(inst32[0]); // sampleRate
+ // 0x24-0x2B are u8 fields, no swap needed
+
+ inst32 = reinterpret_cast<uint32_t*>(instData + 0x2C);
+ uint32_t envOffset = BSWAP32(inst32[0]);
+ inst32[0] = envOffset; // envelopes
+
+ // Swap EnvelopePreset data if not already done
+ // EnvelopePreset: u8 count, pad[3], EnvelopeOffset offsets[count]
+ // EnvelopeOffset: u16 offsetPress, u16 offsetRelease
+ if (envOffset != 0 && swappedEnvelopes.find(envOffset) == swappedEnvelopes.end()) {
+ uint32_t envAbsOff = fileOffset + envOffset;
+ if (CHECK_BOUNDS(envAbsOff, 4, size)) {
+ uint8_t envCount = data[envAbsOff]; // u8, no swap
+ // Swap each EnvelopeOffset entry (4 bytes each)
+ for (uint8_t e = 0; e < envCount; e++) {
+ uint32_t entryOff = envAbsOff + 4 + e * 4;
+ if (CHECK_BOUNDS(entryOff, 4, size)) {
+ uint16_t* envEntry = reinterpret_cast<uint16_t*>(data + entryOff);
+ envEntry[0] = BSWAP16(envEntry[0]); // offsetPress
+ envEntry[1] = BSWAP16(envEntry[1]); // offsetRelease
+ }
+ }
+ swappedEnvelopes.insert(envOffset);
+ }
+ }
+ }
+ }
+
+ // Swap predictor codebook data (s16 array)
+ if (predictorsStart > 0 && predictorsLength > 0) {
+ uint32_t predAbsOff = fileOffset + predictorsStart;
+ if (CHECK_BOUNDS(predAbsOff, predictorsLength, size)) {
+ uint16_t* predData = reinterpret_cast<uint16_t*>(data + predAbsOff);
+ uint32_t numShorts = predictorsLength / 2;
+ for (uint32_t p = 0; p < numShorts; p++) {
+ predData[p] = BSWAP16(predData[p]);
+ }
+ SPDLOG_DEBUG("BK: swapped {} predictor shorts at offset 0x{:X}", numShorts, predictorsStart);
+ }
+ }
+
+ // Swap loop state data (s16 array)
+ if (loopStatesStart > 0 && loopStatesLength > 0) {
+ uint32_t loopAbsOff = fileOffset + loopStatesStart;
+ if (CHECK_BOUNDS(loopAbsOff, loopStatesLength, size)) {
+ uint16_t* loopData = reinterpret_cast<uint16_t*>(data + loopAbsOff);
+ uint32_t numShorts = loopStatesLength / 2;
+ for (uint32_t l = 0; l < numShorts; l++) {
+ loopData[l] = BSWAP16(loopData[l]);
+ }
+ SPDLOG_DEBUG("BK: swapped {} loop state shorts at offset 0x{:X}", numShorts, loopStatesStart);
+ }
+ }
+ }
+ break;
+ }
+
+ case AU_FMT_SEF: {
+ // SEF Header: s32 signature, s32 size, s32 name, pad[2], u8 hasExtraSection, pad[1],
+ // u16 sections[8], u16 section2000
+ if (CHECK_BOUNDS(fileOffset, SEF_HEADER_SIZE, size)) {
+ uint32_t* sef32 = reinterpret_cast<uint32_t*>(fileData);
+ sef32[0] = BSWAP32(sef32[0]); // signature
+ uint32_t sefSize = BSWAP32(sef32[1]); // size
+ sef32[1] = sefSize;
+ sef32[2] = BSWAP32(sef32[2]); // name
+ // pad and u8 at 0x0C-0x0F
+
+ // Swap header section offsets and save them
+ uint16_t* sef16 = reinterpret_cast<uint16_t*>(fileData + 0x10);
+ uint16_t sectionOffsets[9]; // sections[8] + section2000
+ for (int j = 0; j < 9; j++) {
+ sectionOffsets[j] = BSWAP16(sef16[j]);
+ sef16[j] = sectionOffsets[j];
+ }
+
+ // SEF section layout:
+ // - Sections 0-3: lookup tables of (u16 offset, u16 info) pairs + polyphonic sub-tables
+ // Game code dereferences offset via AU_FILE_RELATIVE -> must be swapped
+ // - Sections 4-7 and extra: raw command bytes passed directly as (u8*)cmdList
+ // Game code reads byte-by-byte -> must NOT be swapped
+
+ // Track swapped sub-table offsets to avoid double-swapping
+ std::set<uint16_t> swappedSubTables;
+
+ // Swap sections 0-3 lookup tables
+ for (int j = 0; j < 4; j++) {
+ if (sectionOffsets[j] == 0) continue;
+ uint32_t secAbsOff = fileOffset + sectionOffsets[j];
+ uint32_t entryCount = SEF_SECTION_0_3_ENTRIES;
+ if (!CHECK_BOUNDS(secAbsOff, entryCount * 4, size)) continue;
+
+ uint16_t* entries = reinterpret_cast<uint16_t*>(data + secAbsOff);
+ for (uint32_t k = 0; k < entryCount; k++) {
+ uint16_t cmdOffset = BSWAP16(entries[k * 2]);
+ uint16_t cmdInfo = BSWAP16(entries[k * 2 + 1]);
+ entries[k * 2] = cmdOffset;
+ entries[k * 2 + 1] = cmdInfo;
+
+ if (cmdOffset == 0) continue;
+
+ // Check for polyphonic entries (bits 5-6 of info)
+ uint8_t polyphonyMode = (cmdInfo & 0x60) >> 5;
+ if (polyphonyMode != 0 && swappedSubTables.find(cmdOffset) == swappedSubTables.end()) {
+ // Follow offset to polyphonic sub-table and swap it
+ uint32_t trackCount = 2 << (polyphonyMode - 1); // 2, 4, or 8
+ uint32_t subTableAbsOff = fileOffset + cmdOffset;
+ if (CHECK_BOUNDS(subTableAbsOff, trackCount * 4, size)) {
+ uint16_t* subEntries = reinterpret_cast<uint16_t*>(data + subTableAbsOff);
+ for (uint32_t t = 0; t < trackCount; t++) {
+ subEntries[t * 2] = BSWAP16(subEntries[t * 2]); // offset
+ subEntries[t * 2 + 1] = BSWAP16(subEntries[t * 2 + 1]); // info
+ }
+ }
+ swappedSubTables.insert(cmdOffset);
+ }
+ }
+ }
+
+ // Sections 4-7: raw command bytes, no swap needed
+ // Extra section (section2000): raw command bytes, no swap needed
+
+ SPDLOG_DEBUG("SEF: swapped {} section 0-3 lookup tables, {} polyphonic sub-tables",
+ 4, swappedSubTables.size());
+ }
+ break;
+ }
+
+ case AU_FMT_MSEQ: {
+ // Format 0x40 is shared by MSEQ, PER, and PRG files.
+ // Distinguish by reading the big-endian signature before swapping.
+ uint32_t fileSig = BSWAP32(*reinterpret_cast<uint32_t*>(fileData));
+
+ if (fileSig == PER_SIGNATURE) {
+ // PER file: s32 signature, s32 size, pad[8], then PEREntry data
+ // PEREntry = 12 × BGMDrumInfo (0x0C bytes each) = 0x90 bytes
+ // BGMDrumInfo: u16 bankPatch, u16 keyBase, u8 volume, s8 pan, u8 reverb, ...
+ if (CHECK_BOUNDS(fileOffset, PER_HEADER_SIZE, size)) {
+ uint32_t* per32 = reinterpret_cast<uint32_t*>(fileData);
+ per32[0] = BSWAP32(per32[0]); // signature
+ uint32_t perSize = BSWAP32(per32[1]);
+ per32[1] = perSize; // size
+
+ // Swap internal BGMDrumInfo entries
+ uint32_t dataStart = PER_HEADER_SIZE;
+ uint32_t dataLen = perSize - dataStart;
+ uint32_t numDrums = dataLen / BGM_DRUM_INFO_SIZE;
+ for (uint32_t d = 0; d < numDrums; d++) {
+ uint32_t drumOff = fileOffset + dataStart + d * BGM_DRUM_INFO_SIZE;
+ if (CHECK_BOUNDS(drumOff, BGM_DRUM_INFO_SIZE, size)) {
+ uint16_t* drum16 = reinterpret_cast<uint16_t*>(data + drumOff);
+ drum16[0] = BSWAP16(drum16[0]); // bankPatch
+ drum16[1] = BSWAP16(drum16[1]); // keyBase
+ // remaining fields are u8, no swap
+ }
+ }
+ SPDLOG_DEBUG("PER: swapped {} drum entries", numDrums);
+ }
+ } else if (fileSig == PRG_SIGNATURE) {
+ // PRG file: s32 signature, s32 size, pad[8], then BGMInstrumentInfo data
+ // BGMInstrumentInfo: u16 bankPatch, u8 volume, s8 pan, u8 reverb, s8 coarseTune, s8 fineTune, pad
+ if (CHECK_BOUNDS(fileOffset, PER_HEADER_SIZE, size)) {
+ uint32_t* prg32 = reinterpret_cast<uint32_t*>(fileData);
+ prg32[0] = BSWAP32(prg32[0]); // signature
+ uint32_t prgSize = BSWAP32(prg32[1]);
+ prg32[1] = prgSize; // size
+
+ // Swap internal BGMInstrumentInfo entries
+ uint32_t dataStart = PER_HEADER_SIZE;
+ uint32_t dataLen = prgSize - dataStart;
+ uint32_t numInst = dataLen / BGM_INSTRUMENT_INFO_SIZE;
+ for (uint32_t p = 0; p < numInst; p++) {
+ uint32_t instOff = fileOffset + dataStart + p * BGM_INSTRUMENT_INFO_SIZE;
+ if (CHECK_BOUNDS(instOff, BGM_INSTRUMENT_INFO_SIZE, size)) {
+ uint16_t* inst16 = reinterpret_cast<uint16_t*>(data + instOff);
+ inst16[0] = BSWAP16(inst16[0]); // bankPatch
+ // remaining fields are u8/s8, no swap
+ }
+ }
+ SPDLOG_DEBUG("PRG: swapped {} instrument entries", numInst);
+ }
+ } else {
+ // MSEQ file
+ // MSEQ Header: s32 signature, s32 size, s32 name, u8 firstVoiceIdx, u8 trackSettingsCount,
+ // u16 trackSettingsOffset, u16 dataStart, pad[6]
+ if (CHECK_BOUNDS(fileOffset, MSEQ_HEADER_SIZE, size)) {
+ uint32_t* mseq32 = reinterpret_cast<uint32_t*>(fileData);
+ mseq32[0] = BSWAP32(mseq32[0]); // signature
+ mseq32[1] = BSWAP32(mseq32[1]); // size
+ mseq32[2] = BSWAP32(mseq32[2]); // name
+ // u8 fields at 0x0C-0x0D
+
+ uint16_t* mseq16 = reinterpret_cast<uint16_t*>(fileData + 0x0E);
+ uint16_t trackSettingsOffset = BSWAP16(mseq16[0]);
+ mseq16[0] = trackSettingsOffset; // trackSettingsOffset
+ mseq16[1] = BSWAP16(mseq16[1]); // dataStart
+
+ // Swap MSEQTrackData entries
+ // Each entry: u8 trackIndex, u8 type, s16 time, s16 delta, s16 goal (8 bytes)
+ uint8_t trackSettingsCount = fileData[0x0D];
+ if (trackSettingsCount > 0 && trackSettingsOffset > 0) {
+ for (uint8_t t = 0; t < trackSettingsCount; t++) {
+ uint32_t entryOff = fileOffset + trackSettingsOffset + t * 8;
+ if (CHECK_BOUNDS(entryOff, 8, size)) {
+ uint16_t* td16 = reinterpret_cast<uint16_t*>(data + entryOff + 2);
+ td16[0] = BSWAP16(td16[0]); // time
+ td16[1] = BSWAP16(td16[1]); // delta
+ td16[2] = BSWAP16(td16[2]); // goal
+ }
+ }
+ }
+ }
+ }
+ break;
+ }
+
+ default:
+ // Unknown file type, skip
+ break;
+ }
+ }
+ }
+ }
+
+ // === INIT Section ===
+ if (initOffset > 0 && CHECK_BOUNDS(initOffset, INIT_HEADER_SIZE, size)) {
+ uint8_t* initData = data + initOffset;
+
+ // INIT Header: s32 signature, s32 size, u16 bankListOffset, u16 bankListSize,
+ // u16 songListOffset, u16 songListSize, u16 mseqListOffset, u16 mseqListSize, pad[12]
+ uint32_t* init32 = reinterpret_cast<uint32_t*>(initData);
+ init32[0] = BSWAP32(init32[0]); // signature
+ init32[1] = BSWAP32(init32[1]); // size
+
+ uint16_t* init16 = reinterpret_cast<uint16_t*>(initData + 0x08);
+ uint16_t bankListOffset = BSWAP16(init16[0]);
+ uint16_t bankListSize = BSWAP16(init16[1]);
+ uint16_t songListOffset = BSWAP16(init16[2]);
+ uint16_t songListSize = BSWAP16(init16[3]);
+ uint16_t mseqListOffset = BSWAP16(init16[4]);
+ uint16_t mseqListSize = BSWAP16(init16[5]);
+
+ init16[0] = bankListOffset;
+ init16[1] = bankListSize;
+ init16[2] = songListOffset;
+ init16[3] = songListSize;
+ init16[4] = mseqListOffset;
+ init16[5] = mseqListSize;
+
+ SPDLOG_DEBUG("INIT: songListOffset=0x{:X}, songListSize={}, bankListOffset=0x{:X}, mseqListOffset=0x{:X}",
+ songListOffset, songListSize, bankListOffset, mseqListOffset);
+
+ // === Song List (InitSongEntry array) ===
+ // Each entry: u16 bgmFileIndex, u16 bkFileIndex[3]
+ uint32_t songListAbsOffset = initOffset + songListOffset;
+ uint32_t numSongs = songListSize / INIT_SONG_ENTRY_SIZE;
+ if (songListOffset > 0 && CHECK_BOUNDS(songListAbsOffset, songListSize, size)) {
+ uint16_t* songList = reinterpret_cast<uint16_t*>(data + songListAbsOffset);
+ for (uint32_t i = 0; i < numSongs * 4; i++) { // 4 u16s per entry
+ songList[i] = BSWAP16(songList[i]);
+ }
+ }
+
+ // === Bank List (InitBankEntry array) ===
+ // Each entry: u16 fileIndex, u8 bankIndex, u8 bankSet
+ uint32_t bankListAbsOffset = initOffset + bankListOffset;
+ uint32_t numBanks = bankListSize / INIT_BANK_ENTRY_SIZE;
+ if (bankListOffset > 0 && CHECK_BOUNDS(bankListAbsOffset, bankListSize, size)) {
+ // Only need to swap the u16 fileIndex, the u8 fields don't need swapping
+ for (uint32_t i = 0; i < numBanks; i++) {
+ uint16_t* bankEntry = reinterpret_cast<uint16_t*>(data + bankListAbsOffset + i * INIT_BANK_ENTRY_SIZE);
+ bankEntry[0] = BSWAP16(bankEntry[0]); // fileIndex
+ }
+ }
+
+ // === MSEQ/Extra File List (u16 array) ===
+ // This is a simple array of u16 file indices
+ uint32_t mseqListAbsOffset = initOffset + mseqListOffset;
+ uint32_t numMseqEntries = mseqListSize / 2;
+ if (mseqListOffset > 0 && CHECK_BOUNDS(mseqListAbsOffset, mseqListSize, size)) {
+ uint16_t* mseqList = reinterpret_cast<uint16_t*>(data + mseqListAbsOffset);
+ for (uint32_t i = 0; i < numMseqEntries; i++) {
+ mseqList[i] = BSWAP16(mseqList[i]);
+ }
+ }
+ }
+
+ SPDLOG_DEBUG("PM64:AUDIO byte-swap complete, {} bytes processed", size);
+}
+
+std::optional<std::shared_ptr<IParsedData>> PM64AudioFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) {
+ auto offset = GetSafeNode<uint32_t>(node, "offset");
+ auto size = GetSafeNode<size_t>(node, "size");
+
+ if (offset + size > buffer.size()) {
+ SPDLOG_ERROR("PM64:AUDIO offset 0x{:X} + size 0x{:X} exceeds buffer size 0x{:X}", offset, size, buffer.size());
+ return std::nullopt;
+ }
+
+ // Copy the audio data
+ std::vector<uint8_t> audioData(buffer.begin() + offset, buffer.begin() + offset + size);
+
+ // Byte-swap for little-endian
+ ByteSwapAudioData(audioData.data(), audioData.size());
+
+ return std::make_shared<RawBuffer>(audioData);
+}
+
+ExportResult PM64AudioBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, std::string* replacement) {
+ auto writer = LUS::BinaryWriter();
+ auto data = std::static_pointer_cast<RawBuffer>(raw)->mBuffer;
+
+ // Write as Blob type - game will load as raw binary
+ WriteHeader(writer, Torch::ResourceType::Blob, 0);
+ writer.Write(static_cast<uint32_t>(data.size()));
+ writer.Write(reinterpret_cast<char*>(data.data()), data.size());
+ writer.Finish(write);
+
+ return std::nullopt;
+}
+
+ExportResult PM64AudioHeaderExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, std::string* replacement) {
+ const auto symbol = GetSafeNode(node, "symbol", entryName);
+
+ if (Companion::Instance->IsOTRMode()) {
+ write << "static const ALIGN_ASSET(2) char " << symbol << "[] = \"__OTR__" << (*replacement) << "\";\n\n";
+ return std::nullopt;
+ }
+
+ write << "extern u8 " << symbol << "[];\n";
+ return std::nullopt;
+}