Separate html "shell" javascript; player now standalone

Closes #3.

The code which defines what the buttons do and downloads songs and so
forth are now completely separated from the player, which has the
following API:

 - `XMPlayer.initAudio` -> starts up audio context; it's available as
   `XMPlayer.audioctx`
 - `XMPlayer.loadXM(ArrayBuffer)` -> returns `true` if loaded, otherwise
   barfs randomly
 - `XMPlayer.playXM()` -> starts playing
 - `XMPlayer.pauseXM()` -> obvious
 - `XMPlayer.stopXM()` -> obvious; call this before loading a new one

Loading trackview.js is now optional; without it, the player won't do
any visualizations. Or, you can override the following to get callbacks:

 - `XMView.pushEvent(e)` -> push an audio event onto the queue. Called
   once per tick (about 50Hz, controlled by song). `e` contains fields:
   - `t` - audio timestamp
   - `vu` - Float32Array of RMS power (volume) for each channel
   - `scopes` - [Float32Array] of oscilloscope data, one array per
     channel; `XMView.scope_width` contains # of samples to produce here
   - `songpos` - position in the song (# patterns played)
   - `pat` - pattern number currently playing
   - `row` - row within pattern
 - `XMView.pause()` - pause visualization
 - `XMView.stop()` - stop/reset visualization
This commit is contained in:
Andy Sloane 2015-11-11 21:09:58 -08:00
commit 8c37c8d96b
4 changed files with 164 additions and 144 deletions

View file

@ -4,6 +4,7 @@
<script src="xmeffects.js"></script>
<script src="trackview.js"></script>
<script src="xm.js"></script>
<script src="shell.js"></script>
<style>
.centered {

111
shell.js Normal file
View file

@ -0,0 +1,111 @@
(function (window, document) {
if (!window.XMPlayer) {
window.XMPlayer = {};
}
var XMPlayer = window.XMPlayer;
if (!window.XMView) {
window.XMView = {};
}
var XMView = window.XMView;
function loadXMAndInit(xmdata) {
if (!XMPlayer.loadXM(xmdata)) {
return;
}
XMView.init();
var playbutton = document.getElementById("playpause");
playbutton.innerHTML='Play';
playbutton.onclick = function() {
if (XMPlayer.playing) {
XMPlayer.pauseXM();
playbutton.innerHTML='Play';
} else {
XMPlayer.playXM();
playbutton.innerHTML='Pause';
}
};
playbutton.disabled = false;
return XMPlayer.xm;
}
function downloadXM(uri) {
var xmReq = new XMLHttpRequest();
xmReq.open("GET", uri, true);
xmReq.responseType = "arraybuffer";
xmReq.onload = function (xmEvent) {
var arrayBuffer = xmReq.response;
if (arrayBuffer) {
loadXMAndInit(arrayBuffer);
} else {
console.log("unable to load", uri);
}
};
xmReq.send(null);
}
XMPlayer.allowDrop = function(ev) {
ev.stopPropagation();
ev.preventDefault();
var elem = document.getElementById("playercontainer");
elem.className = (ev.type == "dragover" ? "draghover" : "playercontainer");
return false;
};
XMPlayer.handleDrop = function(e) {
console.log(e);
e.preventDefault();
var elem = document.getElementById("playercontainer");
elem.className = "playercontainer";
var files = e.target.files || e.dataTransfer.files;
if (files.length < 1) return false;
var reader = new FileReader();
reader.onload = function(e) {
XMPlayer.stopXM();
loadXMAndInit(e.target.result);
};
reader.readAsArrayBuffer(files[0]);
return false;
};
function initFilelist() {
var el = document.getElementById('filelist');
xmuris.forEach(function(entry) {
var a = document.createElement('a');
a.text = entry[0];
a.href = '#'+entry[1];
a.onclick = function() {
el.style.display = "none";
XMPlayer.stopXM();
downloadXM(baseuri + entry[1]);
};
el.appendChild(a);
el.appendChild(document.createElement('br'));
});
var loadbutton = document.getElementById('loadbutton');
loadbutton.onclick = function() {
if (el.style.display == "none") {
el.style.display = "block";
} else {
el.style.display = "none";
}
};
}
window.onload = function() {
XMPlayer.initAudio();
initFilelist();
var uri = location.hash.substr(1);
if (uri === "") {
uri = "kamel.xm";
}
if (!uri.startsWith("http")) {
uri = baseuri + uri;
}
downloadXM(uri);
};
})(window, document);

View file

@ -27,8 +27,8 @@ fontimg.src = "ft2font.png";
var _fontmap_notes = [8*5, 8*22, 8*28];
var pat_canvas_patnum;
var audio_events = [];
var paused_events = [];
var audio_events;
var paused_events;
var shown_row;
// canvas to render patterns onto
@ -156,19 +156,22 @@ function RenderPattern(canv, pattern) {
}
function redrawScreen() {
if (audio_events.length === 0) return;
var e;
var t = player.audioctx.currentTime;
do {
while (audio_events.length > 0 && audio_events[0].t < t) {
e = audio_events.shift();
} while(e.t < t && audio_events.length > 0);
if (!e) return;
}
if (!e) {
if (player.playing) {
window.requestAnimationFrame(redrawScreen);
}
return;
}
var VU = e.vu;
var scopes = e.scopes;
var ctx;
if (e.scopes) {
if (e.scopes !== undefined) {
// update VU meters & oscilliscopes
var canvas = document.getElementById("vu");
ctx = canvas.getContext("2d");
@ -217,9 +220,8 @@ function redrawScreen() {
shown_row = e.row;
}
if (audio_events.length > 0) {
var next_event = audio_events[0].t;
setTimeout(redrawScreen, 1000*(next_event - player.audioctx.currentTime));
if (player.playing) {
window.requestAnimationFrame(redrawScreen);
}
}
@ -291,6 +293,8 @@ function init() {
shown_row = undefined;
pat_canvas_patnum = undefined;
audio_events = [];
paused_events = [];
audio_events.push({
t: 0, row: 0, pat: player.xm.songpats[0],
vu: new Float32Array(player.xm.nchan),

170
xm.js
View file

@ -1,4 +1,4 @@
(function (window, document) {
(function (window) {
if (!window.XMPlayer) {
window.XMPlayer = {};
}
@ -7,14 +7,15 @@ var player = window.XMPlayer;
if (!window.XMView) {
window.XMView = {};
}
var view = window.XMView;
var XMView = window.XMView;
player.PeriodForNote = PeriodForNote;
player.prettify_effect = prettify_effect;
player.initAudio = initAudio;
player.loadXM = loadXM;
player.playXM = playXM;
player.pauseXM = pauseXM;
player.stopXM = stopXM;
player.downloadXM = downloadXM;
player.cur_songpos = -1;
player.cur_pat = -1;
player.cur_row = 64;
@ -513,7 +514,7 @@ function audio_cb(e) {
var offset = 0;
var ticklen = 0|(f_smp * 2.5 / player.xm.bpm);
var _scope_width = view.scope_width || 0;
var scopewidth = XMView.scope_width;
while(buflen > 0) {
if (player.cur_pat == -1 || player.cur_ticksamp >= ticklen) {
@ -522,12 +523,12 @@ function audio_cb(e) {
}
var tickduration = Math.min(buflen, ticklen - player.cur_ticksamp);
var VU = new Float32Array(player.xm.nchan);
var scopes;
var scopes = undefined;
for (j = 0; j < player.xm.nchan; j++) {
var scope;
if (tickduration >= 4*_scope_width) {
scope = new Float32Array(_scope_width);
for (k = 0; k < _scope_width; k++) {
if (tickduration >= 4*scopewidth) {
scope = new Float32Array(scopewidth);
for (k = 0; k < scopewidth; k++) {
scope[k] = -dataL[offset+k*4] - dataR[offset+k*4];
}
}
@ -536,25 +537,27 @@ function audio_cb(e) {
player.xm.channelinfo[j], offset, offset + tickduration, dataL, dataR) /
tickduration;
if (tickduration >= 4*_scope_width) {
for (k = 0; k < _scope_width; k++) {
if (tickduration >= 4*scopewidth) {
for (k = 0; k < scopewidth; k++) {
scope[k] += dataL[offset+k*4] + dataR[offset+k*4];
}
if (scopes === undefined) scopes = [];
scopes.push(scope);
}
}
view.pushEvent({
t: e.playbackTime + (0.0 + offset) / f_smp,
vu: VU,
scopes: scopes,
songpos: player.cur_songpos,
pat: player.cur_pat,
row: player.cur_row - 1
});
offset += tickduration;
player.cur_ticksamp += tickduration;
buflen -= tickduration;
if (XMView.pushEvent) {
XMView.pushEvent({
t: e.playbackTime + (0.0 + offset) / f_smp,
vu: VU,
scopes: scopes,
songpos: player.cur_songpos,
pat: player.cur_pat,
row: player.cur_row - 1
});
offset += tickduration;
player.cur_ticksamp += tickduration;
buflen -= tickduration;
}
}
}
@ -616,7 +619,7 @@ function UnrollSampleLoop(samp) {
samp.type = 1;
}
function LoadXM(arrayBuf) {
function loadXM(arrayBuf) {
var dv = new DataView(arrayBuf);
player.xm = {};
@ -839,8 +842,7 @@ function LoadXM(arrayBuf) {
}
console.log("loaded \"" + player.xm.songname + "\"");
return player.xm;
return true;
}
var jsNode, gainNode;
@ -860,11 +862,11 @@ function initAudio() {
gainNode.connect(player.audioctx.destination);
}
var playing = false;
player.playing = false;
function playXM() {
if (!playing) {
if (!player.playing) {
// put paused events back into action, if any
view.resume();
if (XMView.resume) XMView.resume();
// start playing
jsNode.connect(gainNode);
@ -881,125 +883,27 @@ function playXM() {
});
*/
}
playing = true;
player.playing = true;
}
function pauseXM() {
if (playing) {
if (player.playing) {
jsNode.disconnect(gainNode);
view.pause();
if (XMView.pause) XMView.pause();
}
playing = false;
player.playing = false;
}
function stopXM() {
if (playing) {
if (player.playing) {
jsNode.disconnect(gainNode);
playing = false;
player.playing = false;
}
player.cur_pat = -1;
player.cur_row = 64;
player.cur_songpos = -1;
player.cur_ticksamp = 0;
if (XMView.stop) XMView.stop();
initAudio();
}
function loadXMAndInit(xmdata) {
player.xm = LoadXM(xmdata);
if (!player.xm) return;
view.init();
var playbutton = document.getElementById("playpause");
playbutton.innerHTML='Play';
playbutton.onclick = function() {
if (playing) {
pauseXM();
playbutton.innerHTML='Play';
} else {
playXM();
playbutton.innerHTML='Pause';
}
};
playbutton.disabled = false;
return player.xm;
}
function downloadXM(uri) {
var xmReq = new XMLHttpRequest();
xmReq.open("GET", uri, true);
xmReq.responseType = "arraybuffer";
xmReq.onload = function (xmEvent) {
var arrayBuffer = xmReq.response;
if (arrayBuffer) {
player.xm = loadXMAndInit(arrayBuffer);
} else {
console.log("unable to load", uri);
}
};
xmReq.send(null);
}
player.allowDrop = function(ev) {
ev.stopPropagation();
ev.preventDefault();
var elem = document.getElementById("playercontainer");
elem.className = (ev.type == "dragover" ? "draghover" : "playercontainer");
return false;
};
player.handleDrop = function(e) {
console.log(e);
e.preventDefault();
var elem = document.getElementById("playercontainer");
elem.className = "playercontainer";
var files = e.target.files || e.dataTransfer.files;
if (files.length < 1) return false;
var reader = new FileReader();
reader.onload = function(e) {
stopXM();
loadXMAndInit(e.target.result);
};
reader.readAsArrayBuffer(files[0]);
return false;
};
function initFilelist() {
var el = document.getElementById('filelist');
xmuris.forEach(function(entry) {
var a = document.createElement('a');
a.text = entry[0];
a.href = '#'+entry[1];
a.onclick = function() {
el.style.display = "none";
stopXM();
downloadXM(baseuri + entry[1]);
};
el.appendChild(a);
el.appendChild(document.createElement('br'));
});
var loadbutton = document.getElementById('loadbutton');
loadbutton.onclick = function() {
if (el.style.display == "none") {
el.style.display = "block";
} else {
el.style.display = "none";
}
};
}
window.onload = function() {
initAudio();
initFilelist();
var uri = location.hash.substr(1);
if (uri === "") {
uri = "kamel.xm";
}
if (!uri.startsWith("http")) {
uri = baseuri + uri;
}
downloadXM(uri);
};
})(window, document);
})(window);