mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-21 09:09:01 +00:00
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:
parent
8edd11da62
commit
3dc03abc26
5 changed files with 99 additions and 41 deletions
77
js/media/StereoBalanceNode.js
Normal file
77
js/media/StereoBalanceNode.js
Normal 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;
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
3
js/stereo-panner-node.d.ts
vendored
3
js/stereo-panner-node.d.ts
vendored
|
|
@ -1,3 +0,0 @@
|
|||
declare module "stereo-panner-node" {
|
||||
export default StereoPannerNode;
|
||||
}
|
||||
|
|
@ -141,7 +141,6 @@
|
|||
},
|
||||
"prettier": {},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.11",
|
||||
"stereo-panner-node": "^1.4.0"
|
||||
"lodash": "^4.17.11"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10
yarn.lock
10
yarn.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue