Files

154 lines
5.8 KiB
JavaScript

import { fftInPlace } from "./fft.js";
import { interpolateFrequencyResponse } from "./frequency-response.js";
import { TAU } from "./math.js";
export function nextPowerOfTwo(value) {
if (!Number.isFinite(value) || value < 1) throw new Error(`유효하지 않은 FFT 길이: ${value}`);
let result = 1;
while (result < value) result *= 2;
return result;
}
export function prepareDelaySpectrum(samples, maximumDelaySamples = 0) {
const fftSize = nextPowerOfTwo(samples.length + Math.ceil(maximumDelaySamples) + 2);
const real = new Float64Array(fftSize);
const imaginary = new Float64Array(fftSize);
real.set(samples);
fftInPlace(real, imaginary);
return { real, imaginary, fftSize };
}
export function createDelayWorkspace(fftSize) {
return { real: new Float64Array(fftSize), imaginary: new Float64Array(fftSize) };
}
export function addFrequencyDomainPaths(options) {
const {
output,
spectrum,
paths,
gain = 1,
phaseOffsetDeg = 0,
frequencyResponse,
sampleRate,
workspace,
outputOffsetSamples = 0,
pathDelaySampleRate,
} = options;
if (!Array.isArray(paths) || paths.length === 0) {
throw new Error("frequency-domain path가 하나 이상 필요합니다.");
}
const delaySamplesForPath = (path) => {
if (path.delaySamples !== undefined) return path.delaySamples;
if (
path.delaySeconds !== undefined &&
pathDelaySampleRate > 0 &&
Number.isFinite(pathDelaySampleRate)
) {
return path.delaySeconds * pathDelaySampleRate;
}
return undefined;
};
for (const path of paths) {
const delaySamples = delaySamplesForPath(path);
if (!(delaySamples >= 0) || !Number.isFinite(delaySamples)) {
throw new Error("유효하지 않은 delay: " + delaySamples);
}
if (!Number.isFinite(path.gain)) throw new Error("path gain은 유한한 숫자여야 합니다.");
}
if (workspace.real.length !== spectrum.fftSize || workspace.imaginary.length !== spectrum.fftSize) {
throw new Error("fractional-delay workspace 크기가 spectrum과 다릅니다.");
}
if (!Number.isSafeInteger(outputOffsetSamples) || outputOffsetSamples < 0) {
throw new RangeError("outputOffsetSamples must be a non-negative safe integer.");
}
const hasFrequencyResponse = Array.isArray(frequencyResponse) && frequencyResponse.length > 0;
if (hasFrequencyResponse && (!(sampleRate > 0) || !Number.isFinite(sampleRate))) {
throw new Error("frequencyResponse 적용에는 유효한 sampleRate가 필요합니다.");
}
const size = spectrum.fftSize;
const calibrationPhase = (phaseOffsetDeg * Math.PI) / 180;
const response = {};
workspace.real.fill(0);
workspace.imaginary.fill(0);
const halfSize = size / 2;
const phaseReanchorInterval = 1024;
for (const path of paths) {
const phaseStep = (-TAU * delaySamplesForPath(path)) / size;
const stepReal = Math.cos(phaseStep);
const stepImaginary = Math.sin(phaseStep);
let phaseReal = 1;
let phaseImaginary = 0;
for (let bin = 0; bin <= halfSize; bin += 1) {
workspace.real[bin] += phaseReal * path.gain;
workspace.imaginary[bin] += phaseImaginary * path.gain;
if (bin > 0 && bin < halfSize) {
const negativeBin = size - bin;
workspace.real[negativeBin] += phaseReal * path.gain;
workspace.imaginary[negativeBin] -= phaseImaginary * path.gain;
}
const nextBin = bin + 1;
if (nextBin % phaseReanchorInterval === 0) {
phaseReal = Math.cos(phaseStep * nextBin);
phaseImaginary = Math.sin(phaseStep * nextBin);
} else {
const nextReal = phaseReal * stepReal - phaseImaginary * stepImaginary;
phaseImaginary = phaseReal * stepImaginary + phaseImaginary * stepReal;
phaseReal = nextReal;
}
}
}
for (let bin = 0; bin < size; bin += 1) {
const signedBin = bin <= size / 2 ? bin : bin - size;
const phaseSign = signedBin === 0 ? 0 : signedBin > 0 ? 1 : -1;
let responseGain = 1;
let responsePhase = 0;
if (hasFrequencyResponse) {
interpolateFrequencyResponse(
frequencyResponse,
(Math.abs(signedBin) * sampleRate) / size,
response,
);
responseGain = response.linearGain;
responsePhase = response.phaseRadians;
}
const pathReal = workspace.real[bin];
const pathImaginary = workspace.imaginary[bin];
const responseAndCalibrationPhase = phaseSign * (calibrationPhase + responsePhase);
const calibrationReal = Math.cos(responseAndCalibrationPhase) * responseGain * gain;
const calibrationImaginary = Math.sin(responseAndCalibrationPhase) * responseGain * gain;
const transferReal =
pathReal * calibrationReal - pathImaginary * calibrationImaginary;
const transferImaginary =
pathReal * calibrationImaginary + pathImaginary * calibrationReal;
const inputReal = spectrum.real[bin];
const inputImaginary = spectrum.imaginary[bin];
workspace.real[bin] =
inputReal * transferReal - inputImaginary * transferImaginary;
workspace.imaginary[bin] =
inputReal * transferImaginary + inputImaginary * transferReal;
}
// A real sequence has a real-valued Nyquist coefficient. Projecting this single self-conjugate
// bin back to the real axis preserves Hermitian symmetry for fractional delays.
workspace.imaginary[size / 2] = 0;
fftInPlace(workspace.real, workspace.imaginary, true);
const outputLength = Math.min(
output.length,
Math.max(0, workspace.real.length - outputOffsetSamples),
);
for (let sampleIndex = 0; sampleIndex < outputLength; sampleIndex += 1) {
output[sampleIndex] += workspace.real[outputOffsetSamples + sampleIndex];
}
}
export function addFrequencyDomainDelay(options) {
const { delaySamples, gain = 1, ...shared } = options;
return addFrequencyDomainPaths({
...shared,
paths: [{ delaySamples, gain }],
});
}