Contact with the real world

Many, many fixes found after posting this on my website.

 - drag n drop supported
 - plays on OSX and iOS safari now
 - random other hiccups fixed

there seems to be some remaining issues cleaning up when loading a new
song.
This commit is contained in:
Andy Sloane 2015-11-10 20:30:41 -08:00
commit 2cf8840665
2 changed files with 143 additions and 58 deletions

View file

@ -13,22 +13,59 @@
#filelist a { color: #fff; }
.playercontainer {
background:#000;
}
.draghover {
background:#000;
opacity: 0.5;
}
.hscroll {
overflow: auto;
margin-bottom: 14px;
}
.hscroll::-webkit-scrollbar {
-webkit-appearance: none;
}
.hscroll::-webkit-scrollbar:horizontal {
height: 11px;
}
.hscroll::-webkit-scrollbar-thumb {
border-radius: 2px;
border: 1px solid #93C3E9; /* should match background, can't be transparent */
background-color: #A0D4FD;
}
.hscroll::-webkit-scrollbar-track {
background-color: #333;
border-radius: 8px;
}
</style>
</head>
<body onload="main()">
<div style="background:#000">
<div><canvas class="centered" id='title' width=640 height=22></canvas></div>
<div><canvas class="centered" id='vu' width=224 height=64></canvas></div>
<div><canvas class="centered" id='gfxpattern' width=640 height=200></canvas></div>
<body>
<div id="playercontainer" class="playercontainer" ondrop="handleDrop(event)"
ondragover="allowDrop(event)" ondragleave="allowDrop(event)">
<div><canvas class="centered" id='title' width="640" height="22"></canvas></div>
<div class="hscroll">
<div><canvas class="centered" id='vu' width="224" height="64"></canvas></div>
<div><canvas class="centered" id='gfxpattern' width="640" height="200"></canvas></div>
</div>
<div id='instruments'></div>
<div>
<p style="text-align: center">
<button id='playpause' disabled=true style="width: 100px">Play</button>
<button id='loadbutton' style="width: 100px">Load</button>
<button id="playpause" disabled="true" style="width: 100px; background: #ccc;">Play</button>
<button id="loadbutton" style="width: 100px; background: #ccc">Load</button>
</p>
</div>
<div style="display: none" id='filelist'></div>
</div>
<pre>
code: <a href="http://github.com/a1k0n/jsxm/">github.com/a1k0n/jsxm</a>
todo:
@ -37,7 +74,7 @@ todo:
- 7xy - tremolo
- Gxx, Hxy, Kxx, Lxx, Pxy, Txy
- render pattern with the wider fonts for fewer channels
- drag-n-drop .xm files onto player
- fix occasional rendering/audio hiccups when switching songs
</pre>
</body>

150
xm.js
View file

@ -243,11 +243,13 @@ function RedrawScreen() {
// oscilloscope
var scope = scopes[j];
ctx.beginPath();
for (var k = 0; k < _scope_width; k++) {
ctx.lineTo(x + 1 + k, 32 - 16 * scope[k]);
if (scope != undefined) {
ctx.beginPath();
for (var k = 0; k < _scope_width; k++) {
ctx.lineTo(x + 1 + k, 32 - 16 * scope[k]);
}
ctx.stroke();
}
ctx.stroke();
}
}
@ -753,10 +755,10 @@ function ConvertSample(array, bits) {
len /= 2;
var samp = new Float32Array(len);
for (var k = 0; k < len; k++) {
acc += array[k*2] + (array[k*2 + 1] << 8);
var b = acc&65535;
var b = array[k*2] + (array[k*2 + 1] << 8);
if (b & 32768) b = b-65536;
samp[k] = b / 32768.0;
acc = Math.max(-1, Math.min(1, acc + b / 32768.0));
samp[k] = acc;
}
return samp;
}
@ -766,7 +768,6 @@ function ConvertSample(array, bits) {
// uninterrupted for as long as possible; this also handles pingpong loops.
function UnrollSampleLoop(samp) {
var nloops = ((2048 + samp.looplen - 1) / samp.looplen) | 0;
console.log(samp.looplen, nloops);
var pingpong = samp.type & 2;
if (pingpong) {
// make sure we have an even number of loops if we are pingponging
@ -896,9 +897,7 @@ function LoadXM(arrayBuf) {
'number': i,
};
if (nsamp > 0) {
// return a slice so we have a fresh copy and don't retain pointers to
// the original xm file arraybuf forever
var samplemap = new Uint8Array(arrayBuf, idx+33, 96).slice();
var samplemap = new Uint8Array(arrayBuf, idx+33, 96);
var env_nvol = dv.getUint8(idx+225);
var env_vol_type = dv.getUint8(idx+233);
@ -1074,21 +1073,25 @@ function LoadXM(arrayBuf) {
return xm;
}
var playing = false;
var jsNode, gainNode;
var paused_events = [];
function InitAudio() {
if (audioctx == undefined) {
audioContext = window.AudioContext || window.webkitAudioContext;
var audioContext = window.AudioContext || window.webkitAudioContext;
audioctx = new audioContext();
gainNode = audioctx.createGain();
gainNode.gain.value = 0.1; // master volume
}
jsNode = audioctx.createScriptProcessor(16384, 0, 2);
if (audioctx.createScriptProcessor == undefined) {
jsNode = audioctx.createJavaScriptNode(16384, 0, 2);
} else {
jsNode = audioctx.createScriptProcessor(16384, 0, 2);
}
jsNode.onaudioprocess = audio_cb;
gainNode.connect(audioctx.destination);
}
var playing = false;
var paused_events = [];
function PlayXM() {
if (!playing) {
// put paused events back into action, if any
@ -1100,6 +1103,17 @@ function PlayXM() {
}
// start playing
jsNode.connect(gainNode);
// hack to get iOS to play anything
var temp_osc = audioctx.createOscillator();
temp_osc.connect(audioctx.destination);
if (temp_osc.noteOn) temp_osc.start = temp_osc.noteOn;
temp_osc.frequency.value = 1;
temp_osc.start(0);
window.setTimeout(10, function() {
temp_osc.disconnect();
});
requestAnimationFrame(RedrawScreen);
}
playing = true;
@ -1108,7 +1122,7 @@ function PlayXM() {
function PauseXM() {
if (playing) {
jsNode.disconnect(gainNode);
// grab all the audio events
// grab all the audio events
var t = audioctx.currentTime;
while (audio_events.length > 0) {
var e = audio_events.shift();
@ -1130,6 +1144,45 @@ function StopXM() {
InitAudio();
}
function LoadXMAndInit(xmdata) {
xm = LoadXM(xmdata);
if (!xm) return;
document.getElementById('vu').width = _pattern_border + _pattern_cellwidth * xm.nchan;
var gfxpattern = document.getElementById("gfxpattern");
gfxpattern.width = _pattern_cellwidth * xm.nchan + _pattern_border;
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;
// generate a fake audio event to render the initial paused screen
var scopes = [];
for (var i = 0; i < xm.nchan; i++) {
scopes.push(new Float32Array(_scope_width));
}
// reset display
shown_row = undefined;
pat_canvas_patnum = undefined;
audio_events.push({
t: 0, row: 0, pat: xm.songpats[0],
vu: new Float32Array(xm.nchan),
scopes: scopes
});
RedrawScreen();
return xm;
}
function DownloadXM(uri) {
var xmReq = new XMLHttpRequest();
xmReq.open("GET", uri, true);
@ -1137,45 +1190,37 @@ function DownloadXM(uri) {
xmReq.onload = function (xmEvent) {
var arrayBuffer = xmReq.response;
if (arrayBuffer) {
xm = LoadXM(arrayBuffer);
xm = LoadXMAndInit(arrayBuffer);
} else {
console.log("unable to load", uri);
}
document.getElementById('vu').width = _pattern_border + _pattern_cellwidth * xm.nchan;
var gfxpattern = document.getElementById("gfxpattern");
gfxpattern.width = _pattern_cellwidth * xm.nchan + _pattern_border;
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;
// generate a fake audio event to render the initial paused screen
var scopes = [];
for (var i = 0; i < xm.nchan; i++) {
scopes.push(new Float32Array(_scope_width));
}
// reset display
shown_row = undefined;
pat_canvas_patnum = undefined;
audio_events.push({
t: 0, row: 0, pat: xm.songpats[0],
vu: new Float32Array(xm.nchan),
scopes: scopes
});
RedrawScreen();
}
xmReq.send(null);
}
function allowDrop(ev) {
ev.stopPropagation();
ev.preventDefault();
var elem = document.getElementById("playercontainer");
elem.className = (ev.type == "dragover" ? "draghover" : "playercontainer");
return false;
}
function handleDrop(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) {
LoadXMAndInit(e.target.result);
}
reader.readAsArrayBuffer(files[0]);
return false;
}
function InitFilelist() {
var el = document.getElementById('filelist');
xmuris.forEach(function(entry) {
@ -1200,13 +1245,16 @@ function InitFilelist() {
}
}
function main() {
window.onload = function() {
InitAudio();
InitFilelist();
var uri = location.hash.substr(1);
if (uri == "") {
uri = "kamel.xm";
}
DownloadXM(baseuri + uri);
if (!uri.startsWith("http")) {
uri = baseuri + uri;
}
DownloadXM(uri);
}