web-audio-api
Version:
Portable Web Audio API
94 lines (79 loc) • 3.45 kB
JavaScript
// PeriodicWave — custom waveforms for OscillatorNode
// Stores wavetable generated from Fourier coefficients (W3C spec convention)
import { DOMErr } from './errors.js'
import buildWavetable from 'periodic-function/wavetable.js'
export const TABLE_SIZE = 8192
class PeriodicWave {
#real
#imag
#table
#disableNormalization
constructor(contextOrReal, optionsOrImag, constraints) {
let real, imag, disableNormalization = false
// Spec constructor: new PeriodicWave(context, {real, imag, disableNormalization})
if (contextOrReal && typeof contextOrReal === 'object' && 'sampleRate' in contextOrReal) {
let opts = optionsOrImag || {}
real = opts.real
imag = opts.imag
disableNormalization = opts.disableNormalization ?? false
// Per spec: if only real is given, imag defaults to zeros; if only imag, real defaults to zeros
if (real && !imag) imag = new Float32Array(real.length)
if (imag && !real) real = new Float32Array(imag.length)
} else {
// Legacy: new PeriodicWave(real, imag, {disableNormalization})
real = contextOrReal
imag = optionsOrImag
disableNormalization = constraints?.disableNormalization ?? false
}
if (!real || !imag) throw new TypeError('real and imag are required')
if (real.length !== imag.length) throw DOMErr('real and imag must have equal length', 'IndexSizeError')
if (real.length < 2) throw DOMErr('real and imag must have at least 2 elements', 'IndexSizeError')
for (let i = 0; i < real.length; i++)
if (!isFinite(real[i])) throw new TypeError('real values must be finite')
for (let i = 0; i < imag.length; i++)
if (!isFinite(imag[i])) throw new TypeError('imag values must be finite')
this.#real = Float32Array.from(real)
this.#imag = Float32Array.from(imag)
this.#disableNormalization = disableNormalization
this.#table = PeriodicWave.buildTable(this.#real, this.#imag, disableNormalization)
}
get table() { return this.#table }
// W3C Web Audio spec §1.31 OscillatorNode:
// x(n) = Σ[ real[k]·cos(2πkn/N) + imag[k]·sin(2πkn/N) ]
static buildTable(real, imag, disableNormalization = false) {
return buildWavetable(real, imag, { size: TABLE_SIZE, normalize: !disableNormalization })
}
static _builtIn = {}
static getBuiltIn(type) {
if (this._builtIn[type]) return this._builtIn[type]
let n = 64
let real = new Float32Array(n)
let imag = new Float32Array(n)
switch (type) {
case 'sine':
// sin(t) = imag[1] * sin(t) → imag[1] = 1
imag[1] = 1
break
case 'square':
// odd harmonics: 4/(πk) * sin(kt) → imag[k] = 4/(πk)
for (let k = 1; k < n; k += 2) imag[k] = 4 / (Math.PI * k)
break
case 'sawtooth':
// Σ (-1)^(k+1) * 2/(πk) * sin(kt) → imag[k] = (-1)^(k+1) * 2/(πk)
for (let k = 1; k < n; k++) imag[k] = (k % 2 ? 1 : -1) * 2 / (Math.PI * k)
break
case 'triangle':
// odd harmonics: 8/(π²k²) * (-1)^m * sin(kt), m = (k-1)/2
for (let k = 1; k < n; k += 2) {
let m = (k - 1) / 2
imag[k] = (8 / (Math.PI * Math.PI * k * k)) * (m % 2 ? -1 : 1)
}
break
default:
throw new Error('Unknown waveform type: ' + type)
}
this._builtIn[type] = PeriodicWave.buildTable(real, imag, true)
return this._builtIn[type]
}
}
export default PeriodicWave