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

253 lines
5.7 KiB
JavaScript

import Emitter from "./emitter";
const invariant = (assertion, message) => {
if (!assertion) {
console.error(message);
}
};
async function readUrlAsArrayBuffer(url) {
return new Promise((resolve, reject) => {
const oReq = new XMLHttpRequest();
oReq.open("GET", url, true);
oReq.responseType = "arraybuffer";
oReq.onload = () => {
const arrayBuffer = oReq.response; // Note: not oReq.responseText
resolve(arrayBuffer);
};
oReq.onerror = e => {
reject(e);
};
oReq.send(null);
return true;
});
}
const STATUS = {
PLAYING: "PLAYING",
STOPPED: "STOPPED",
PAUSED: "PAUSED"
};
export default class BufferSource extends Emitter {
constructor(context, destination) {
super();
this._context = context;
this._destination = destination;
//this._loop = false;
this._buffer = null;
this._source = null;
this._status = STATUS.STOPPED;
this._stalled = false;
this._startTime = null;
this._pausedAtTime = null;
this._onEnded = this._onEnded.bind(this);
}
disconnect() {
if (this._source != null) {
this._source.disconnect();
this._source = null;
}
}
// Just adapt the callback into a promise.
_decodeAudioData(buffer) {
return new Promise((resolve, reject) => {
this._context.decodeAudioData(buffer, resolve, reject);
});
}
_startLoading() {
this.stop();
// Ensure we don't start playing while we are waiting to load.
this._buffer = null;
this._setStalled(true);
}
async loadUrl(url) {
this._startLoading();
const arrayBuffer = await readUrlAsArrayBuffer(url);
return this._loadArrayBuffer(arrayBuffer);
}
async _loadArrayBuffer(arrayBuffer) {
this._buffer = await this._decodeAudioData(arrayBuffer);
this.trigger("loaded");
this._setStalled(false);
this._setStatus(STATUS.STOPPED);
}
play() {
if (this._stalled) {
return;
}
switch (this._status) {
case STATUS.PLAYING:
case STATUS.STOPPED:
this._start(0);
break;
case STATUS.PAUSED:
invariant(
this._pausedAtTime != null,
"Status is paused, but there is no _pausedAtTime"
);
this._start(this._pausedAtTime);
break;
}
}
pause() {
this._pausedAtTime = this.getTimeElapsed();
this._silence(STATUS.PAUSED);
}
stop() {
this._silence(STATUS.STOPPED);
}
seekToTime(time) {
if (!this.getDuration()) {
return;
}
time = Math.min(time, this.getDuration());
time = Math.max(time, 0);
this._start(time);
}
getStatus() {
return this._status;
}
getStalled() {
return this._stalled;
}
getDuration() {
if (this._buffer == null) {
return 0;
}
return this._buffer.duration;
}
getTimeElapsed() {
switch (this._status) {
case STATUS.PLAYING:
invariant(
this._startTime != null,
"status is playing, but there is no _startTime"
);
const totalTime = this._context.currentTime - this._startTime;
// If looping, the time may be greater than the song's duration.
return totalTime % this.getDuration();
case STATUS.STOPPED:
return 0;
case STATUS.PAUSED:
invariant(
this._pausedAtTime != null,
"Status is paused, but there is no _pausedAtTime"
);
return this._pausedAtTime;
}
throw new Error("Invalid status");
}
getNumberOfChannels() {
if (this._buffer == null) {
return null;
}
// TODO: This is actually the channels of the audio node, not the source media.
return this._buffer.numberOfChannels;
}
getSampleRate() {
if (this._buffer == null) {
return null;
}
// TODO: This is actually the sample rate of the audio node, not the source media.
return this._buffer.sampleRate;
}
/*
getLoop() {
return this._loop;
}
setLoop(shouldLoop) {
this._loop = shouldLoop;
if (this._source) {
this._source.loop = this._loop;
}
}
*/
_start(position) {
invariant(!isNaN(position));
if (this._buffer == null) {
return;
}
if (this._source) {
// Does this work?
this.disconnect();
}
this._source = this._context.createBufferSource();
this._source.buffer = this._buffer;
//this._source.loop = this._loop;
this._source.connect(this._destination);
this._source.onended = this._onEnded;
this._startTime = this._context.currentTime - position;
this._setStatus(STATUS.PLAYING);
this._source.start(0, position);
}
_silence(reason) {
if (!reason) {
throw new Error("Tried to silence the audio without a reason");
}
// We must always update the status before ending the audio, so that onEnded
// can distinguish between audio that got to the end of the track, and audio
// that the user stopped/paused
this._setStatus(reason);
if (this._source == null) {
return;
}
this._source.stop(0);
this._source = null;
}
_startMonitoringPosition() {
this._nextPositionUpdate = setTimeout(() => {
this.trigger("positionChange");
this._startMonitoringPosition();
}, 50);
}
_stopMonitoringPosition() {
clearTimeout(this._nextPositionUpdate);
}
_setStatus(status) {
this._status = status;
if (status === STATUS.PLAYING) {
this._startMonitoringPosition();
} else {
this._stopMonitoringPosition();
}
this.trigger("statusChange");
}
_setStalled(stalled) {
this._stalled = stalled;
this.trigger("statusChange");
}
_onEnded() {
if (this._status === STATUS.PLAYING) {
this.trigger("ended");
this._setStatus(STATUS.STOPPED);
}
}
}