snes-disassembler
Version:
A Super Nintendo (SNES) ROM disassembler for 65816 assembly
214 lines • 7.7 kB
JavaScript
;
// =============================================================================
// 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