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; } #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> </style>
</head> </head>
<body onload="main()"> <body>
<div style="background:#000"> <div id="playercontainer" class="playercontainer" ondrop="handleDrop(event)"
<div><canvas class="centered" id='title' width=640 height=22></canvas></div> ondragover="allowDrop(event)" ondragleave="allowDrop(event)">
<div><canvas class="centered" id='vu' width=224 height=64></canvas></div> <div><canvas class="centered" id='title' width="640" height="22"></canvas></div>
<div><canvas class="centered" id='gfxpattern' width=640 height=200></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 id='instruments'></div>
<div> <div>
<p style="text-align: center"> <p style="text-align: center">
<button id='playpause' disabled=true style="width: 100px">Play</button> <button id="playpause" disabled="true" style="width: 100px; background: #ccc;">Play</button>
<button id='loadbutton' style="width: 100px">Load</button> <button id="loadbutton" style="width: 100px; background: #ccc">Load</button>
</p> </p>
</div> </div>
<div style="display: none" id='filelist'></div> <div style="display: none" id='filelist'></div>
</div> </div>
<pre> <pre>
code: <a href="http://github.com/a1k0n/jsxm/">github.com/a1k0n/jsxm</a> code: <a href="http://github.com/a1k0n/jsxm/">github.com/a1k0n/jsxm</a>
todo: todo:
@ -37,7 +74,7 @@ todo:
- 7xy - tremolo - 7xy - tremolo
- Gxx, Hxy, Kxx, Lxx, Pxy, Txy - Gxx, Hxy, Kxx, Lxx, Pxy, Txy
- render pattern with the wider fonts for fewer channels - 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> </pre>
</body> </body>

148
xm.js
View file

@ -243,11 +243,13 @@ function RedrawScreen() {
// oscilloscope // oscilloscope
var scope = scopes[j]; var scope = scopes[j];
ctx.beginPath(); if (scope != undefined) {
for (var k = 0; k < _scope_width; k++) { ctx.beginPath();
ctx.lineTo(x + 1 + k, 32 - 16 * scope[k]); 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; len /= 2;
var samp = new Float32Array(len); var samp = new Float32Array(len);
for (var k = 0; k < len; k++) { for (var k = 0; k < len; k++) {
acc += array[k*2] + (array[k*2 + 1] << 8); var b = array[k*2] + (array[k*2 + 1] << 8);
var b = acc&65535;
if (b & 32768) b = b-65536; 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; return samp;
} }
@ -766,7 +768,6 @@ function ConvertSample(array, bits) {
// uninterrupted for as long as possible; this also handles pingpong loops. // uninterrupted for as long as possible; this also handles pingpong loops.
function UnrollSampleLoop(samp) { function UnrollSampleLoop(samp) {
var nloops = ((2048 + samp.looplen - 1) / samp.looplen) | 0; var nloops = ((2048 + samp.looplen - 1) / samp.looplen) | 0;
console.log(samp.looplen, nloops);
var pingpong = samp.type & 2; var pingpong = samp.type & 2;
if (pingpong) { if (pingpong) {
// make sure we have an even number of loops if we are pingponging // make sure we have an even number of loops if we are pingponging
@ -896,9 +897,7 @@ function LoadXM(arrayBuf) {
'number': i, 'number': i,
}; };
if (nsamp > 0) { if (nsamp > 0) {
// return a slice so we have a fresh copy and don't retain pointers to var samplemap = new Uint8Array(arrayBuf, idx+33, 96);
// the original xm file arraybuf forever
var samplemap = new Uint8Array(arrayBuf, idx+33, 96).slice();
var env_nvol = dv.getUint8(idx+225); var env_nvol = dv.getUint8(idx+225);
var env_vol_type = dv.getUint8(idx+233); var env_vol_type = dv.getUint8(idx+233);
@ -1074,21 +1073,25 @@ function LoadXM(arrayBuf) {
return xm; return xm;
} }
var playing = false;
var jsNode, gainNode; var jsNode, gainNode;
var paused_events = [];
function InitAudio() { function InitAudio() {
if (audioctx == undefined) { if (audioctx == undefined) {
audioContext = window.AudioContext || window.webkitAudioContext; var audioContext = window.AudioContext || window.webkitAudioContext;
audioctx = new audioContext(); audioctx = new audioContext();
gainNode = audioctx.createGain(); gainNode = audioctx.createGain();
gainNode.gain.value = 0.1; // master volume 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; jsNode.onaudioprocess = audio_cb;
gainNode.connect(audioctx.destination); gainNode.connect(audioctx.destination);
} }
var playing = false;
var paused_events = [];
function PlayXM() { function PlayXM() {
if (!playing) { if (!playing) {
// put paused events back into action, if any // put paused events back into action, if any
@ -1100,6 +1103,17 @@ function PlayXM() {
} }
// start playing // start playing
jsNode.connect(gainNode); 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); requestAnimationFrame(RedrawScreen);
} }
playing = true; playing = true;
@ -1130,6 +1144,45 @@ function StopXM() {
InitAudio(); 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) { function DownloadXM(uri) {
var xmReq = new XMLHttpRequest(); var xmReq = new XMLHttpRequest();
xmReq.open("GET", uri, true); xmReq.open("GET", uri, true);
@ -1137,45 +1190,37 @@ function DownloadXM(uri) {
xmReq.onload = function (xmEvent) { xmReq.onload = function (xmEvent) {
var arrayBuffer = xmReq.response; var arrayBuffer = xmReq.response;
if (arrayBuffer) { if (arrayBuffer) {
xm = LoadXM(arrayBuffer); xm = LoadXMAndInit(arrayBuffer);
} else { } else {
console.log("unable to load", uri); 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); 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() { function InitFilelist() {
var el = document.getElementById('filelist'); var el = document.getElementById('filelist');
xmuris.forEach(function(entry) { xmuris.forEach(function(entry) {
@ -1200,13 +1245,16 @@ function InitFilelist() {
} }
} }
function main() { window.onload = function() {
InitAudio(); InitAudio();
InitFilelist(); InitFilelist();
var uri = location.hash.substr(1); var uri = location.hash.substr(1);
if (uri == "") { if (uri == "") {
uri = "kamel.xm"; uri = "kamel.xm";
} }
DownloadXM(baseuri + uri); if (!uri.startsWith("http")) {
uri = baseuri + uri;
}
DownloadXM(uri);
} }