UNPKG

snes-disassembler

Version:

A Super Nintendo (SNES) ROM disassembler for 65816 assembly

343 lines 14.2 kB
"use strict"; /** * SPC File Format Builder * * Creates SPC (SNES-SPC700 Sound File Data) files with proper structure * based on the format used in snes9x smp_state.cpp * * SPC File Structure: * - 33-byte header with "SNES-SPC700 Sound File Data v0.30" * - SPC700 CPU state (PC, registers, PSW, SP) * - 64KB APU RAM allocation * - 128 DSP register configuration * - Sample directory setup at 0x200 (DIR register) * - Voice configuration for each sample */ Object.defineProperty(exports, "__esModule", { value: true }); exports.SPCBuilder = void 0; const audio_types_1 = require("../types/audio-types"); /** * Builder class for creating SPC files from SNES audio data */ class SPCBuilder { /** * Initialize SPC builder with default values */ constructor() { this.reset(); } /** * Reset builder to default state */ reset() { // SPC700 CPU state this.cpuState = { pc: 0x0000, a: 0x00, x: 0x00, y: 0x00, psw: 0x02, // Default PSW value sp: 0xEF // Default stack pointer }; // APU RAM (64KB) this.apuRam = new Uint8Array(SPCBuilder.APURAM_SIZE); // DSP registers (128 bytes) this.dspRegisters = new Uint8Array(SPCBuilder.DSP_REGISTERS_SIZE); // IPL ROM (64 bytes) - SPC700 boot ROM this.iplRom = new Uint8Array([ 0xCD, 0xEF, 0xBD, 0xE8, 0x00, 0xC6, 0x1D, 0xD0, 0xFC, 0x8F, 0xAA, 0xF4, 0x8F, 0xBB, 0xF5, 0x78, 0xCC, 0xF4, 0xD0, 0xFB, 0x2F, 0x19, 0xEB, 0xF4, 0xD0, 0xFC, 0x7E, 0xF4, 0xD0, 0x0B, 0xE4, 0xF5, 0xCB, 0xF4, 0xD7, 0x00, 0xFC, 0xD0, 0xF3, 0xAB, 0x01, 0x10, 0xEF, 0x7E, 0xF4, 0x10, 0xEB, 0xBA, 0xF6, 0xDA, 0x00, 0xBA, 0xF4, 0xC4, 0xF4, 0xDD, 0x5D, 0xD0, 0xDB, 0x1F, 0x00, 0x00, 0xC0, 0xFF ]); // Initialize default DSP settings this.setupDefaultDSP(); } /** * Setup default DSP register values */ setupDefaultDSP() { // Set sample directory to page 2 (0x0200) this.dspRegisters[SPCBuilder.DSP_DIR] = 0x02; // Default main volume this.dspRegisters[SPCBuilder.DSP_MVOLL] = 0x7F; this.dspRegisters[SPCBuilder.DSP_MVOLR] = 0x7F; // Clear voice end flags this.dspRegisters[SPCBuilder.DSP_ENDX] = 0x00; // Setup default FIR filter coefficients (simple pass-through) const firCoeffs = [0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; for (let i = 0; i < firCoeffs.length; i++) { this.dspRegisters[SPCBuilder.DSP_FIR + (i * 0x10)] = firCoeffs[i] & 0xFF; } } /** * Set SPC700 CPU state */ setCPUState(state) { if (state.pc !== undefined) this.cpuState.pc = state.pc & 0xFFFF; if (state.a !== undefined) this.cpuState.a = state.a & 0xFF; if (state.x !== undefined) this.cpuState.x = state.x & 0xFF; if (state.y !== undefined) this.cpuState.y = state.y & 0xFF; if (state.psw !== undefined) this.cpuState.psw = state.psw & 0xFF; if (state.sp !== undefined) this.cpuState.sp = state.sp & 0xFF; } /** * Set APU RAM data at specified offset */ setAPURAM(data, offset = 0) { if (offset + data.length > SPCBuilder.APURAM_SIZE) { throw new Error(`Data exceeds APU RAM bounds: ${offset + data.length} > ${SPCBuilder.APURAM_SIZE}`); } this.apuRam.set(data, offset); } /** * Set individual DSP register */ setDSPRegister(register, value) { if (register >= SPCBuilder.DSP_REGISTERS_SIZE) { throw new Error(`DSP register out of range: ${register} >= ${SPCBuilder.DSP_REGISTERS_SIZE}`); } this.dspRegisters[register] = value & 0xFF; } /** * Configure a DSP voice (0-7) */ setupVoice(voice, sampleNum = 0, volumeLeft = 0x7F, volumeRight = 0x7F, pitch = 0x1000, adsr0 = 0x8F, adsr1 = 0xE0) { if (voice >= 8) { throw new Error(`Voice number out of range: ${voice} >= 8`); } const base = voice * 0x10; // Set voice parameters this.dspRegisters[base + SPCBuilder.VOICE_VOLL] = volumeLeft & 0xFF; this.dspRegisters[base + SPCBuilder.VOICE_VOLR] = volumeRight & 0xFF; this.dspRegisters[base + SPCBuilder.VOICE_PITCHL] = pitch & 0xFF; this.dspRegisters[base + SPCBuilder.VOICE_PITCHH] = (pitch >> 8) & 0x3F; this.dspRegisters[base + SPCBuilder.VOICE_SRCN] = sampleNum & 0xFF; this.dspRegisters[base + SPCBuilder.VOICE_ADSR0] = adsr0 & 0xFF; this.dspRegisters[base + SPCBuilder.VOICE_ADSR1] = adsr1 & 0xFF; this.dspRegisters[base + SPCBuilder.VOICE_GAIN] = 0x00; // Use ADSR this.dspRegisters[base + SPCBuilder.VOICE_ENVX] = 0x00; this.dspRegisters[base + SPCBuilder.VOICE_OUTX] = 0x00; } /** * Setup sample directory at specified page (default 0x200) */ setupSampleDirectory(samples, dirPage = 0x02) { // Set DIR register this.dspRegisters[SPCBuilder.DSP_DIR] = dirPage & 0xFF; // Calculate directory address const dirAddr = dirPage * 0x100; if (dirAddr + samples.length * 4 > SPCBuilder.APURAM_SIZE) { throw new Error('Sample directory exceeds APU RAM bounds'); } if (samples.length > 256) { throw new Error('Too many samples (max 256)'); } // Each sample entry is 4 bytes: start_addr (2 bytes) + loop_addr (2 bytes) for (let i = 0; i < samples.length; i++) { const offset = dirAddr + (i * 4); const startAddr = samples[i].startAddress; const loopAddr = samples[i].loopAddress; // Store as little-endian 16-bit values this.apuRam[offset] = startAddr & 0xFF; this.apuRam[offset + 1] = (startAddr >> 8) & 0xFF; this.apuRam[offset + 2] = loopAddr & 0xFF; this.apuRam[offset + 3] = (loopAddr >> 8) & 0xFF; } } /** * Load BRR sample data into APU RAM * @returns End address after sample data */ loadBRRSample(brrData, startAddr) { if (startAddr + brrData.length > SPCBuilder.APURAM_SIZE) { throw new Error('BRR sample exceeds APU RAM bounds'); } this.apuRam.set(brrData, startAddr); return startAddr + brrData.length; } /** * Enable voices using KON register */ enableVoices(voiceMask) { this.dspRegisters[SPCBuilder.DSP_KON] = voiceMask & 0xFF; } /** * Set main output volume */ setMainVolume(left = 0x7F, right = 0x7F) { this.dspRegisters[SPCBuilder.DSP_MVOLL] = left & 0xFF; this.dspRegisters[SPCBuilder.DSP_MVOLR] = right & 0xFF; } /** * Configure echo settings */ setEchoSettings(settings) { if (settings.enableMask !== undefined) { this.dspRegisters[SPCBuilder.DSP_EON] = settings.enableMask & 0xFF; } if (settings.volumeLeft !== undefined) { this.dspRegisters[SPCBuilder.DSP_EVOLL] = settings.volumeLeft & 0xFF; } if (settings.volumeRight !== undefined) { this.dspRegisters[SPCBuilder.DSP_EVOLR] = settings.volumeRight & 0xFF; } if (settings.feedback !== undefined) { this.dspRegisters[SPCBuilder.DSP_EFB] = settings.feedback & 0xFF; } if (settings.startAddress !== undefined) { this.dspRegisters[SPCBuilder.DSP_ESA] = (settings.startAddress >> 8) & 0xFF; } if (settings.delay !== undefined) { this.dspRegisters[SPCBuilder.DSP_EDL] = settings.delay & 0x0F; } } /** * Build complete SPC file data */ build() { const spcData = new Uint8Array(SPCBuilder.SPC_FILE_SIZE); let offset = 0; // Header (33 bytes) const header = new TextEncoder().encode('SNES-SPC700 Sound File Data v0.30'); spcData.set(header, offset); offset += SPCBuilder.HEADER_SIZE; // ID tag and version (4 bytes) spcData[offset] = 26; // ID tag 0 spcData[offset + 1] = 26; // ID tag 1 spcData[offset + 2] = 27; // ID tag 2 spcData[offset + 3] = 30; // Version minor offset += 4; // SPC700 CPU state (8 bytes) spcData[offset] = this.cpuState.pc & 0xFF; // PC low spcData[offset + 1] = (this.cpuState.pc >> 8) & 0xFF; // PC high spcData[offset + 2] = this.cpuState.a; // A register spcData[offset + 3] = this.cpuState.x; // X register spcData[offset + 4] = this.cpuState.y; // Y register spcData[offset + 5] = this.cpuState.psw; // PSW spcData[offset + 6] = this.cpuState.sp; // SP spcData[offset + 7] = 0; // Unused offset += 8; // ID666 tag (210 bytes) - song metadata, zero-filled offset += 210; // APU RAM (65536 bytes) spcData.set(this.apuRam, offset); // Copy current MMIO registers to APU RAM for (let i = 0xF2; i <= 0xF9; i++) { spcData[offset + i] = this.apuRam[i]; } for (let i = 0xFD; i < 0x100; i++) { spcData[offset + i] = this.apuRam[i]; } offset += SPCBuilder.APURAM_SIZE; // DSP registers (128 bytes) spcData.set(this.dspRegisters, offset); offset += SPCBuilder.DSP_REGISTERS_SIZE; // Unused area (64 bytes) offset += 64; // IPL ROM (64 bytes) spcData.set(this.iplRom, offset); return spcData; } /** * Save SPC file to binary data (for use with file system APIs) */ save() { return this.build(); } /** * Create a simple SPC file with samples and voice configuration */ static createSimpleSPC(samples, voices) { const builder = new SPCBuilder(); // Load samples into APU RAM and setup directory const sampleDir = []; for (let i = 0; i < samples.length; i++) { const sample = samples[i]; const startAddr = sample.startAddress ?? (0x0400 + (i * 0x1000)); const loopAddr = sample.loopAddress ?? startAddr; builder.loadBRRSample(sample.brrData, startAddr); sampleDir.push({ startAddress: startAddr, loopAddress: loopAddr }); } builder.setupSampleDirectory(sampleDir); // Configure voices if (voices) { for (let i = 0; i < Math.min(voices.length, 8); i++) { const voiceCfg = voices[i]; builder.setupVoice(i, voiceCfg.sourceNumber ?? 0, voiceCfg.leftVolume ?? 0x7F, voiceCfg.rightVolume ?? 0x7F, voiceCfg.pitch ?? 0x1000, voiceCfg.adsr?.attack !== undefined ? ((voiceCfg.adsr.attack & 0xF) << 4) | (voiceCfg.adsr.decay & 0x7) : 0x8F, voiceCfg.adsr?.sustain !== undefined ? ((voiceCfg.adsr.sustain & 0x7) << 5) | (voiceCfg.adsr.release & 0x1F) : 0xE0); } } // Enable configured voices if (voices) { const voiceMask = (1 << voices.length) - 1; builder.enableVoices(voiceMask); } return builder; } /** * Get current CPU state */ getCPUState() { return { ...this.cpuState }; } /** * Get APU RAM copy */ getAPURAM() { return new Uint8Array(this.apuRam); } /** * Get DSP registers copy */ getDSPRegisters() { return new Uint8Array(this.dspRegisters); } } exports.SPCBuilder = SPCBuilder; // SPC file constants SPCBuilder.SPC_FILE_SIZE = 66048; SPCBuilder.HEADER_SIZE = 33; SPCBuilder.APURAM_SIZE = 65536; SPCBuilder.DSP_REGISTERS_SIZE = 128; SPCBuilder.IPLROM_SIZE = 64; // DSP register offsets (using constants from types) SPCBuilder.DSP_MVOLL = audio_types_1.DSP_REGISTERS.MAIN_LEFT_VOL; // Main volume left SPCBuilder.DSP_MVOLR = audio_types_1.DSP_REGISTERS.MAIN_RIGHT_VOL; // Main volume right SPCBuilder.DSP_EVOLL = audio_types_1.DSP_REGISTERS.ECHO_LEFT_VOL; // Echo volume left SPCBuilder.DSP_EVOLR = audio_types_1.DSP_REGISTERS.ECHO_RIGHT_VOL; // Echo volume right SPCBuilder.DSP_KON = audio_types_1.DSP_REGISTERS.KEY_ON; // Key on SPCBuilder.DSP_KOFF = audio_types_1.DSP_REGISTERS.KEY_OFF; // Key off SPCBuilder.DSP_FLG = audio_types_1.DSP_REGISTERS.FLAGS; // DSP flags SPCBuilder.DSP_ENDX = audio_types_1.DSP_REGISTERS.ENDX; // Voice end flags SPCBuilder.DSP_EFB = audio_types_1.DSP_REGISTERS.ECHO_FEEDBACK; // Echo feedback SPCBuilder.DSP_PMON = audio_types_1.DSP_REGISTERS.PITCH_MOD; // Pitch modulation SPCBuilder.DSP_NON = audio_types_1.DSP_REGISTERS.NOISE_ON; // Noise on SPCBuilder.DSP_EON = audio_types_1.DSP_REGISTERS.ECHO_ON; // Echo on SPCBuilder.DSP_DIR = audio_types_1.DSP_REGISTERS.SOURCE_DIR; // Sample directory page SPCBuilder.DSP_ESA = audio_types_1.DSP_REGISTERS.ECHO_START; // Echo start address SPCBuilder.DSP_EDL = audio_types_1.DSP_REGISTERS.ECHO_DELAY; // Echo delay SPCBuilder.DSP_FIR = audio_types_1.DSP_REGISTERS.FIR_C0; // FIR coefficients // Voice register offsets (per voice, 0x10 bytes apart) SPCBuilder.VOICE_VOLL = audio_types_1.DSP_REGISTERS.VOICE_LEFT_VOL; // Volume left SPCBuilder.VOICE_VOLR = audio_types_1.DSP_REGISTERS.VOICE_RIGHT_VOL; // Volume right SPCBuilder.VOICE_PITCHL = audio_types_1.DSP_REGISTERS.VOICE_PITCH_LOW; // Pitch low SPCBuilder.VOICE_PITCHH = audio_types_1.DSP_REGISTERS.VOICE_PITCH_HIGH; // Pitch high SPCBuilder.VOICE_SRCN = audio_types_1.DSP_REGISTERS.VOICE_SRC_NUM; // Sample source number SPCBuilder.VOICE_ADSR0 = audio_types_1.DSP_REGISTERS.VOICE_ADSR1; // ADSR envelope 0 SPCBuilder.VOICE_ADSR1 = audio_types_1.DSP_REGISTERS.VOICE_ADSR2; // ADSR envelope 1 SPCBuilder.VOICE_GAIN = audio_types_1.DSP_REGISTERS.VOICE_GAIN; // Gain SPCBuilder.VOICE_ENVX = audio_types_1.DSP_REGISTERS.VOICE_ENV_VAL; // Current envelope value SPCBuilder.VOICE_OUTX = audio_types_1.DSP_REGISTERS.VOICE_OUT_VAL; // Current sample output //# sourceMappingURL=SPCBuilder.js.map