UNPKG

snes-disassembler

Version:

A Super Nintendo (SNES) ROM disassembler for 65816 assembly

508 lines 18.8 kB
"use strict"; /** * SPC State Extractor for SNES Disassembler * Analyzes SNES ROMs to extract SPC700 and DSP states for audio playback * * This module identifies: * - SPC700 upload routines in SNES code * - Audio data locations and formats * - DSP register configurations * - BRR sample data * - Music sequence data */ Object.defineProperty(exports, "__esModule", { value: true }); exports.SPCStateExtractor = void 0; class SPCStateExtractor { /** * Extract SPC700 and DSP states from ROM analysis */ static extractAudioState(lines, romData, cartridgeInfo) { const result = { spc700State: this.createBasicSPC700State(), dspState: this.createBasicDSPState(), audioData: [], spcUploads: [], brrSamples: [], musicSequences: [], metadata: this.extractBasicMetadata(cartridgeInfo) }; // Phase 1: Identify SPC upload routines result.spcUploads = this.findSPCUploadSequences(lines, romData); // Phase 2: Extract audio data locations result.audioData = this.findAudioDataLocations(lines, romData); // Phase 3: Identify BRR samples result.brrSamples = this.findBRRSamples(romData); // Phase 4: Find music sequence data result.musicSequences = this.findMusicSequences(lines, romData); // Phase 5: Analyze SPC upload data to build states this.analyzeSPCUploads(result); // Phase 6: Extract DSP register configurations this.extractDSPConfigurations(result, lines); // Phase 7: Enhanced metadata extraction this.enhanceMetadata(result, cartridgeInfo); return result; } /** * Find SPC upload sequences in the disassembled code */ static findSPCUploadSequences(lines, romData) { const sequences = []; for (let i = 0; i < lines.length - 10; i++) { const line = lines[i]; // Look for APU port writes which indicate SPC communication if (this.isAPUPortWrite(line)) { const sequence = this.analyzePotentialSPCUpload(lines, i, romData); if (sequence && sequence.confidence > 0.5) { sequences.push(sequence); } } } return sequences; } /** * Check if instruction writes to APU ports ($2140-$2143) */ static isAPUPortWrite(line) { if (line.instruction.mnemonic !== 'STA') return false; if (!line.operand) return false; const address = line.operand; return address >= 0x2140 && address <= 0x2143; } /** * Analyze potential SPC upload sequence starting at given line */ static analyzePotentialSPCUpload(lines, startIndex, romData) { const uploadLines = []; let confidence = 0; let dataSize = 0; let targetRamAddress = 0; let uploadMethod = 'DIRECT'; // Analyze the next 20-50 instructions for upload patterns for (let i = startIndex; i < Math.min(startIndex + 50, lines.length); i++) { const line = lines[i]; uploadLines.push(line); // Check for APU port communication patterns if (this.isAPUPortWrite(line) || this.isAPUPortRead(line)) { confidence += 0.1; } // Check for IPL boot sequence (common pattern) if (this.isIPLBootPattern(line)) { uploadMethod = 'IPL_BOOT'; confidence += 0.3; } // Check for DMA setup if (this.isDMASetup(line)) { uploadMethod = 'DMA'; confidence += 0.2; } // Check for loop patterns (data upload loops) if (this.isLoopInstruction(line.instruction.mnemonic)) { confidence += 0.1; } // Try to determine target RAM address and data size if (line.instruction.mnemonic === 'LDX' && line.operand && line.operand < 0x10000) { if (targetRamAddress === 0) targetRamAddress = line.operand; } if (line.instruction.mnemonic === 'LDY' && line.operand && line.operand < 0x8000) { if (dataSize === 0) dataSize = line.operand; } // Stop if we hit a return or jump to different section if (['RTS', 'RTL', 'JMP', 'JML'].includes(line.instruction.mnemonic)) { break; } } // Must have reasonable confidence to be considered valid if (confidence < 0.3) return null; return { startAddress: lines[startIndex].address, endAddress: uploadLines[uploadLines.length - 1].address, instructions: uploadLines, targetRamAddress, dataSize, uploadMethod, confidence: Math.min(confidence, 1.0) }; } /** * Check if instruction reads from APU ports */ static isAPUPortRead(line) { if (!['LDA', 'CMP'].includes(line.instruction.mnemonic)) return false; if (!line.operand) return false; const address = line.operand; return address >= 0x2140 && address <= 0x2143; } /** * Check for IPL boot patterns */ static isIPLBootPattern(line) { // Common IPL boot: load $CC to $2141, $BB to $2140 if (line.instruction.mnemonic === 'LDA' && line.operand === 0xCC) return true; if (line.instruction.mnemonic === 'LDA' && line.operand === 0xBB) return true; return false; } /** * Check for DMA setup instructions */ static isDMASetup(line) { if (line.instruction.mnemonic !== 'STA') return false; if (!line.operand) return false; const address = line.operand; // DMA channel registers $43xx return (address >= 0x4300 && address <= 0x437F) || address === 0x420B; } /** * Check if instruction is a loop instruction */ static isLoopInstruction(mnemonic) { return ['BNE', 'BEQ', 'BCC', 'BCS', 'BMI', 'BPL', 'DEX', 'DEY', 'CPX', 'CPY'].includes(mnemonic); } /** * Find audio data locations in ROM */ static findAudioDataLocations(lines, romData) { const locations = []; // Search for potential audio data patterns for (let i = 0; i < romData.length - 1024; i++) { // Check for BRR sample directory (common at $1000 intervals) if (i % 0x1000 === 0) { const confidence = this.analyzeBRRDirectory(romData, i); if (confidence > 0.5) { locations.push({ address: i, size: 1024, // Sample directories are typically small type: 'DSP_DATA', description: 'BRR Sample Directory', confidence }); } } // Check for music sequence data if (this.looksLikeMusicData(romData, i)) { const size = this.estimateMusicDataSize(romData, i); locations.push({ address: i, size, type: 'MUSIC_DATA', description: 'Music Sequence Data', confidence: 0.7 }); } } return locations; } /** * Analyze potential BRR directory */ static analyzeBRRDirectory(romData, offset) { let confidence = 0; // BRR directories contain 16-bit pointers to samples for (let i = 0; i < 64; i += 4) { // Check first 16 entries if (offset + i + 3 >= romData.length) break; const startAddr = romData[offset + i] | (romData[offset + i + 1] << 8); const loopAddr = romData[offset + i + 2] | (romData[offset + i + 3] << 8); // Valid BRR pointers should be reasonable if (startAddr >= 0x200 && startAddr < 0xFFFF && loopAddr >= startAddr && loopAddr < 0xFFFF) { confidence += 0.1; } } return Math.min(confidence, 1.0); } /** * Check if data looks like music sequence data */ static looksLikeMusicData(romData, offset) { if (offset + 16 >= romData.length) return false; // Music data often starts with tempo/track information const firstByte = romData[offset]; const secondByte = romData[offset + 1]; // Common music data patterns if (firstByte >= 0x40 && firstByte <= 0x80) { // Tempo range if (secondByte < 0x08) { // Track count return true; } } return false; } /** * Estimate music data size */ static estimateMusicDataSize(romData, offset) { // Simple heuristic: look for end patterns or next data block for (let i = offset + 16; i < Math.min(offset + 4096, romData.length); i++) { // Look for common end patterns if (romData[i] === 0x00 && romData[i + 1] === 0x00) { return i - offset + 2; } } return 1024; // Default size } /** * Find BRR samples in ROM data */ static findBRRSamples(romData) { const samples = []; // BRR samples are 9-byte aligned blocks for (let i = 0; i < romData.length - 9; i += 9) { if (this.isBRRBlock(romData, i)) { const sample = this.analyzeBRRSample(romData, i); if (sample) { samples.push(sample); } } } return samples; } /** * Check if data at offset is a BRR block */ static isBRRBlock(romData, offset) { if (offset + 8 >= romData.length) return false; const headerByte = romData[offset]; // BRR header byte format: RRRRLLLL // R = range (0-12), L = loop flags const range = (headerByte >> 4) & 0x0F; const filter = (headerByte >> 2) & 0x03; // Valid range is 0-15 (allow extended range as some games use it) if (range > 15) return false; // Check for reasonable filter values if (filter > 3) return false; // Retain filter validation return true; } /** * Analyze BRR sample starting at offset */ static analyzeBRRSample(romData, offset) { let size = 0; let loopPoint; let blockOffset = offset; // Read BRR blocks until end flag while (blockOffset + 8 < romData.length) { const headerByte = romData[blockOffset]; const endFlag = (headerByte & 0x01) !== 0; const loopFlag = (headerByte & 0x02) !== 0; if (loopFlag && loopPoint === undefined) { loopPoint = size; } size += 9; // Each BRR block is 9 bytes blockOffset += 9; if (endFlag) break; // Safety check - don't read beyond reasonable sample size if (size > 8192) break; } if (size < 18) return null; // Must have at least 2 blocks return { address: offset, size, loopPoint, confidence: 0.8 }; } /** * Find music sequences in ROM */ static findMusicSequences(lines, romData) { const sequences = []; // Look for music data references in the code for (const line of lines) { if (line.instruction.mnemonic === 'LDA' && line.operand) { const addr = line.operand; if (addr >= 0x8000 && this.looksLikeMusicData(romData, addr - 0x8000)) { sequences.push({ address: addr, size: this.estimateMusicDataSize(romData, addr - 0x8000), trackCount: romData[addr - 0x8000 + 1] || 1, channelMask: 0xFF, // Default to all channels confidence: 0.6 }); } } } return sequences; } /** * Analyze SPC uploads to build SPC700 state */ static analyzeSPCUploads(result) { for (const upload of result.spcUploads) { if (upload.targetRamAddress && upload.dataSize) { // Update SPC700 RAM with uploaded data // This is a simplified approach - in reality, you'd need to trace // the actual data being uploaded result.spc700State.ram = result.spc700State.ram || new Uint8Array(0x10000); // Set typical initial state for uploaded SPC result.spc700State.pc = upload.targetRamAddress; result.spc700State.sp = 0xFF; result.spc700State.psw = 0x02; } } } /** * Extract DSP register configurations */ static extractDSPConfigurations(result, lines) { // Look for DSP register setup in the code for (const line of lines) { if (line.instruction.mnemonic === 'STA' && line.operand === 0x2140) { // This could be setting up DSP registers via APU // More sophisticated analysis would track the data flow } } // Set up basic DSP state with default values if (!result.dspState.registers) { result.dspState.registers = new Uint8Array(128); // Set some reasonable defaults result.dspState.mainVolumeLeft = 127; result.dspState.mainVolumeRight = 127; result.dspState.flg = 0x20; // Mute flag clear } } /** * Create basic SPC700 state */ static createBasicSPC700State() { return { pc: 0x0200, a: 0, x: 0, y: 0, psw: 0x02, sp: 0xFF, ram: new Uint8Array(0x10000), 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 basic DSP state */ static createBasicDSPState() { return { registers: new Uint8Array(128), voices: Array.from({ length: 8 }, () => ({ volumeLeft: 0, volumeRight: 0, pitch: 0x1000, sourceNumber: 0, adsr1: 0, adsr2: 0, gain: 0, envx: 0, outx: 0 })), mainVolumeLeft: 127, mainVolumeRight: 127, echoVolumeLeft: 0, echoVolumeRight: 0, keyOn: 0, keyOff: 0, flg: 0x20, endx: 0, efb: 0, pmon: 0, non: 0, eon: 0, dir: 0, esa: 0, edl: 0 }; } /** * Extract basic metadata from cartridge info */ static extractBasicMetadata(cartridgeInfo) { return { gameTitle: 'Unknown Game', // TODO: Extract title from ROM header when available dumperName: 'SNES Disassembler', dumpDate: new Date().toLocaleDateString('en-US'), emulator: 0, // Unknown playTime: 120, // Default 2 minutes fadeLength: 5000 // Default 5 second fade }; } /** * Enhance metadata with additional information */ static enhanceMetadata(result, cartridgeInfo) { // Add information based on ROM analysis if (result.musicSequences.length > 0) { result.metadata.comments = `${result.musicSequences.length} music sequences found`; } if (result.brrSamples.length > 0) { const samplesComment = result.metadata.comments || ''; result.metadata.comments = `${samplesComment} ${result.brrSamples.length} BRR samples`.trim(); } // TODO: Add publisher info when available from ROM header analysis } } exports.SPCStateExtractor = SPCStateExtractor; SPCStateExtractor.SPC_UPLOAD_PATTERNS = [ // Common SPC upload patterns { pattern: [0xCD, 0x00, 0x21], mask: [0xFF, 0x00, 0xFF], description: 'CMP $2100 (waiting for SPC)' }, { pattern: [0x8D, 0x40, 0x21], mask: [0xFF, 0xFF, 0xFF], description: 'STA $2140 (APU port 0)' }, { pattern: [0x8D, 0x41, 0x21], mask: [0xFF, 0xFF, 0xFF], description: 'STA $2141 (APU port 1)' }, { pattern: [0x8D, 0x42, 0x21], mask: [0xFF, 0xFF, 0xFF], description: 'STA $2142 (APU port 2)' }, { pattern: [0x8D, 0x43, 0x21], mask: [0xFF, 0xFF, 0xFF], description: 'STA $2143 (APU port 3)' } ]; SPCStateExtractor.BRR_SIGNATURES = [ // BRR sample signatures (9-byte BRR blocks) { pattern: [0x00, 0x00, 0x00, 0x00], offset: 0, description: 'BRR silent block' }, { pattern: [0x01], offset: 0, mask: 0x0F, description: 'BRR block with loop flag' }, { pattern: [0x03], offset: 0, mask: 0x0F, description: 'BRR block with end flag' } ]; SPCStateExtractor.DSP_REGISTER_MAP = { // Voice registers (8 voices * 16 bytes each) 0x00: 'V0VOLL', // Voice 0 Volume Left 0x01: 'V0VOLR', // Voice 0 Volume Right 0x02: 'V0PITCHL', // Voice 0 Pitch Low 0x03: 'V0PITCHH', // Voice 0 Pitch High 0x04: 'V0SRCN', // Voice 0 Source Number 0x05: 'V0ADSR1', // Voice 0 ADSR1 0x06: 'V0ADSR2', // Voice 0 ADSR2 0x07: 'V0GAIN', // Voice 0 GAIN 0x08: 'V0ENVX', // Voice 0 Current Envelope 0x09: 'V0OUTX', // Voice 0 Current Output // ... (repeat for voices 1-7) // Global registers 0x0C: 'MVOLL', // Main Volume Left 0x1C: 'MVOLR', // Main Volume Right 0x2C: 'EVOLL', // Echo Volume Left 0x3C: 'EVOLR', // Echo Volume Right 0x4C: 'KON', // Key On 0x5C: 'KOFF', // Key Off 0x6C: 'FLG', // Reset, Mute, Echo-Write flags and Noise Clock 0x7C: 'ENDX', // Voice End flags 0x0D: 'EFB', // Echo Feedback 0x2D: 'PMON', // Pitch Modulation 0x3D: 'NON', // Noise Enable 0x4D: 'EON', // Echo Enable 0x5D: 'DIR', // Sample Directory 0x6D: 'ESA', // Echo Start Address 0x7D: 'EDL' // Echo Delay }; //# sourceMappingURL=spc-state-extractor.js.map