diff --git a/libxmp.exported b/libxmp.exported index 4293117..7865ef7 100644 --- a/libxmp.exported +++ b/libxmp.exported @@ -19,4 +19,7 @@ _xmp_channel_mute _xmp_channel_vol _xmp_inject_event _xmp_test_module_from_memory -_xmp_load_module_from_memory \ No newline at end of file +_xmp_load_module_from_memory +_xmp_prev_position +_xmp_next_position +_xmp_set_position \ No newline at end of file diff --git a/src/player.css b/src/player.css index d44001d..b9a5ca5 100644 --- a/src/player.css +++ b/src/player.css @@ -94,19 +94,20 @@ div.tracker .header .controls .part { border: var(--header-border); color: white; } -div.tracker .header .controls .title-div { display: flex; grid-column: span 3 / span 3; } -div.tracker .header .controls .player-div { padding: 10px 0px 0px 0px; text-align: center; grid-column: span 2 / span 2; grid-column-start: 3; grid-row-start: 3; } -div.tracker .header .controls .type-div { margin: 2px; display: flex; grid-column: span 2 / span 2; grid-row-start: 4; } +div.tracker .header .controls .title-div { grid-column: span 2 / span 2; } +div.tracker .header .controls .seq-div { grid-column-start: 3; } div.tracker .header .controls .bpm-div { padding: 10px 0px 0px 0px; text-align: center; grid-column-start: 4; } -div.tracker .header .controls .vol-div { display:flex; min-width: 280px; gap: 4px; grid-column: span 2 / span 2; grid-column-start: 1; grid-row-start: 2; } -div.tracker .header .controls .stsep-div { display:flex; min-width: 280px; gap: 4px; grid-column: span 2 / span 2; grid-column-start: 3; grid-row-start: 2; } +div.tracker .header .controls .vol-div { min-width: 280px; gap: 4px; grid-column: span 2 / span 2; grid-row-start: 2; } +div.tracker .header .controls .stsep-div { min-width: 280px; gap: 4px; grid-column: span 2 / span 2; grid-column-start: 3; grid-row-start: 2; } +div.tracker .header .controls .player-div { padding: 10px 0px 0px 0px; text-align: center; grid-column: span 2 / span 2; grid-row-start: 3; } +div.tracker .header .controls .type-div { grid-column: span 2 / span 2; grid-column-start: 3; grid-row-start: 3; } div.tracker .header .controls .instruments-div, div.tracker .header .controls .samples-div { display: none; } body.tracker div.tracker .header .controls .instruments-div { grid-column: span 2 / span 2; - grid-row: span 4 / span 4; + grid-row: span 5 / span 5; grid-column-start: 5; grid-row-start: 1; display: unset; @@ -114,30 +115,38 @@ body.tracker div.tracker .header .controls .instruments-div { body.tracker div.tracker .header .controls .samples-div { grid-column: span 2 / span 2; - grid-row: span 4 / span 4; + grid-row: span 5 / span 5; grid-column-start: 7; grid-row-start: 1; display: unset; } -div.tracker .header .controls button#play { grid-column-start: 1; grid-row-start: 3; } -div.tracker .header .controls button#pause { grid-column-start: 1; grid-row-start: 4; } -div.tracker .header .controls button#stop { grid-column-start: 2; grid-row-start: 3; } -div.tracker .header .controls button#download { grid-column-start: 2; grid-row-start: 4; } +div.tracker .header .controls button#play { grid-row-start: 4; } +div.tracker .header .controls button#pause { grid-row-start: 4; } +div.tracker .header .controls button#stop { grid-row-start: 4; } +div.tracker .header .controls button#download { grid-row-start: 4; } -div.tracker .header .controls .title-div .title { display: flex; } -div.tracker .header .controls .title-div .part { flex-grow: 1; } -div.tracker .header .controls .title-div .part:not(.title) { padding: 10px 0px 0px 0px; text-align: center; } +div.tracker .header .controls .header-div { display: flex; color: var(--text-color); } +div.tracker .header .controls .header-div .part { flex-grow: 1; padding: 0px 4px; } +div.tracker .header .controls .header-div .part:not(.text) { text-align: center; font-size: 14px; padding: 2px 0px 0px 0px; } +div.tracker .header .controls .title-div .title, +div.tracker .header .controls .type-div .type, +div.tracker .header .controls .seq-div .seq, +div.tracker .header .controls .vol-div .vol, +div.tracker .header .controls .stsep-div .stsep { display: flex; } +div.tracker .header .controls .title-div .part, +div.tracker .header .controls .type-div .part, +div.tracker .header .controls .seq-div .part, +div.tracker .header .controls .vol-div .part, +div.tracker .header .controls .stsep-div .part { flex-grow: 1; } +div.tracker .header .controls .seq-div .seq button { flex-grow: 1; font-size: 14px; } div.tracker .header .controls .vol-div img, -div.tracker .header .controls .stsep-div img { padding: 6px; } - -div.tracker .header .controls .vol-div .vol-text, -div.tracker .header .controls .stsep-div .stsep-text { margin: 4px 0px; width: 40px; padding :4px 10px; } +div.tracker .header .controls .stsep-div img { padding: 8px; } div.tracker .header .controls .container { overflow-x: clip; overflow-x: auto; - height: 132px; + height: 162px; font-size: 14px; } @@ -203,7 +212,11 @@ input[type="range"]::slider-thumb { } div.tracker .header .controls .title-div .title input, -div.tracker .header .controls .type-div input { flex-grow: 1; } +div.tracker .header .controls .type-div input, +div.tracker .header .controls .vol-div input, +div.tracker .header .controls .stsep-div input { flex-grow: 1; } +div.tracker .header .controls .vol-div input, +div.tracker .header .controls .stsep-div input { height: 16px; } div.tracker .header .curtime, div.tracker .header .totaltime { } div.tracker .header .speed, div.tracker .header .bpm { display: inline-block; margin: 2px; } diff --git a/src/player.html b/src/player.html index cc253f5..cb2b2d9 100644 --- a/src/player.html +++ b/src/player.html @@ -11,16 +11,43 @@
-
-
00:00
+
+
Song Name:
+
00:00
+
00:00
+
-
00:00
+
+
+
+
Subsong:
+
1 / 1
+
+
0 / 0 bpm
-
100%
-
50%
-
WaXMPlayer 1.0
+
+
+
Volume:
+
100%
+
+
+
+
+
+
Stereo Separation:
+
100%
+
+
+
+
WaXMPlayer
+
+
+
Format:
+
+
+
diff --git a/src/player.js b/src/player.js index d812a9d..9bcb929 100644 --- a/src/player.js +++ b/src/player.js @@ -95,10 +95,12 @@ const EFFECTS_MAP = new Map([ ], ]); +const version = [ 1, 1 ]; let lib = null, ctx = 0, audio = null, node = null, xmi = null; let playing = false, paused = false; +let sequence = 0, sequences = 1; let lastPattern = -1, lastPos = -1, lastRow = -1, lastSpeed = -1, lastBpm = -1; -let volume = 100, stereo = 50; +let volume = 100, stereo = 100; let efx_map = new Map(); let meters = new Array(); let active_meters = new Array(); @@ -114,11 +116,15 @@ const buttonPlay = uiControls.querySelector(':scope button#play'); const buttonPause = uiControls.querySelector(':scope button#pause'); const buttonStop = uiControls.querySelector(':scope button#stop'); const buttonDownload = uiControls.querySelector(':scope button#download'); +const buttonPrevSeq = uiControls.querySelector(':scope button#prev-seq'); +const buttonNextSeq = uiControls.querySelector(':scope button#next-seq'); const titleBar = uiControls.querySelector(':scope input[name=title]'); +const playerText = uiControls.querySelector(':scope .player-div'); const curTimeText = uiControls.querySelector(':scope .curtime'); const totalTimeText = uiControls.querySelector(':scope .totaltime'); const speedText = uiControls.querySelector(':scope .speed'); const bpmText = uiControls.querySelector(':scope .bpm'); +const subsongText = uiControls.querySelector(':scope .seqno'); const typeText = uiControls.querySelector(':scope input[name=type]'); const volumeBar = uiControls.querySelector(':scope input[name=volume]'); const volumeText = uiControls.querySelector(':scope .vol-div .vol-text'); @@ -143,11 +149,13 @@ let sampleRows = []; libxmp().then(m => { lib = m; - if (!lib) throw new Error('libxmp module is null'); + if (!lib) throw new Error('libxmp module failed to load'); ctx = lib._xmp_create_context(); if (!ctx) throw new Error('Failed to create context'); + playerText.innerText = "WaXMPlayer "+version.join('.'); + console.log('libxmp ready, ctx:', ctx); volumeBar.value = volume; stereoBar.value = stereo; @@ -156,36 +164,22 @@ libxmp().then(m => { e.preventDefault(); }); }); - + ['dragleave', 'drop'].forEach(ev => { document.body.addEventListener(ev, e => { e.preventDefault(); }); }); - + const params = new URLSearchParams(window.location.search); let url = params.get('media'); if (url) { loadFromUrl(url); } else { + titleBar.value = "Drop a file to load"; document.body.addEventListener('drop', e => { const file = e.dataTransfer.files[0]; if (file) { - stop(); - orderCells.forEach(e => e.remove()); - patternTables.forEach(e => e.remove()); - patternMeters.forEach(e => {e.forEach(f => {f.l.remove(); f.r.remove();})}); - patternRows.forEach(e => e.forEach(f => f.remove())); - instrumentRows.forEach(e => e.remove()); - sampleRows.forEach(e => e.remove()); - orderCells = []; - patternTables = []; - patternMeters = []; - patternRows = []; - instrumentRows = []; - sampleRows = []; - meters = new Array(); - active_meters = new Array(); filename = file.name; const objectUrl = URL.createObjectURL(file); file.arrayBuffer().then(buffer => loadModule(new Uint8Array(buffer))); @@ -491,6 +485,7 @@ function get_frame_info(context) { } async function loadFromUrl(url) { + titleBar.value = "Loading..."; const res = await fetch(url); console.log(res); if (!res.ok) throw new Error(`HTTP ${res.status}`); @@ -498,7 +493,7 @@ async function loadFromUrl(url) { if (filename && (filename.indexOf('attachment') !== -1 || filename.indexOf('inline') !== -1)) { var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/; var matches = filenameRegex.exec(filename); - if (matches != null && matches[1]) { + if (matches != null && matches[1]) { filename = matches[1].replace(/['"]/g, ''); } } else { @@ -512,13 +507,32 @@ async function loadFromUrl(url) { } async function loadModule(u8buf) { + stop(); + sequence = 0; sequences = 1; + subsongText.innerText = sequence + " / " + sequences; + buttonPrevSeq.disabled = buttonNextSeq.disabled = true; + orderCells.forEach(e => e.remove()); + patternTables.forEach(e => e.remove()); + patternMeters.forEach(e => {e.forEach(f => {f.l.remove(); f.r.remove();})}); + patternRows.forEach(e => e.forEach(f => f.remove())); + instrumentRows.forEach(e => e.remove()); + sampleRows.forEach(e => e.remove()); + orderCells = []; + patternTables = []; + patternMeters = []; + patternRows = []; + instrumentRows = []; + sampleRows = []; + meters = new Array(); + active_meters = new Array(); + try { + titleBar.value = "Loading..."; console.log('Module size:', u8buf.byteLength, 'bytes'); const ptr = lib._malloc(u8buf.byteLength); if (!ptr) throw new Error('Malloc failed for module'); lib.HEAPU8.set(u8buf, ptr); - console.log(u8buf); const ret = lib._xmp_load_module_from_memory(ctx, ptr, u8buf.byteLength); lib._free(ptr); @@ -536,14 +550,18 @@ async function loadModule(u8buf) { xmi = get_module_info(ctx); efx_map = getEffectsMap(); + sequences = xmi.num_sequences; + subsongText.innerText = (sequence+1) + " / " + sequences; + if (sequences > 1) buttonNextSeq.disabled = false; + for(let c = 0; c < xmi.mod.chn; c++) active_meters.push({l:null,r:null}); Promise.resolve().then(() => { titleBar.value = xmi.mod.name; typeText.value = xmi.mod.type; - totalTimeText.textContent = (Math.floor(((xmi.sequences[0].length / 1000) / 60) % 60)).toString().padStart(2, '0') + ':' + - (Math.floor((xmi.sequences[0].length / 1000) % 60)).toString().padStart(2, '0'); + totalTimeText.textContent = (Math.floor(((xmi.sequences[sequence].length / 1000) / 60) % 60)).toString().padStart(2, '0') + ':' + + (Math.floor((xmi.sequences[sequence].length / 1000) % 60)).toString().padStart(2, '0'); if (mode == 'tracker') { preloadOrder(); @@ -737,6 +755,10 @@ async function play() { dispatch('rowChanged', {xfi}); lastRow = xfi.row; } + if (xfi.sequence !== sequence) { + dispatch('sequenceChanged', {xfi}); + sequence = xfi.sequence; + } if (xfi.speed !== lastSpeed) { dispatch('speedChanged', {xfi}); lastSpeed = xfi.speed; @@ -768,6 +790,7 @@ async function play() { lib._xmp_start_player(ctx, SR, 0); lib._xmp_set_player(ctx, 7, volume); lib._xmp_set_player(ctx, 1, stereo); + lib._xmp_set_position(ctx, xmi.sequences[sequence].start); } paused = false; @@ -796,6 +819,20 @@ function stop() { buttonStop.disabled = true; } +function prev_sequence() { + if (sequence > 0) { + if (playing) lib._xmp_set_position(ctx, xmi.sequences[sequence-1].start); + else { sequence--; dispatch('sequenceChanged', {xfi:{sequence:sequence}}); } + } +} + +function next_sequence() { + if (sequence < sequences) { + if (playing) lib._xmp_set_position(ctx, xmi.sequences[sequence+1].start); + else { sequence++; dispatch('sequenceChanged', {xfi:{sequence:sequence}}); } + } +} + function changeVolume() { volumeText.textContent = volumeBar.value+'%'; volume = volumeBar.value; @@ -811,6 +848,8 @@ function changeStereoSeparation() { buttonPlay.onclick = play; buttonPause.onclick = pause; buttonStop.onclick = stop; +buttonNextSeq.onclick = next_sequence; +buttonPrevSeq.onclick = prev_sequence; volumeBar.oninput = changeVolume; stereoBar.oninput = changeStereoSeparation; @@ -879,6 +918,14 @@ window.addEventListener('speedChanged', e => { speedText.textContent = e.detail.xfi.speed; }); +window.addEventListener('sequenceChanged', e => { + buttonPrevSeq.disabled = (e.detail.xfi.sequence+1) <= 1; + buttonNextSeq.disabled = (e.detail.xfi.sequence+1) >= sequences; + subsongText.textContent = (e.detail.xfi.sequence+1) + " / " + sequences; + totalTimeText.textContent = (Math.floor(((xmi.sequences[e.detail.xfi.sequence].length / 1000) / 60) % 60)).toString().padStart(2, '0') + ':' + + (Math.floor((xmi.sequences[e.detail.xfi.sequence].length / 1000) % 60)).toString().padStart(2, '0'); +}); + window.addEventListener('bpmChanged', e => { bpmText.textContent = e.detail.xfi.bpm; }); diff --git a/src/stereo.png b/src/stereo.png deleted file mode 100644 index ff36058..0000000 Binary files a/src/stereo.png and /dev/null differ diff --git a/src/volume.png b/src/volume.png deleted file mode 100644 index beeb0d2..0000000 Binary files a/src/volume.png and /dev/null differ