UNPKG

snes-disassembler

Version:

A Super Nintendo (SNES) ROM disassembler for 65816 assembly

227 lines 8.06 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const brr_decoder_utils_1 = require("./brr-decoder-utils"); const fs_1 = require("fs"); const SAMPLE_RATE = (0, brr_decoder_utils_1.getStandardSampleRate)(); const MAX_INT16 = 32767; const MIN_INT16 = -32768; // BRR filter coefficients (from SNES9x implementation) const BRR_FILTER_COEFFS = [ [0, 0], // Filter 0 [15, 0], // Filter 1: 15/16 [61, -15], // Filter 2: 61/32 - 15/16 [115, -13] // Filter 3: 115/64 - 13/16 ]; // ADSR envelope processor class ADSRProcessor { constructor(config) { this.state = 'attack'; this.envelope = 0; this.enabled = true; this.config = config; this.enabled = config.enabled; } processSample() { if (!this.enabled) { return 1.0; } switch (this.state) { case 'attack': if (this.config.attack === 15) { this.envelope += 1024; // Linear increase +1024 at rate 31 } else { this.envelope += 32; // Linear increase +32 at specified rate } if (this.envelope >= 2047) { this.envelope = 2047; this.state = 'decay'; } break; case 'decay': this.envelope -= Math.max(1, (this.envelope - 1) >> 8); if ((this.envelope >> 8) <= this.config.sustain) { this.state = 'sustain'; } break; case 'sustain': if (this.config.release > 0) { this.envelope -= Math.max(1, (this.envelope - 1) >> 8); } break; case 'release': this.envelope -= 8; if (this.envelope < 0) { this.envelope = 0; } break; } return this.envelope / 2047.0; } keyOn() { this.envelope = 0; this.state = 'attack'; } keyOff() { this.state = 'release'; } } class BRRDecoder { constructor(brrData, adsrParams = {}, pitch = 0x1000) { this.samples = []; this.prevSamples = [0, 0]; this.loopStart = 0; this.loopEnabled = false; this.brrData = brrData; this.pitch = pitch; const adsrConfig = this.initADSR(adsrParams); this.adsrProcessor = new ADSRProcessor(adsrConfig); } initADSR(params) { return { attack: params.attack ?? 15, decay: params.decay ?? 7, sustain: params.sustain ?? 7, release: params.release ?? 0, enabled: params.enabled ?? true }; } decodeBlock(data) { const header = this.parseBRRHeader(data[0]); const samples = []; for (let i = 1; i < data.length; i++) { const byte = data[i]; for (const nibble of [(byte >> 4) & 0x0F, byte & 0x0F]) { let sample = nibble >= 8 ? nibble - 16 : nibble; sample <<= header.range; if (header.filter > 0) { const coeffs = BRR_FILTER_COEFFS[header.filter]; sample += Math.floor(coeffs[0] * this.prevSamples[0]); if (header.filter > 1) { sample += Math.floor(coeffs[1] * this.prevSamples[1]); } } sample = this.clamp16(sample); this.prevSamples[1] = this.prevSamples[0]; this.prevSamples[0] = sample; samples.push(sample); } } return samples; } parseBRRHeader(header) { return { range: header >> 4, filter: (header >> 2) & 0x03, loop: (header & 2) !== 0, end: (header & 1) !== 0 }; } clamp16(value) { return Math.max(MIN_INT16, Math.min(MAX_INT16, value)); } decode() { this.samples = []; this.adsrProcessor.keyOn(); let blockIndex = 0; while (blockIndex + 9 <= this.brrData.length) { const blockData = this.brrData.slice(blockIndex, blockIndex + 9); const header = this.parseBRRHeader(blockData[0]); const decodedSamples = this.decodeBlock(blockData); // Handle loop flag if (header.loop) { this.loopStart = this.samples.length; this.loopEnabled = true; } // Apply ADSR envelope to each sample for (const sample of decodedSamples) { const envelope = this.adsrProcessor.processSample(); const finalSample = this.clamp16(sample * envelope * 2); // Scale up for final output this.samples.push(finalSample); } // Handle end flag if (header.end) { if (this.loopEnabled && header.loop) { // In a real-time player, this would loop back to loopStart // For file decoding, we stop here break; } else { break; } } blockIndex += 9; } return this.samples; } /** * Apply Gaussian interpolation for pitch adjustment */ applyGaussianInterpolation(samples) { if (this.pitch === 0x1000) { return samples; // No pitch adjustment needed } const pitchRatio = this.pitch / 0x1000; const outputSamples = []; // Gaussian interpolation coefficients (simplified) const gaussianTable = [ 0.0, 0.0625, 0.125, 0.1875, 0.25, 0.3125, 0.375, 0.4375, 0.5, 0.5625, 0.625, 0.6875, 0.75, 0.8125, 0.875, 0.9375 ]; let pos = 0.0; while (Math.floor(pos) < samples.length - 3) { const idx = Math.floor(pos); const frac = pos - idx; const fracIndex = Math.floor(frac * 16) & 0x0F; // 4-point Gaussian interpolation const s0 = samples[Math.max(0, idx - 1)] || 0; const s1 = samples[idx] || 0; const s2 = samples[idx + 1] || 0; const s3 = samples[Math.min(samples.length - 1, idx + 2)] || 0; const g = gaussianTable[fracIndex]; const interpolated = Math.floor(s1 * (1 - g) + s2 * g + (s0 - s1 - s2 + s3) * g * (1 - g) * 0.25); outputSamples.push(this.clamp16(interpolated)); pos += pitchRatio; } return outputSamples; } exportToWAV(filename) { const numberOfSamples = this.samples.length; const waveBuffer = Buffer.alloc(44 + numberOfSamples * 2); let offset = 0; waveBuffer.write('RIFF', offset); offset += 4; waveBuffer.writeUInt32LE(36 + numberOfSamples * 2, offset); offset += 4; waveBuffer.write('WAVE', offset); offset += 4; waveBuffer.write('fmt ', offset); offset += 4; waveBuffer.writeUInt32LE(16, offset); offset += 4; waveBuffer.writeUInt16LE(1, offset); offset += 2; waveBuffer.writeUInt16LE(1, offset); offset += 2; waveBuffer.writeUInt32LE(SAMPLE_RATE, offset); offset += 4; waveBuffer.writeUInt32LE(SAMPLE_RATE * 2, offset); offset += 4; waveBuffer.writeUInt16LE(2, offset); offset += 2; waveBuffer.writeUInt16LE(16, offset); offset += 2; waveBuffer.write('data', offset); offset += 4; waveBuffer.writeUInt32LE(numberOfSamples * 2, offset); offset += 4; for (const sample of this.samples) { waveBuffer.writeInt16LE(sample, offset); offset += 2; } (0, fs_1.writeFileSync)(filename, waveBuffer); } } exports.default = BRRDecoder; //# sourceMappingURL=BRRDecoder.js.map