/**
* ISO 226:2023 Equal-Loudness-Level Contour Model.
* This object contains the data and methods to calculate the Sound Pressure Level (SPL)
* for a given loudness level (phon) and frequency, based on the international standard.
*/
const ISO226_MODEL = {
// Data from ISO 226:2023, Table 1
frequencies: [20, 25, 31.5, 40, 50, 63, 80, 100, 125, 160, 200, 250, 315, 400, 500, 630, 800, 1000, 1250, 1600, 2000, 2500, 3150, 4000, 5000, 6300, 8000, 10000, 12500],
alpha_f: [0.635, 0.602, 0.569, 0.537, 0.509, 0.482, 0.456, 0.433, 0.412, 0.391, 0.373, 0.357, 0.343, 0.330, 0.320, 0.311, 0.303, 0.300, 0.295, 0.292, 0.290, 0.290, 0.289, 0.289, 0.289, 0.293, 0.303, 0.323, 0.354],
L_U: [-31.5, -27.2, -23.1, -19.3, -16.1, -13.1, -10.4, -8.2, -6.3, -4.6, -3.2, -2.1, -1.2, -0.5, 0.0, 0.4, 0.5, 0.0, -2.7, -4.2, -1.2, 1.4, 2.3, 1.0, -2.3, -7.2, -11.2, -10.9, -3.5],
T_f: [78.1, 68.7, 59.5, 51.1, 44.0, 37.5, 31.5, 26.5, 22.1, 17.9, 14.4, 11.4, 8.6, 6.2, 4.4, 3.0, 2.2, 2.4, 3.5, 1.7, -1.3, -4.2, -6.0, -5.4, -1.5, 6.0, 12.6, 13.9, 12.3],
// Helper to find interpolation indices and factor for a given frequency
_getInterp(freq) {
if (freq <= this.frequencies[0]) return { i0: 0, i1: 0, factor: 1 };
if (freq >= this.frequencies[this.frequencies.length - 1]) return { i0: this.frequencies.length - 1, i1: this.frequencies.length - 1, factor: 1 };
const i1 = this.frequencies.findIndex(f => f >= freq);
const i0 = i1 - 1;
// Logarithmic interpolation for frequency
const f0 = this.frequencies[i0];
const f1 = this.frequencies[i1];
const factor = (Math.log(freq) - Math.log(f0)) / (Math.log(f1) - Math.log(f0));
return { i0, i1, factor };
},
// Helper to get an interpolated parameter value for a given frequency
_getParam(freq, paramArray) {
const { i0, i1, factor } = this._getInterp(freq);
if (i0 === i1) return paramArray[i0];
// Linear interpolation for parameters
return paramArray[i0] * (1 - factor) + paramArray[i1] * factor;
},
/**
* Calculates the required Sound Pressure Level (SPL) for a given loudness (in phon)
* at a specific frequency.
*/
getSpl(phon, freq) {
const af = this._getParam(freq, this.alpha_f);
const Lu = this._getParam(freq, this.L_U);
const Tf = this._getParam(freq, this.T_f);
const B = Math.pow(4e-10, 0.3 - af);
const C = Math.pow(10, 0.072);
const term1 = Math.pow(10, 3 * phon / 100) - C;
if (term1 < 0) return Tf; // Below threshold, SPL is the threshold of hearing
const term2 = B * term1 + Math.pow(10, af * (Tf + Lu) / 10);
const spl = (10 / af) * Math.log10(term2) - Lu;
return spl;
}
};
/**
* Finds the starting index in a frequency array for a given frequency value.
* @param {number} freq The frequency to find.
* @param {Float32Array|number[]} frequencies The sorted array of frequencies.
* @returns {number} The index of the first frequency greater than or equal to the target.
*/
function freqToIndex(freq, frequencies) {
const index = frequencies.findIndex(f => f >= freq);
return index === -1 ? frequencies.length - 1 : index;
}
/**
* Calculates the necessary volume adjustment to level a speaker's response to a target
* curve, based on an equal-loudness weighting derived from ISO 226.
*
* This function emphasizes differences in frequency regions where human hearing is more
* sensitive, aiming for a perceptually balanced level match.
*
* @param {Float32Array} responseToAdjust - The measured magnitude response of the speaker (in dB).
* @param {Float32Array} targetMagnitude - The target magnitude response (in dB).
* @param {Float32Array} frequencies - The frequency points corresponding to the magnitude arrays.
* @param {number} startFreq - The lower frequency bound for the calculation.
* @param {number} endFreq - The upper frequency bound for the calculation.
* @returns {number} The calculated volume offset (in dB) that should be applied.
*/
function calculateEqualLoudnessOffset(responseToAdjust, targetMagnitude, frequencies, startFreq, endFreq) {
let weightedSumDiff = 0;
let totalWeight = 0;
const startIndex = freqToIndex(startFreq, frequencies);
const endIndex = freqToIndex(endFreq, frequencies);
// A reference loudness of 60 phon is used to determine the perceptual weights.
// This is a typical level for moderate listening.
const referenceLoudness = 60;
const refSplAt1k = referenceLoudness; // At 1kHz, phon is equal to dB SPL by definition.
for (let i = startIndex; i <= endIndex; i++) {
// Ensure both magnitude values are valid numbers
if (isFinite(responseToAdjust[i]) && isFinite(targetMagnitude[i])) {
// Calculate the perceptual weight for the current frequency.
// Frequencies where our hearing is more sensitive (e.g., 2-5kHz) will get
// a higher weight because the equal-loudness contour is lower there.
const splFor60Phon = ISO226_MODEL.getSpl(referenceLoudness, frequencies[i]);
const weight = Math.pow(10, (refSplAt1k - splFor60Phon) / 10);
// Calculate the difference between the target and the measured response.
const diff = targetMagnitude[i] - responseToAdjust[i];
// Apply the weight to the difference.
weightedSumDiff += diff * weight;
totalWeight += weight;
}
}
// The final adjustment is the average of the weighted differences.
return (totalWeight === 0) ? 0 : weightedSumDiff / totalWeight;
}