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:
parent
5aa5c06ab0
commit
2cf8840665
2 changed files with 143 additions and 58 deletions
53
index.html
53
index.html
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
150
xm.js
150
xm.js
|
|
@ -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;
|
||||||
|
|
@ -1108,7 +1122,7 @@ function PlayXM() {
|
||||||
function PauseXM() {
|
function PauseXM() {
|
||||||
if (playing) {
|
if (playing) {
|
||||||
jsNode.disconnect(gainNode);
|
jsNode.disconnect(gainNode);
|
||||||
// grab all the audio events
|
// grab all the audio events
|
||||||
var t = audioctx.currentTime;
|
var t = audioctx.currentTime;
|
||||||
while (audio_events.length > 0) {
|
while (audio_events.length > 0) {
|
||||||
var e = audio_events.shift();
|
var e = audio_events.shift();
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue