UNPKG

web-audio-api

Version:
146 lines (125 loc) 5.05 kB
import AudioNode from './AudioNode.js' import { fft as computeFFT } from 'fourier-transform' import { DOMErr } from './errors.js' import { BLOCK_SIZE } from './constants.js' class AnalyserNode extends AudioNode { #fftSize = 2048 #minDecibels = -100 #maxDecibels = -30 #smoothingTimeConstant = 0.8 #timeBuf // circular time-domain buffer #writePos = 0 #prevSpectrum // smoothed magnitude spectrum #spectrum // pre-allocated output #windowedBuf // pre-allocated windowed input for FFT get fftSize() { return this.#fftSize } set fftSize(val) { if (val < 32 || val > 32768 || (val & (val - 1)) !== 0) throw DOMErr('fftSize must be power of 2 between 32 and 32768', 'IndexSizeError') this.#fftSize = val this._allocBuffers(val) } get frequencyBinCount() { return this.#fftSize / 2 } get minDecibels() { return this.#minDecibels } set minDecibels(val) { if (val >= this.#maxDecibels) throw DOMErr('minDecibels must be less than maxDecibels', 'IndexSizeError') this.#minDecibels = val } get maxDecibels() { return this.#maxDecibels } set maxDecibels(val) { if (val <= this.#minDecibels) throw DOMErr('maxDecibels must be greater than minDecibels', 'IndexSizeError') this.#maxDecibels = val } get smoothingTimeConstant() { return this.#smoothingTimeConstant } set smoothingTimeConstant(val) { if (val < 0 || val > 1) throw DOMErr('smoothingTimeConstant must be between 0 and 1', 'IndexSizeError') this.#smoothingTimeConstant = val } constructor(context, options) { options = AudioNode._checkOpts(options) super(context, 1, 1, undefined, 'max', 'speakers') if (options.fftSize !== undefined) this.fftSize = options.fftSize // Set both dB values before validating (constructor may provide both) if (options.minDecibels !== undefined || options.maxDecibels !== undefined) { let min = options.minDecibels ?? this.#minDecibels let max = options.maxDecibels ?? this.#maxDecibels if (min >= max) throw DOMErr('minDecibels must be less than maxDecibels', 'IndexSizeError') this.#minDecibels = min this.#maxDecibels = max } if (options.smoothingTimeConstant !== undefined) this.smoothingTimeConstant = options.smoothingTimeConstant this._allocBuffers(this.#fftSize) this._applyOpts(options) // Register as tail node so _tick() is called during rendering // even when not connected to destination (spec: AnalyserNode captures input data) context._tailNodes?.add(this) } _allocBuffers(n) { this.#timeBuf = new Float32Array(n) this.#prevSpectrum = new Float64Array(n / 2) this.#spectrum = new Float64Array(n / 2) this.#windowedBuf = new Float64Array(n) this.#writePos = 0 } _tick() { super._tick() let inBuf = this._inputs[0]._tick() let ch0 = inBuf.getChannelData(0) let n = this.#fftSize for (let i = 0; i < BLOCK_SIZE; i++) this.#timeBuf[(this.#writePos + i) % n] = ch0[i] this.#writePos = (this.#writePos + BLOCK_SIZE) % n return inBuf } getFloatTimeDomainData(array) { let n = this.#fftSize for (let i = 0; i < Math.min(array.length, n); i++) array[i] = this.#timeBuf[(this.#writePos + i) % n] } getByteTimeDomainData(array) { let n = this.#fftSize for (let i = 0; i < Math.min(array.length, n); i++) { let val = this.#timeBuf[(this.#writePos + i) % n] array[i] = Math.max(0, Math.min(255, Math.round((val + 1) * 128))) } } getFloatFrequencyData(array) { let spectrum = this._computeSpectrum() let n = Math.min(array.length, spectrum.length) for (let i = 0; i < n; i++) array[i] = spectrum[i] > 0 ? 20 * Math.log10(spectrum[i]) : -120 } getByteFrequencyData(array) { let spectrum = this._computeSpectrum() let range = this.#maxDecibels - this.#minDecibels let n = Math.min(array.length, spectrum.length) for (let i = 0; i < n; i++) { let dB = spectrum[i] > 0 ? 20 * Math.log10(spectrum[i]) : -120 let scaled = (dB - this.#minDecibels) / range array[i] = Math.max(0, Math.min(255, Math.round(scaled * 255))) } } _computeSpectrum() { let n = this.#fftSize let bins = n / 2 let windowed = this.#windowedBuf // copy ordered time-domain data + apply Blackman window for (let i = 0; i < n; i++) { let w = 0.42 - 0.5 * Math.cos(2 * Math.PI * i / n) + 0.08 * Math.cos(4 * Math.PI * i / n) windowed[i] = this.#timeBuf[(this.#writePos + i) % n] * w } // split-radix FFT → complex spectrum { re, im } with N/2+1 bins let [re, im] = computeFFT(windowed) // compute magnitude spectrum with smoothing let smooth = this.#smoothingTimeConstant let prev = this.#prevSpectrum let spectrum = this.#spectrum for (let i = 0; i < bins; i++) { let mag = Math.sqrt(re[i] * re[i] + im[i] * im[i]) / n spectrum[i] = smooth * prev[i] + (1 - smooth) * mag prev[i] = spectrum[i] } return spectrum } } export default AnalyserNode