web-audio-api
Version:
Portable Web Audio API
104 lines (91 loc) • 3.69 kB
JavaScript
import AudioNode from './AudioNode.js'
import AudioParam from './AudioParam.js'
import AudioBuffer from 'audio-buffer'
import { BLOCK_SIZE } from './constants.js'
import { DOMErr } from './errors.js'
// W3C spec equal-power panning:
// For mono input: outputL = input * cos(x), outputR = input * sin(x)
// where x = (pan + 1) / 2 * π/2
// For stereo input (continuous at pan=0, identity at pan=0):
// If pan < 0: x = -pan * π/2; outputL = inputL + inputR * sin(x), outputR = inputR * cos(x)
// If pan >= 0: x = pan * π/2; outputL = inputL * cos(x), outputR = inputR + inputL * sin(x)
class StereoPannerNode extends AudioNode {
#pan
get pan() { return this.#pan }
constructor(context, options) {
options = AudioNode._checkOpts(options)
super(context, 1, 1, 2, 'clamped-max', 'speakers')
this.#pan = new AudioParam(this.context, options.pan ?? 0, 'a', -1, 1)
this._outBuf = new AudioBuffer(2, BLOCK_SIZE, context.sampleRate)
this._applyOpts(options)
}
_validateChannelCount(val) {
if (val > 2) throw DOMErr('channelCount cannot be greater than 2', 'NotSupportedError')
}
_validateChannelCountMode(val) {
if (val === 'max') throw DOMErr("channelCountMode cannot be 'max'", 'NotSupportedError')
}
_tick() {
super._tick()
let inBuf = this._inputs[0]._tick()
let panArr = this.#pan._tick()
let outL = this._outBuf.getChannelData(0)
let outR = this._outBuf.getChannelData(1)
let inCh = inBuf.numberOfChannels
let PI2 = Math.PI / 2
// Fast path: constant pan across block — compute trig once
let isConst = panArr[0] === panArr[BLOCK_SIZE - 1]
if (inCh === 1) {
let inp = inBuf.getChannelData(0)
if (isConst) {
let p = Math.max(-1, Math.min(1, panArr[0]))
let x = (p + 1) / 2 * PI2
let cosX = Math.cos(x), sinX = Math.sin(x)
for (let i = 0; i < BLOCK_SIZE; i++) {
outL[i] = inp[i] * cosX
outR[i] = inp[i] * sinX
}
} else {
for (let i = 0; i < BLOCK_SIZE; i++) {
let p = Math.max(-1, Math.min(1, panArr[i]))
let x = (p + 1) / 2 * PI2
outL[i] = inp[i] * Math.cos(x)
outR[i] = inp[i] * Math.sin(x)
}
}
} else {
let inL = inBuf.getChannelData(0), inR = inBuf.getChannelData(1)
if (isConst) {
let p = Math.max(-1, Math.min(1, panArr[0]))
if (p <= -1) {
for (let i = 0; i < BLOCK_SIZE; i++) { outL[i] = inL[i] + inR[i]; outR[i] = 0 }
} else if (p >= 1) {
for (let i = 0; i < BLOCK_SIZE; i++) { outL[i] = 0; outR[i] = inR[i] + inL[i] }
} else if (p < 0) {
let x = -p * PI2, sinX = Math.sin(x), cosX = Math.cos(x)
for (let i = 0; i < BLOCK_SIZE; i++) { outL[i] = inL[i] + inR[i] * sinX; outR[i] = inR[i] * cosX }
} else {
let x = p * PI2, cosX = Math.cos(x), sinX = Math.sin(x)
for (let i = 0; i < BLOCK_SIZE; i++) { outL[i] = inL[i] * cosX; outR[i] = inR[i] + inL[i] * sinX }
}
} else {
for (let i = 0; i < BLOCK_SIZE; i++) {
let p = Math.max(-1, Math.min(1, panArr[i]))
if (p <= -1) {
outL[i] = inL[i] + inR[i]; outR[i] = 0
} else if (p >= 1) {
outL[i] = 0; outR[i] = inR[i] + inL[i]
} else if (p < 0) {
let x = -p * PI2
outL[i] = inL[i] + inR[i] * Math.sin(x); outR[i] = inR[i] * Math.cos(x)
} else {
let x = p * PI2
outL[i] = inL[i] * Math.cos(x); outR[i] = inR[i] + inL[i] * Math.sin(x)
}
}
}
}
return this._outBuf
}
}
export default StereoPannerNode