UNPKG

snes-disassembler

Version:

A Super Nintendo (SNES) ROM disassembler for 65816 assembly

367 lines 13.5 kB
"use strict"; /** * SPC File Export for SNES Disassembler * Implements SPC700 state export for audio playback as per SPC File Format v0.30 * * Based on SPC File Format Specification: * - 27-byte header with SPC700 register states * - 64KB SPC700 RAM state * - 128 bytes DSP register states * - ID666 metadata tags support * - Timer and I/O port states */ Object.defineProperty(exports, "__esModule", { value: true }); exports.SPCExporter = void 0; class SPCExporter { /** * Export SPC700 and DSP state to SPC file format */ static exportSPC(spcState, dspState, metadata, options = {}) { const { includeID666 = true, binaryFormat = false, includeExtended = false, validate = true } = options; // Calculate total file size const headerSize = 0x100; // 256 bytes (header + ID666) const ramSize = this.RAM_SIZE; const dspSize = this.DSP_REG_SIZE; const unusedSize = 0x40; // 64 bytes unused const extraRamSize = this.EXTRA_RAM_SIZE; let totalSize = headerSize + ramSize + dspSize + unusedSize + extraRamSize; // Add extended ID666 size if needed let extendedData = null; if (includeExtended && metadata) { extendedData = this.createExtendedID666(metadata); totalSize += extendedData.length; } const buffer = new Uint8Array(totalSize); let offset = 0; // Write file header offset = this.writeHeader(buffer, offset, spcState, includeID666, binaryFormat); // Write ID666 metadata if (includeID666 && metadata) { offset = this.writeID666(buffer, offset, metadata, binaryFormat); } else { // Fill with zeros if no metadata buffer.fill(0, offset, 0x100); offset = 0x100; } // Write 64KB RAM buffer.set(spcState.ram, offset); offset += ramSize; // Write DSP registers buffer.set(dspState.registers, offset); offset += dspSize; // Write unused section (zeros) buffer.fill(0, offset, offset + unusedSize); offset += unusedSize; // Write extra RAM (IPL ROM region when read-only) // For most cases, this should be zeros unless specific IPL ROM data exists buffer.fill(0, offset, offset + extraRamSize); offset += extraRamSize; // Write extended ID666 data if present if (extendedData) { buffer.set(extendedData, offset); offset += extendedData.length; } // Validate the exported data if (validate) { this.validateSPCFile(buffer); } return buffer; } /** * Write SPC file header including SPC700 register states */ static writeHeader(buffer, offset, spcState, includeID666, binaryFormat) { // Write SPC header string (exactly 27 bytes) const headerBytes = new TextEncoder().encode(this.SPC_HEADER); buffer.set(headerBytes, offset); offset = 27; // Force to be exactly at position 27 // Write signature bytes at position 27 (0x1A, 26, 26) buffer[offset++] = 0x1A; // Version marker buffer[offset++] = 0x1A; // Signature byte 1 buffer[offset++] = 0x1A; // Signature byte 2 // Write ID666 flag buffer[offset++] = includeID666 ? (binaryFormat ? 27 : 26) : 27; // Write version minor (30 for v0.30) buffer[offset++] = 30; // Write SPC700 register states // PC (16-bit little endian) buffer[offset++] = spcState.pc & 0xFF; buffer[offset++] = (spcState.pc >> 8) & 0xFF; // A, X, Y registers (8-bit each) buffer[offset++] = spcState.a & 0xFF; buffer[offset++] = spcState.x & 0xFF; buffer[offset++] = spcState.y & 0xFF; // PSW (Processor Status Word) buffer[offset++] = spcState.psw & 0xFF; // SP (Stack Pointer - lower byte only) buffer[offset++] = spcState.sp & 0xFF; // Reserved bytes (2 bytes) buffer[offset++] = 0; buffer[offset++] = 0; return offset; } /** * Write ID666 metadata tags */ static writeID666(buffer, offset, metadata, binaryFormat) { const startOffset = offset; // Song title (32 bytes) this.writeString(buffer, offset, metadata.songTitle || '', 32); offset += 32; // Game title (32 bytes) this.writeString(buffer, offset, metadata.gameTitle || '', 32); offset += 32; // Dumper name (16 bytes) this.writeString(buffer, offset, metadata.dumperName || '', 16); offset += 16; // Comments (32 bytes) this.writeString(buffer, offset, metadata.comments || '', 32); offset += 32; // Date format depends on binary vs text format if (binaryFormat) { // Binary format: YYYYMMDD (4 bytes) if (metadata.dumpDate) { const date = this.parseDateToBinary(metadata.dumpDate); buffer[offset++] = date & 0xFF; buffer[offset++] = (date >> 8) & 0xFF; buffer[offset++] = (date >> 16) & 0xFF; buffer[offset++] = (date >> 24) & 0xFF; } else { offset += 4; // Skip if no date } offset += 7; // Unused bytes in binary format } else { // Text format: MM/DD/YYYY (11 bytes) this.writeString(buffer, offset, metadata.dumpDate || '', 11); offset += 11; } // Play time (3 bytes for seconds) const playTime = metadata.playTime || 0; this.writeString(buffer, offset, playTime.toString(), 3); offset += 3; // Fade length (4-5 bytes for milliseconds) const fadeLength = metadata.fadeLength || 0; if (binaryFormat) { // Binary: 4 bytes buffer[offset++] = fadeLength & 0xFF; buffer[offset++] = (fadeLength >> 8) & 0xFF; buffer[offset++] = (fadeLength >> 16) & 0xFF; buffer[offset++] = (fadeLength >> 24) & 0xFF; } else { // Text: 5 bytes this.writeString(buffer, offset, fadeLength.toString(), 5); offset += 5; } // Artist (32 bytes) this.writeString(buffer, offset, metadata.artist || '', 32); offset += 32; // Default channel disables (1 byte) buffer[offset++] = metadata.defaultChannelDisables || 0; // Emulator used (1 byte) buffer[offset++] = metadata.emulator || 0; // Reserved bytes (fill remaining space to reach 0x100) const remainingBytes = 0x100 - (offset - startOffset + 0x2E); buffer.fill(0, offset, offset + remainingBytes); return 0x100; } /** * Create extended ID666 (xid6) chunk */ static createExtendedID666(metadata) { const chunks = []; let totalSize = 8; // Header size // Add extended fields if present if (metadata.publisher) { const chunk = this.createXID6Chunk(0x10, 1, metadata.publisher); chunks.push(chunk); totalSize += chunk.length; } if (metadata.year) { const chunk = this.createXID6Chunk(0x11, 0, metadata.year); chunks.push(chunk); totalSize += chunk.length; } if (metadata.genre) { const chunk = this.createXID6Chunk(0x12, 1, metadata.genre); chunks.push(chunk); totalSize += chunk.length; } // Create the complete extended data const buffer = new Uint8Array(totalSize); let offset = 0; // Write chunk header buffer.set(new TextEncoder().encode('xid6'), offset); offset += 4; // Write chunk size (excluding header) const dataSize = totalSize - 8; buffer[offset++] = dataSize & 0xFF; buffer[offset++] = (dataSize >> 8) & 0xFF; buffer[offset++] = (dataSize >> 16) & 0xFF; buffer[offset++] = (dataSize >> 24) & 0xFF; // Write sub-chunks for (const chunk of chunks) { buffer.set(chunk, offset); offset += chunk.length; } return buffer; } /** * Create individual xid6 sub-chunk */ static createXID6Chunk(id, type, data) { let dataBytes; let length = 0; if (type === 0) { // Data stored in header (length field) dataBytes = new Uint8Array(0); length = typeof data === 'number' ? data : 0; } else { // Data stored as string const str = typeof data === 'string' ? data : data.toString(); dataBytes = new TextEncoder().encode(str + '\0'); length = dataBytes.length; // Pad to 32-bit boundary const padding = (4 - (length % 4)) % 4; if (padding > 0) { const paddedBytes = new Uint8Array(length + padding); paddedBytes.set(dataBytes); dataBytes = paddedBytes; } } const chunk = new Uint8Array(4 + dataBytes.length); chunk[0] = id; chunk[1] = type; chunk[2] = length & 0xFF; chunk[3] = (length >> 8) & 0xFF; chunk.set(dataBytes, 4); return chunk; } /** * Write string to buffer with null padding */ static writeString(buffer, offset, str, maxLength) { const bytes = new TextEncoder().encode(str.substring(0, maxLength)); buffer.set(bytes, offset); // Null-terminate and pad for (let i = bytes.length; i < maxLength; i++) { buffer[offset + i] = 0; } } /** * Parse date string to binary format */ static parseDateToBinary(dateStr) { // Try to parse MM/DD/YYYY format const match = dateStr.match(/(\d{1,2})\/(\d{1,2})\/(\d{4})/); if (match) { const month = parseInt(match[1], 10); const day = parseInt(match[2], 10); const year = parseInt(match[3], 10); return (year * 10000) + (month * 100) + day; } // Try YYYYMMDD format const numDate = parseInt(dateStr.replace(/\D/g, ''), 10); return isNaN(numDate) ? 0 : numDate; } /** * Validate exported SPC file */ static validateSPCFile(buffer) { // Check minimum file size if (buffer.length < 0x10200) { throw new Error('SPC file too small'); } // Validate header (exactly 27 bytes) const headerStr = new TextDecoder().decode(buffer.slice(0, 27)); if (!headerStr.startsWith('SNES-SPC700 Sound File Data')) { throw new Error('Invalid SPC header'); } // Validate signature bytes at positions 27, 28, 29 if (buffer[27] !== 0x1A) { throw new Error('Invalid SPC version marker'); } if (buffer[28] !== 0x1A || buffer[29] !== 0x1A) { throw new Error('Invalid SPC signature bytes'); } // Validate ID666 flag const id666Flag = buffer[30]; if (id666Flag !== 26 && id666Flag !== 27) { throw new Error('Invalid ID666 flag'); } // Basic validation passed console.log('SPC file validation passed'); } /** * Create default SPC700 state for testing */ static createDefaultSPC700State() { return { pc: 0x0200, // Typical start address a: 0, x: 0, y: 0, psw: 0x02, // Typical initial PSW sp: 0xFF, // Full stack ram: new Uint8Array(0x10000), // 64KB RAM timer0: { value: 0, target: 0, enabled: false }, timer1: { value: 0, target: 0, enabled: false }, timer2: { value: 0, target: 0, enabled: false }, ports: { cpuToApu: new Uint8Array(4), apuToCpu: new Uint8Array(4) } }; } /** * Create default DSP state for testing */ static createDefaultDSPState() { const dspState = { registers: new Uint8Array(128), voices: [], mainVolumeLeft: 127, mainVolumeRight: 127, echoVolumeLeft: 0, echoVolumeRight: 0, keyOn: 0, keyOff: 0, flg: 0, endx: 0, efb: 0, pmon: 0, non: 0, eon: 0, dir: 0, esa: 0, edl: 0 }; // Initialize 8 voices for (let i = 0; i < 8; i++) { dspState.voices.push({ volumeLeft: 0, volumeRight: 0, pitch: 0x1000, // Default pitch sourceNumber: 0, adsr1: 0, adsr2: 0, gain: 0, envx: 0, outx: 0 }); } return dspState; } } exports.SPCExporter = SPCExporter; SPCExporter.SPC_HEADER = 'SNES-SPC700 Sound File Data'; // Exactly 27 bytes SPCExporter.SPC_SIGNATURE = [0x1A, 26, 26]; // Version markers: 0x1A followed by two 26 values SPCExporter.RAM_SIZE = 0x10000; // 64KB SPCExporter.DSP_REG_SIZE = 0x80; // 128 bytes SPCExporter.EXTRA_RAM_SIZE = 0x40; // 64 bytes // Export types for use in other modules //# sourceMappingURL=spc-exporter.js.map