webamp/js/media/index.js
2018-01-03 19:52:00 -08:00

260 lines
6.7 KiB
JavaScript

/* Emulate the native <audio> element with Web Audio API */
import { elementSource } from "../config";
import { BANDS } from "../constants";
import BufferSource from "./bufferSource";
import ElementSource from "./elementSource";
export default class Media {
constructor(fileInput) {
this._context = new (window.AudioContext || window.webkitAudioContext)();
this._callbacks = {
waiting: function() {},
stopWaiting: function() {},
playing: function() {},
timeupdate: function() {},
visualizerupdate: function() {},
ended: function() {},
fileLoaded: function() {}
};
this._balance = 0;
this.name = null;
this.fileInput = fileInput;
// 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
// Create the preamp node
this._preamp = this._context.createGain();
// Create the spliter node
this._chanSplit = this._context.createChannelSplitter(2);
// Create the gains for left and right
this._leftGain = this._context.createGain();
this._rightGain = this._context.createGain();
// Create channel merge
this._chanMerge = this._context.createChannelMerger(2);
// Create the analyser node for the visualizer
this._analyser = this._context.createAnalyser();
this._analyser.fftSize = 2048;
// Create the gain node for the volume control
this._gainNode = this._context.createGain();
// Connect all the nodes in the correct way
// (Note, source is created and connected later)
//
// <source>
// |
// |_____________
// | \
// <preamp> |
// | | <-- Optional bypass
// [...biquadFilters] |
// |_____________/
// |
// (split using createChannelSplitter)
// |
// / \
// / \
// <leftGain><rightGain>
// \ /
// \ /
// |
// (merge using createChannelMerger)
// |
// <chanMerge>
// |
// |\
// | <analyser>
// <gain>
// |
// <destination>
this._source = elementSource
? new ElementSource(this._context, this._staticSource)
: new BufferSource(this._context, this._staticSource);
this._source.on("positionChange", () => {
this._callbacks.timeupdate();
});
this._source.on("ended", () => {
this._callbacks.ended();
});
this._source.on("statusChange", () => {
switch (this._source.getStatus()) {
case "PLAYING":
this._callbacks.playing();
break;
}
this._callbacks.timeupdate();
});
this._source.on("loaded", () => {
this._callbacks.fileLoaded();
});
this._staticSource.connect(this._preamp);
let output = this._preamp;
this.bands = {};
BANDS.forEach((band, i) => {
const filter = this._context.createBiquadFilter();
this.bands[band] = filter;
if (i === 0) {
// The first filter, includes all lower frequencies
filter.type = "lowshelf";
} else if (i === band.length - 1) {
// The last filter, includes all higher frequencies
filter.type = "highshelf";
} else {
filter.type = "peaking";
}
filter.frequency.value = band;
filter.gain.value = 0;
output.connect(filter);
output = filter;
});
output.connect(this._chanSplit);
// Connect split channels to left / right gains
this._chanSplit.connect(this._leftGain, 0);
this._chanSplit.connect(this._rightGain, 1);
// Reconnect the left / right gains to the merge node
this._leftGain.connect(this._chanMerge, 0, 0);
this._rightGain.connect(this._chanMerge, 0, 1);
this._chanMerge.connect(this._gainNode);
this._chanMerge.connect(this._analyser);
this._gainNode.connect(this._context.destination);
}
/* Properties */
duration() {
return this._source.getDuration();
}
timeElapsed() {
return this._source.getTimeElapsed();
}
timeRemaining() {
return this.duration() - this.timeElapsed();
}
percentComplete() {
return this.timeElapsed() / this.duration() * 100;
}
channels() {
return this._source.getNumberOfChannels();
}
sampleRate() {
return this._source.getSampleRate();
}
/* Actions */
previous() {
// Implement this when we support playlists
}
play() {
this._source.play();
}
pause() {
this._source.pause();
}
stop() {
this._source.stop();
}
/* Actions with arguments */
seekToPercentComplete(percent) {
const seekTime = this.duration() * (percent / 100);
this.seekToTime(seekTime);
}
// From 0-1
setVolume(volume) {
this._gainNode.gain.value = volume / 100;
}
// From 0-1
setPreamp(value) {
this._preamp.gain.value = value / 100;
}
// From -100 to 100
setBalance(balance) {
let changeVal = Math.abs(balance) / 100;
// Hack for Firefox. Having either channel set to 0 seems to revert us
// to equal balance.
changeVal = changeVal - 0.00000001;
if (balance > 0) {
// Right
this._leftGain.gain.value = 1 - changeVal;
this._rightGain.gain.value = 1;
} else if (balance < 0) {
// Left
this._leftGain.gain.value = 1;
this._rightGain.gain.value = 1 - changeVal;
} else {
// Center
this._leftGain.gain.value = 1;
this._rightGain.gain.value = 1;
}
this._balance = balance;
}
setEqBand(band, value) {
const db = value / 100 * 24 - 12;
this.bands[band].gain.value = db;
}
disableEq() {
this._staticSource.disconnect();
this._staticSource.connect(this._chanSplit);
}
enableEq() {
this._staticSource.disconnect();
this._staticSource.connect(this._preamp);
}
/* Listeners */
addEventListener(event, callback) {
this._callbacks[event] = callback;
}
seekToTime(time) {
this._source.seekToTime(time);
}
// Used only for the initial load, since it must have a CORS header
async loadFromUrl(url, fileName, autoPlay) {
this.name = fileName;
this._callbacks.waiting();
await this._source.loadUrl(url);
this._callbacks.stopWaiting();
if (autoPlay) {
this.play();
}
}
}