/**
 * Stateful windowed-sinc resampler for the tab-audio -> ASR path.
 *
 * AudioWorklet delivers short render quanta.  A stateless linear interpolator
 * at every quantum creates discontinuities and aliases high-frequency content
 * into the 16 kHz speech band.  This small FIR implementation keeps its input
 * history and fractional phase between calls, so it is deterministic in the
 * extension and independently testable without WebAudio.
 */
export class BandLimitedResampler {
    ratio;
    cutoff;
    radius;
    samples = [];
    position;
    constructor(fromRate, toRate, taps = 32) {
        if (!Number.isFinite(fromRate) || !Number.isFinite(toRate) || fromRate <= 0 || toRate <= 0)
            throw new Error("invalid_resample_rate");
        if (!Number.isInteger(taps) || taps < 8 || taps % 2 !== 0)
            throw new Error("invalid_resample_taps");
        this.ratio = fromRate / toRate;
        // Normalised to the input sample rate.  Keep a small transition band so
        // frequencies above the output Nyquist are attenuated before decimation.
        this.cutoff = 0.45 / Math.max(1, this.ratio);
        this.radius = taps / 2;
        this.position = this.radius;
    }
    push(input) {
        for (const value of input)
            this.samples.push(value);
        return this.render(false);
    }
    /** Flush only when a capture session ends; zero-padding emits the FIR tail. */
    flush() {
        for (let index = 0; index < this.radius; index++)
            this.samples.push(0);
        return this.render(true);
    }
    render(flushing) {
        const values = [];
        const lastNeeded = flushing ? this.samples.length : this.samples.length - this.radius - 1;
        while (this.position <= lastNeeded) {
            const center = Math.floor(this.position);
            let sum = 0;
            let weightSum = 0;
            for (let offset = -this.radius + 1; offset <= this.radius; offset++) {
                const index = center + offset;
                if (index < 0 || index >= this.samples.length)
                    continue;
                const distance = index - this.position;
                const window = 0.5 + 0.5 * Math.cos(Math.PI * distance / this.radius);
                const weight = 2 * this.cutoff * sinc(2 * this.cutoff * distance) * window;
                sum += this.samples[index] * weight;
                weightSum += weight;
            }
            values.push(weightSum ? sum / weightSum : 0);
            this.position += this.ratio;
        }
        const discard = Math.max(0, Math.floor(this.position) - this.radius);
        if (discard) {
            this.samples.splice(0, discard);
            this.position -= discard;
        }
        return Float32Array.from(values);
    }
}
function sinc(value) {
    return Math.abs(value) < 1e-8 ? 1 : Math.sin(Math.PI * value) / (Math.PI * value);
}
