Go back to using two gains for balance

SteroPannerNode handled mono files correctly, but apparently panning is not the same as balance.

Eventually I found a work around [here](https://github.com/WebAudio/web-audio-api/issues/975#issue-177242377). The idea is to use a dummy GainNode before the channel splitter to handle the up-mixing before the split is applied.

Tested a mono file as well as left/right balance on Chrome, Firefox, and Safari on Mac desktop.
This commit is contained in:
Jordan Eldredge 2019-01-24 20:27:09 -08:00
parent 8edd11da62
commit 3dc03abc26
5 changed files with 99 additions and 41 deletions

View file

@ -0,0 +1,77 @@
// The Web Audio API does not offer an easy way to make a stereo balance
// control. This is an attempt to fill that void, using an API similar to
// [StereoPannerNode](https://developer.mozilla.org/en-US/docs/Web/API/StereoPannerNode)
export default function StereoBalanceNode(context) {
let balance = 0;
// ChannelSplitterNode cannot be told to use a `channelInterperatation` of
// "speakers". This means that if we get a mono file, we will end up only
// playing in the left speaker. So instead we use this dummy gain node to
// convert whatever source we get (stereo, mono, or n channels) into a stereo
// signal.
// Idea credit: https://github.com/WebAudio/web-audio-api/issues/975#issue-177242377
const upMixer = context.createGain();
upMixer.channelCount = 2;
upMixer.channelCountMode = "explicit";
upMixer.channelInterpretation = "speakers";
const splitter = context.createChannelSplitter(2);
// Create the gains for left and right
const leftGain = context.createGain();
const rightGain = context.createGain();
const merger = context.createChannelMerger(2);
upMixer.connect(splitter);
splitter.connect(leftGain, 0);
splitter.connect(rightGain, 1);
leftGain.connect(merger, 0, 0);
rightGain.connect(merger, 0, 1);
// -1 (left) to 1 (right)
function set(value) {
leftGain.gain.value = value > 0 ? 1 - value : 1;
rightGain.gain.value = value > 0 ? 1 : 1 + value;
balance = value;
}
function get() {
return balance;
}
// Create our own version of an [AudioParam](https://developer.mozilla.org/en-US/docs/Web/API/AudioParam).
// We don't currently support any of the "over time" methods, but maybe some day
// we'll want to.
const audioParam = {};
Object.defineProperties(audioParam, {
value: { get, set, enumerable: true, configurable: true }
});
// The way the `.connect` API works, we can't actually construct our own
// AudioNode. Instead we have to take an existing node and monkey patch it.
Object.defineProperties(upMixer, {
balance: {
value: audioParam,
enumerable: true,
writable: false,
configurable: true
},
connect: {
value: AudioNode.prototype.connect.bind(merger),
enumerable: false,
writable: false,
configurable: true
},
disconnect: {
value: AudioNode.prototype.disconnect.bind(merger),
enumerable: false,
writable: false,
configurable: true
}
});
return upMixer;
}

View file

@ -1,26 +1,23 @@
/* Emulate the native <audio> element with Web Audio API */
import { BANDS, MEDIA_STATUS } from "../constants";
// Safari does not, yet, support the StereoPannerNode, so we use this polyfill
// Hopefully this can be removed in thefuture.
import SteroPannerNode from "stereo-panner-node";
import StereoBalanceNode from "./StereoBalanceNode";
import Emitter from "../emitter";
import ElementSource from "./elementSource";
import { Band } from "../types";
function createStereoPanner(context: AudioContext): StereoPannerNode {
if ("createStereoPanner" in context) {
return context.createStereoPanner();
}
return new SteroPannerNode(context);
interface StereoBalanceNodeType extends AudioNode {
constructor(context: AudioContext): StereoBalanceNodeType;
balance: {
value: number;
};
}
export default class Media {
_emitter: Emitter;
_context: AudioContext;
_balance: number;
_staticSource: AnalyserNode;
_balance: StereoBalanceNodeType;
_staticSource: GainNode;
_preamp: GainNode;
_panner: StereoPannerNode;
_analyser: AnalyserNode;
_gainNode: GainNode;
_source: ElementSource;
@ -51,21 +48,16 @@ export default class Media {
document.body.addEventListener("click", resume, false);
document.body.addEventListener("keydown", resume, false);
}
this._balance = 0;
// The _source node has to be recreated each time it's stopped or
// paused, so we don't create it here. Instead we create this dummy
// node wich the real source will connect to.
// TODO: Maybe we can get rid of this now that we are using AudioAbstraction?
this._staticSource = this._context.createAnalyser(); // Just a noop node
this._staticSource = this._context.createGain(); // Just a noop node
// @ts-ignore The way this class has to be monkey patched, makes it very hard to type.
this._balance = new StereoBalanceNode(this._context);
// Create the preamp node
this._preamp = this._context.createGain();
// Create the panner node
this._panner = createStereoPanner(this._context);
// Create the analyser node for the visualizer
this._analyser = this._context.createAnalyser();
this._analyser.fftSize = 2048;
@ -87,7 +79,9 @@ export default class Media {
// [...biquadFilters] |
// |_____________/
// |
// <stereoPanner>
// <staticSource>
// |
// <balance>
// |
// |\
// | <analyser>
@ -141,10 +135,10 @@ export default class Media {
output = filter;
});
output.connect(this._panner);
output.connect(this._balance);
this._panner.connect(this._gainNode);
this._panner.connect(this._analyser);
this._balance.connect(this._gainNode);
this._balance.connect(this._analyser);
this._gainNode.connect(this._context.destination);
}
@ -205,7 +199,8 @@ export default class Media {
// From -100 to 100
setBalance(balance: number) {
this._panner.pan.setValueAtTime(balance / 100, this._context.currentTime);
// Yo Dawg.
this._balance.balance.value = balance / 100;
}
setEqBand(band: Band, value: number) {
@ -215,7 +210,7 @@ export default class Media {
disableEq() {
this._staticSource.disconnect();
this._staticSource.connect(this._panner);
this._staticSource.connect(this._balance);
}
enableEq() {

View file

@ -1,3 +0,0 @@
declare module "stereo-panner-node" {
export default StereoPannerNode;
}

View file

@ -141,7 +141,6 @@
},
"prettier": {},
"dependencies": {
"lodash": "^4.17.11",
"stereo-panner-node": "^1.4.0"
"lodash": "^4.17.11"
}
}

View file

@ -1387,10 +1387,6 @@ balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
base-audio-context@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/base-audio-context/-/base-audio-context-1.1.1.tgz#d75a5b6cbbfda6728722b8590095b5136d8b8d28"
base64-js@^1.0.2:
version "1.2.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886"
@ -7983,12 +7979,6 @@ stealthy-require@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
stereo-panner-node@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/stereo-panner-node/-/stereo-panner-node-1.4.0.tgz#3af2cd65699716b95e252d40dbf589a2c35dce69"
dependencies:
base-audio-context "^1.1.1"
stream-browserify@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"