UNPKG

web-audio-api

Version:
157 lines (128 loc) 5.47 kB
import AudioNode from './AudioNode.js' import AudioBuffer from 'audio-buffer' import { BLOCK_SIZE } from './constants.js' import { DOMErr } from './errors.js' import iir from 'digital-filter/core/iir.js' const MAX_COEF = 20 class IIRFilterNode extends AudioNode { #feedforward #feedback #state // per-channel state constructor(context, options) { if (!(context && typeof context === 'object' && 'sampleRate' in context)) throw new TypeError('First argument must be an AudioContext') if (!options || typeof options !== 'object') throw new TypeError('Second argument must be an IIRFilterOptions dictionary') let feedforward = options.feedforward let feedback = options.feedback // Required members per spec if (feedforward === undefined || feedforward === null) throw new TypeError('feedforward is required') if (feedback === undefined || feedback === null) throw new TypeError('feedback is required') // Convert to array (handles TypedArrays, array-like) feedforward = Array.from(feedforward) feedback = Array.from(feedback) // Non-finite check (TypeError per spec) - must come before length checks for (let i = 0; i < feedforward.length; i++) { let v = +feedforward[i] if (!Number.isFinite(v)) throw new TypeError('feedforward coefficient at index ' + i + ' is not finite') feedforward[i] = v } for (let i = 0; i < feedback.length; i++) { let v = +feedback[i] if (!Number.isFinite(v)) throw new TypeError('feedback coefficient at index ' + i + ' is not finite') feedback[i] = v } // Empty arrays -> NotSupportedError if (feedforward.length === 0) throw DOMErr('feedforward must not be empty', 'NotSupportedError') if (feedback.length === 0) throw DOMErr('feedback must not be empty', 'NotSupportedError') // Max 20 coefficients -> NotSupportedError if (feedforward.length > MAX_COEF) throw DOMErr('feedforward length exceeds ' + MAX_COEF, 'NotSupportedError') if (feedback.length > MAX_COEF) throw DOMErr('feedback length exceeds ' + MAX_COEF, 'NotSupportedError') // All-zero feedforward -> InvalidStateError if (feedforward.every(v => v === 0)) throw DOMErr('feedforward coefficients must not all be zero', 'InvalidStateError') // feedback[0] === 0 -> InvalidStateError if (feedback[0] === 0) throw DOMErr('feedback[0] must be non-zero', 'InvalidStateError') super(context, 1, 1, undefined, 'max', 'speakers') this._applyOpts(options) this.#feedforward = Float64Array.from(feedforward) this.#feedback = Float64Array.from(feedback) // normalize by a0 let a0 = this.#feedback[0] if (a0 !== 1) { for (let i = 0; i < this.#feedforward.length; i++) this.#feedforward[i] /= a0 for (let i = 0; i < this.#feedback.length; i++) this.#feedback[i] /= a0 } this.#state = [] this._outBuf = null this._outCh = 0 } getFrequencyResponse(frequencyHz, magResponse, phaseResponse) { // Null/undefined check per spec -> TypeError if (!frequencyHz || !(frequencyHz instanceof Float32Array)) throw new TypeError('frequencyHz must be a Float32Array') if (!magResponse || !(magResponse instanceof Float32Array)) throw new TypeError('magResponse must be a Float32Array') if (!phaseResponse || !(phaseResponse instanceof Float32Array)) throw new TypeError('phaseResponse must be a Float32Array') // Length mismatch -> InvalidAccessError if (magResponse.length < frequencyHz.length) throw DOMErr('magResponse length must be >= frequencyHz length', 'InvalidAccessError') if (phaseResponse.length < frequencyHz.length) throw DOMErr('phaseResponse length must be >= frequencyHz length', 'InvalidAccessError') let sr = this.context.sampleRate let nyquist = sr / 2 let ff = this.#feedforward, fb = this.#feedback for (let i = 0; i < frequencyHz.length; i++) { let freq = frequencyHz[i] // Out-of-range frequencies (< 0 or > Nyquist) -> NaN per spec if (freq < 0 || freq > nyquist) { magResponse[i] = NaN phaseResponse[i] = NaN continue } let w = 2 * Math.PI * freq / sr let numR = 0, numI = 0, denR = 0, denI = 0 for (let k = 0; k < ff.length; k++) { numR += ff[k] * Math.cos(k * w) numI -= ff[k] * Math.sin(k * w) } for (let k = 0; k < fb.length; k++) { denR += fb[k] * Math.cos(k * w) denI -= fb[k] * Math.sin(k * w) } let denMag = denR * denR + denI * denI let realPart = (numR * denR + numI * denI) / denMag let imagPart = (numI * denR - numR * denI) / denMag magResponse[i] = Math.sqrt(realPart * realPart + imagPart * imagPart) phaseResponse[i] = Math.atan2(imagPart, realPart) } } _tick() { super._tick() let inBuf = this._inputs[0]._tick() let ch = inBuf.numberOfChannels let sr = this.context.sampleRate if (ch !== this._outCh) { this._outBuf = new AudioBuffer(ch, BLOCK_SIZE, sr) this._outCh = ch this.#state = Array.from({ length: ch }, () => ({ b: this.#feedforward, a: this.#feedback })) } for (let c = 0; c < ch; c++) { let out = this._outBuf.getChannelData(c) out.set(inBuf.getChannelData(c)) iir(out, this.#state[c]) } return this._outBuf } } export default IIRFilterNode