mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-23 10:07:35 +00:00
260 lines
6.7 KiB
JavaScript
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();
|
|
}
|
|
}
|
|
}
|