mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-20 16:49:52 +00:00
Commit first draft of source abstraction expriment
This commit is contained in:
parent
87b8eaf0b9
commit
576f1e9942
6 changed files with 517 additions and 47 deletions
58
.eslintrc
58
.eslintrc
|
|
@ -7,11 +7,7 @@
|
|||
"experimentalObjectRestSpread": true
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
"react",
|
||||
"prettier",
|
||||
"import"
|
||||
],
|
||||
"plugins": ["react", "prettier", "import"],
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "15.2"
|
||||
|
|
@ -33,21 +29,12 @@
|
|||
"maxBOF": 0
|
||||
}
|
||||
],
|
||||
"arrow-body-style": [
|
||||
"error",
|
||||
"as-needed"
|
||||
],
|
||||
"arrow-body-style": ["error", "as-needed"],
|
||||
"arrow-spacing": "error",
|
||||
"block-scoped-var": "warn",
|
||||
"brace-style": [
|
||||
"warn",
|
||||
"1tbs"
|
||||
],
|
||||
"brace-style": ["warn", "1tbs"],
|
||||
"camelcase": "error",
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"comma-dangle": ["error", "never"],
|
||||
"comma-spacing": "error",
|
||||
"consistent-return": "warn",
|
||||
"constructor-super": "error",
|
||||
|
|
@ -58,10 +45,7 @@
|
|||
}
|
||||
],
|
||||
"eol-last": "error",
|
||||
"eqeqeq": [
|
||||
"error",
|
||||
"smart"
|
||||
],
|
||||
"eqeqeq": ["error", "smart"],
|
||||
"guard-for-in": "error",
|
||||
"indent": [
|
||||
"error",
|
||||
|
|
@ -70,21 +54,12 @@
|
|||
"SwitchCase": 1
|
||||
}
|
||||
],
|
||||
"jsx-quotes": [
|
||||
"error",
|
||||
"prefer-double"
|
||||
],
|
||||
"jsx-quotes": ["error", "prefer-double"],
|
||||
"key-spacing": "warn",
|
||||
"keyword-spacing": "error",
|
||||
"linebreak-style": "error",
|
||||
"max-depth": [
|
||||
"warn",
|
||||
4
|
||||
],
|
||||
"max-params": [
|
||||
"warn",
|
||||
5
|
||||
],
|
||||
"max-depth": ["warn", 4],
|
||||
"max-params": ["warn", 5],
|
||||
"new-cap": "error",
|
||||
"no-alert": "error",
|
||||
"no-caller": "error",
|
||||
|
|
@ -139,26 +114,16 @@
|
|||
"no-unreachable": "error",
|
||||
"no-unused-expressions": "error",
|
||||
"no-unused-vars": "error",
|
||||
"no-use-before-define": [
|
||||
"error",
|
||||
"nofunc"
|
||||
],
|
||||
"no-use-before-define": ["error", "nofunc"],
|
||||
"no-useless-rename": "error",
|
||||
"no-var": "error",
|
||||
"no-with": "error",
|
||||
"object-curly-spacing": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"prefer-arrow-callback": "warn",
|
||||
"prefer-const": "error",
|
||||
"prefer-spread": "error",
|
||||
"prefer-template": "warn",
|
||||
"quotes": [
|
||||
"error",
|
||||
"double",
|
||||
"avoid-escape"
|
||||
],
|
||||
"quotes": ["error", "double", "avoid-escape"],
|
||||
"radix": "error",
|
||||
"react/no-string-refs": "error",
|
||||
"react/jsx-boolean-value": "error",
|
||||
|
|
@ -188,7 +153,6 @@
|
|||
"template-curly-spacing": "error",
|
||||
"use-isnan": "error",
|
||||
"valid-typeof": "error",
|
||||
"wrap-iife": "error",
|
||||
"prettier/prettier": "error",
|
||||
"import/no-unresolved": "error",
|
||||
"import/default": "error",
|
||||
|
|
|
|||
73
experiments/audioAbstraction/app.js
Normal file
73
experiments/audioAbstraction/app.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/* global ElementSource BufferSource */
|
||||
const play = document.getElementById("play");
|
||||
const pause = document.getElementById("pause");
|
||||
const stop = document.getElementById("stop");
|
||||
const eject = document.getElementById("eject");
|
||||
const url = document.getElementById("url");
|
||||
const loadUrl = document.getElementById("loadUrl");
|
||||
const info = document.getElementById("info");
|
||||
const position = document.getElementById("position");
|
||||
const sourceType = document.getElementById("sourceType");
|
||||
|
||||
const context = new (window.AudioContext || window.webkitAudioContext)();
|
||||
|
||||
const getSourceClass = () =>
|
||||
sourceType.value === "buffer" ? BufferSource : ElementSource;
|
||||
|
||||
let source = new (getSourceClass())(context, context.destination);
|
||||
source.setAutoPlay(true);
|
||||
|
||||
sourceType.addEventListener("change", () => {
|
||||
source.disconnect();
|
||||
source = new (getSourceClass())(context, context.destination);
|
||||
source.setAutoPlay(true);
|
||||
});
|
||||
|
||||
//const source = new BufferSource(context, context.destination);
|
||||
|
||||
play.addEventListener("click", () => {
|
||||
source.play();
|
||||
});
|
||||
pause.addEventListener("click", () => {
|
||||
source.pause();
|
||||
});
|
||||
stop.addEventListener("click", () => {
|
||||
source.stop();
|
||||
});
|
||||
eject.addEventListener("change", e => {
|
||||
source.loadFile(e.target.files[0]);
|
||||
});
|
||||
|
||||
loadUrl.addEventListener("click", () => {
|
||||
source.loadUrl(url.value);
|
||||
});
|
||||
|
||||
position.addEventListener("change", () => {
|
||||
const percent = position.value / 100;
|
||||
const newTime = source.getDuration() * percent;
|
||||
source.seekToTime(newTime);
|
||||
});
|
||||
|
||||
const updateInfo = () => {
|
||||
const data = {
|
||||
numberOfChannels: source.getNumberOfChannels(),
|
||||
status: source.getStatus(),
|
||||
duration: source.getDuration(),
|
||||
timeElapsed: source.getTimeElapsed(),
|
||||
sampleRate: source.getSampleRate(),
|
||||
loop: source.getLoop()
|
||||
};
|
||||
|
||||
info.value = JSON.stringify(data, null, 2);
|
||||
};
|
||||
info.addEventListener("click", updateInfo);
|
||||
|
||||
source.on("statusChange", () => {
|
||||
updateInfo();
|
||||
});
|
||||
|
||||
source.on("positionChange", () => {
|
||||
const percent = source.getTimeElapsed() / source.getDuration();
|
||||
position.value = percent * 100;
|
||||
updateInfo();
|
||||
});
|
||||
252
experiments/audioAbstraction/bufferSource.js
Normal file
252
experiments/audioAbstraction/bufferSource.js
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
(function() {
|
||||
const invariant = (assertion, message) => {
|
||||
if (!assertion) {
|
||||
console.error(message);
|
||||
}
|
||||
};
|
||||
|
||||
async function readFileAsArrayBuffer(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
const arrayBuffer = e.target.result;
|
||||
resolve(arrayBuffer);
|
||||
};
|
||||
reader.onerror = function(e) {
|
||||
reject(e);
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
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 = {
|
||||
WAITING: "WAITING",
|
||||
PLAYING: "PLAYING",
|
||||
STOPPED: "STOPPED",
|
||||
PAUSED: "PAUSED"
|
||||
};
|
||||
|
||||
class BufferSource extends window.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._startTime = null;
|
||||
this._autoPlay = false;
|
||||
this._pausedAtTime = null;
|
||||
this._onEnded = this._onEnded.bind(this);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this._source != null) {
|
||||
this._source.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// Just adapt the callback into a promise.
|
||||
_decodeAudioData(buffer) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._context.decodeAudioData(buffer, resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
async loadFile(file) {
|
||||
this._setStatus(STATUS.WAITING);
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
this._buffer = await this._decodeAudioData(arrayBuffer);
|
||||
if (this._autoPlay) {
|
||||
this.play();
|
||||
}
|
||||
}
|
||||
|
||||
async loadUrl(url) {
|
||||
this._setStatus(STATUS.WAITING);
|
||||
const arrayBuffer = await readUrlAsArrayBuffer(url);
|
||||
this._buffer = await this._decodeAudioData(arrayBuffer);
|
||||
if (this._autoPlay) {
|
||||
this.play();
|
||||
}
|
||||
}
|
||||
|
||||
play() {
|
||||
switch (this._status) {
|
||||
case STATUS.WAITING:
|
||||
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) {
|
||||
time = Math.min(time, this.getDuration());
|
||||
time = Math.max(time, 0);
|
||||
this._start(time);
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return this._status;
|
||||
}
|
||||
|
||||
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"
|
||||
);
|
||||
return this._context.currentTime - this._startTime;
|
||||
case STATUS.STOPPED:
|
||||
case STATUS.WAITING:
|
||||
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;
|
||||
}
|
||||
return this._buffer.numberOfChannels;
|
||||
}
|
||||
|
||||
getSampleRate() {
|
||||
if (this._buffer == null) {
|
||||
return null;
|
||||
}
|
||||
return this._buffer.sampleRate;
|
||||
}
|
||||
|
||||
getLoop() {
|
||||
return this._loop;
|
||||
}
|
||||
|
||||
setLoop(shouldLoop) {
|
||||
this._loop = shouldLoop;
|
||||
if (this._source) {
|
||||
this._source.loop = this._loop;
|
||||
}
|
||||
}
|
||||
|
||||
setAutoPlay(shouldAutoPlay) {
|
||||
this._autoPlay = shouldAutoPlay;
|
||||
}
|
||||
|
||||
_start(position) {
|
||||
if (this._buffer == null) {
|
||||
return;
|
||||
}
|
||||
if (this._source) {
|
||||
// Does this work?
|
||||
this._source.disconnect();
|
||||
this._source = null;
|
||||
}
|
||||
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();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
_stopMonitoringPosition() {
|
||||
clearTimeout(this._nextPositionUpdate);
|
||||
}
|
||||
|
||||
_setStatus(status) {
|
||||
this._status = status;
|
||||
if (status === STATUS.PLAYING) {
|
||||
this._startMonitoringPosition();
|
||||
} else {
|
||||
this._stopMonitoringPosition();
|
||||
}
|
||||
this.trigger("statusChange");
|
||||
}
|
||||
|
||||
_onEnded() {
|
||||
if (this._status === STATUS.PLAYING) {
|
||||
this._setStatus(STATUS.STOPPED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.BufferSource = BufferSource;
|
||||
//export default BufferSource;
|
||||
})();
|
||||
120
experiments/audioAbstraction/elementSource.js
Normal file
120
experiments/audioAbstraction/elementSource.js
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
(function() {
|
||||
const STATUS = {
|
||||
WAITING: "WAITING",
|
||||
PLAYING: "PLAYING",
|
||||
STOPPED: "STOPPED",
|
||||
PAUSED: "PAUSED"
|
||||
};
|
||||
|
||||
class ElementSource extends window.Emitter {
|
||||
constructor(context, destination) {
|
||||
super();
|
||||
this._context = context;
|
||||
this._destination = destination;
|
||||
this._audio = document.createElement("audio");
|
||||
this._audio.crossorigin = "anonymous";
|
||||
|
||||
this._audio.addEventListener("loadend", () => {
|
||||
if (this._autoPlay) {
|
||||
this.play();
|
||||
}
|
||||
});
|
||||
|
||||
this._audio.addEventListener("timeupdate", () => {
|
||||
this.trigger("positionChange");
|
||||
});
|
||||
|
||||
this._source = this._context.createMediaElementSource(this._audio);
|
||||
this._loop = true;
|
||||
this._autoPlay = true; // Do this ourselves for now. The actual API is bonkers and may not play well with mobile Safari
|
||||
this._source.connect(destination);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this._source.disconnect();
|
||||
}
|
||||
|
||||
// Async for now, for compatibility with BufferAudioSource
|
||||
async loadUrl(url) {
|
||||
this._audio.src = url;
|
||||
if (this._autoPlay) {
|
||||
this.play();
|
||||
}
|
||||
}
|
||||
|
||||
loadFile(fileReference) {
|
||||
this.loadUrl(URL.createObjectURL(fileReference));
|
||||
}
|
||||
|
||||
async play() {
|
||||
// TODO: set status waiting
|
||||
if (this._status !== STATUS.PAUSED) {
|
||||
this.seekToTime(0);
|
||||
}
|
||||
await this._audio.play();
|
||||
this._setStatus(STATUS.PLAYING);
|
||||
// TODO: set status playing
|
||||
}
|
||||
|
||||
pause() {
|
||||
this._audio.pause();
|
||||
this._setStatus(STATUS.PAUSED);
|
||||
}
|
||||
|
||||
stop() {
|
||||
this._audio.pause();
|
||||
this._audio.currentTime = 0;
|
||||
this._setStatus(STATUS.STOPPED);
|
||||
}
|
||||
|
||||
seekToTime(time) {
|
||||
// Make sure we are within range
|
||||
// TODO: Use clamp
|
||||
time = Math.min(time, this.getDuration());
|
||||
time = Math.max(time, 0);
|
||||
this._audio.currentTime = time;
|
||||
this.trigger("positionChange");
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return this._status;
|
||||
}
|
||||
|
||||
getDuration() {
|
||||
const { duration } = this._audio;
|
||||
return isNaN(duration) ? 0 : duration;
|
||||
}
|
||||
|
||||
getTimeElapsed() {
|
||||
return this._audio.currentTime;
|
||||
}
|
||||
|
||||
getNumberOfChannels() {
|
||||
return this._source.channelCount;
|
||||
}
|
||||
|
||||
getSampleRate() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getLoop() {
|
||||
return this._loop;
|
||||
}
|
||||
|
||||
setLoop(shouldLoop) {
|
||||
this._loop = shouldLoop;
|
||||
}
|
||||
|
||||
setAutoPlay(shouldAutoPlay) {
|
||||
this._autoPlay = shouldAutoPlay;
|
||||
}
|
||||
|
||||
_setStatus(status) {
|
||||
this._status = status;
|
||||
this.trigger("statusChange");
|
||||
}
|
||||
}
|
||||
|
||||
window.ElementSource = ElementSource;
|
||||
//export default AudioSource;
|
||||
})();
|
||||
27
experiments/audioAbstraction/emitter.js
Normal file
27
experiments/audioAbstraction/emitter.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
(function() {
|
||||
class Emitter {
|
||||
constructor() {
|
||||
this._listeners = {};
|
||||
}
|
||||
|
||||
on(event, callback) {
|
||||
const eventListeners = this._listeners[event] || [];
|
||||
eventListeners.push(callback);
|
||||
this._listeners[event] = eventListeners;
|
||||
const unsubscribe = () => {
|
||||
this._listeners[event] = eventListeners.filter(cb => cb !== callback);
|
||||
};
|
||||
return unsubscribe;
|
||||
}
|
||||
|
||||
trigger(event) {
|
||||
const callbacks = this._listeners[event];
|
||||
if (callbacks) {
|
||||
callbacks.forEach(cb => cb());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.Emitter = Emitter;
|
||||
//export default AudioSource;
|
||||
})();
|
||||
34
experiments/audioAbstraction/index.html
Normal file
34
experiments/audioAbstraction/index.html
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Audio Abstraction</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<select id='sourceType'>
|
||||
<option value='buffer'>Bufer</option>
|
||||
<option value='element'>Element</option>
|
||||
</select>
|
||||
<button id='play'>Play</button>
|
||||
<button id='pause'>Pause</button>
|
||||
<button id='stop'>Stop</button>
|
||||
<br />
|
||||
<input type='file' id='eject' name='eject' />
|
||||
<br />
|
||||
<input id='url' name='url' value="../../mp3/llama-2.91.mp3" />
|
||||
<button id='loadUrl'>Load URL</button>
|
||||
<br />
|
||||
<input type='range' id='position' min="0" max="100" />
|
||||
<br />
|
||||
<textarea id='info' cols="50" rows="20"></textarea>
|
||||
<script src="./emitter.js"></script>
|
||||
<script src="./bufferSource.js"></script>
|
||||
<script src="./elementSource.js"></script>
|
||||
<script src="./app.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue