UNPKG

snes-disassembler

Version:

A Super Nintendo (SNES) ROM disassembler for 65816 assembly

214 lines 7.7 kB
"use strict"; // ============================================================================= // Custom Error Types // ============================================================================= Object.defineProperty(exports, "__esModule", { value: true }); exports.BRRDecodingError = void 0; exports.clamp = clamp; exports.applyFilter = applyFilter; exports.decodeBrrFile = decodeBrrFile; exports.convertSampleRateGaussian = convertSampleRateGaussian; exports.getStandardSampleRate = getStandardSampleRate; exports.calculatePitchRatio = calculatePitchRatio; /** * Error thrown when BRR decoding fails */ class BRRDecodingError extends Error { constructor(message) { super(message); this.name = 'BRRDecodingError'; } } exports.BRRDecodingError = BRRDecodingError; /** * Error thrown when sample rate conversion fails */ class SampleRateConversionError extends Error { constructor(message) { super(message); this.name = 'SampleRateConversionError'; } } // ============================================================================= // BRR Block Class (equivalent to Python BRRBlock) // ============================================================================= /** * Represents a 9-byte BRR block containing 16 samples */ class BRRBlock { constructor(data) { if (data.length !== 9) { throw new BRRDecodingError('BRR block must be exactly 9 bytes'); } // Parse header byte (SSSS FFLE) const header = data[0]; this.range = header >> 4; // Left-shift amount (S) this.filter = (header >> 2) & 3; // Decoding filter (F) this.loopFlag = (header & 2) !== 0; // Loop flag (L) this.endFlag = (header & 1) !== 0; // End flag (E) // Sample data bytes (8 bytes containing 16 4-bit samples) this.data = data.slice(1); } /** * Extract 16 4-bit samples from the 8 data bytes (high nibble first) */ decodeNibbles() { const samples = []; for (const byte of this.data) { // High nibble first, then low nibble const high = (byte >> 4) & 0x0F; const low = byte & 0x0F; // Convert to signed 4-bit values (-8 to +7) const signedHigh = high >= 8 ? high - 16 : high; const signedLow = low >= 8 ? low - 16 : low; samples.push(signedHigh, signedLow); } return samples; } } // ============================================================================= // Utility Functions // ============================================================================= /** * Clamp a sample value to 16-bit signed integer range */ function clamp(sample) { return Math.max(Math.min(sample, 32767), -32768); } /** * Apply IIR filtering with previous samples */ function applyFilter(filterNum, samples, prev1, prev2) { const filteredSamples = []; let currentPrev1 = prev1; let currentPrev2 = prev2; for (const sample of samples) { let filteredSample; if (filterNum === 0) { // No filter filteredSample = sample; } else if (filterNum === 1) { // Linear filter: s' = s + p1 * 15/16 filteredSample = sample + Math.floor(currentPrev1 * 15 / 16); } else if (filterNum === 2) { // Quadratic filter: s' = s + p1 * 61/32 - p2 * 15/16 filteredSample = sample + Math.floor(currentPrev1 * 61 / 32) - Math.floor(currentPrev2 * 15 / 16); } else if (filterNum === 3) { // Cubic filter: s' = s + p1 * 115/64 - p2 * 13/16 filteredSample = sample + Math.floor(currentPrev1 * 115 / 64) - Math.floor(currentPrev2 * 13 / 16); } else { filteredSample = sample; } // Clamp to 16-bit range filteredSample = clamp(filteredSample); filteredSamples.push(filteredSample); // Update previous samples currentPrev2 = currentPrev1; currentPrev1 = filteredSample; } return { filteredSamples, prev1: currentPrev1, prev2: currentPrev2 }; } /** * Decode complete BRR file to 16-bit PCM samples */ function decodeBrrFile(data, enableLooping = false) { const pcmSamples = []; let prev1 = 0; let prev2 = 0; let loopStartPosition = null; let position = 0; while (position < data.length) { if (position + 9 > data.length) { break; // Not enough data for a complete block } const block = new BRRBlock(data.slice(position, position + 9)); position += 9; // Mark loop start position if loop flag is set if (block.loopFlag) { loopStartPosition = pcmSamples.length; } // Decode 16 samples from this block let samples = block.decodeNibbles(); // Apply range (left-shift) if (block.range > 0) { samples = samples.map(s => s << block.range); } // Apply BRR filter const filterResult = applyFilter(block.filter, samples, prev1, prev2); pcmSamples.push(...filterResult.filteredSamples); prev1 = filterResult.prev1; prev2 = filterResult.prev2; // Handle end flag if (block.endFlag) { if (enableLooping && block.loopFlag && loopStartPosition !== null) { // Loop back to the loop start position // In a real implementation, this would continue playing from loop point // For file decoding, we'll just stop here break; } else { // End of sample break; } } } return pcmSamples; } // ============================================================================= // Sample Rate Conversion Helpers // ============================================================================= /** * Convert sample rate using Gaussian interpolation (higher quality) */ function convertSampleRateGaussian(samples, fromRate, toRate) { if (fromRate === toRate) { return samples; } if (fromRate <= 0 || toRate <= 0) { throw new SampleRateConversionError('Sample rates must be positive'); } // 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 ]; const ratio = fromRate / toRate; const output = []; 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); output.push(clamp(interpolated)); pos += ratio; } return output; } /** * Get the standard SNES BRR sample rate */ function getStandardSampleRate() { return 32000; } /** * Calculate pitch ratio from SNES pitch value */ function calculatePitchRatio(pitch) { return pitch / 0x1000; } // ============================================================================= // BRR Block Parsing Utilities // ============================================================================= //# sourceMappingURL=brr-decoder-utils.js.map