initial commit
This commit is contained in:
894
player.js
Normal file
894
player.js
Normal file
@@ -0,0 +1,894 @@
|
||||
const XMP_NAME_SIZE = 64;
|
||||
const XMP_MAX_MOD_LENGTH = 256;
|
||||
const XMP_MAX_ENV_POINTS = 32;
|
||||
const XMP_MAX_KEYS = 121;
|
||||
const XMP_MAX_CHANNELS = 64;
|
||||
|
||||
const SR = 44100;
|
||||
const BUF = 1024;
|
||||
const BYTES = BUF * 4;
|
||||
const NOTES = ["C ", "C#", "D ", "D#", "E ", "F ", "F#", "G ", "G#", "A ", "A#", "B "];
|
||||
const EFFECTS_MAP = new Map([
|
||||
[
|
||||
'669',
|
||||
new Map([
|
||||
[96, 'A'], [97, 'B'], [98, 'C'], [99, 'D'], [100, 'E'], [126, 'F']
|
||||
])
|
||||
],
|
||||
[
|
||||
'Farandole',
|
||||
new Map([
|
||||
[249, '1'], [248, '2'], [122, '3'], [251, '4'], [254, '5'], [4, '6'],
|
||||
[256, '7'], [252, '8'], [123, '9'], [250, 'C'], [15, 'F']
|
||||
])
|
||||
],
|
||||
[
|
||||
'Imago Orpheus',
|
||||
new Map([
|
||||
[1, '1'], [2, '2'], [3, '3'], [4, '4'], [5, '5'], [6, '6'],
|
||||
[7, '7'], [8, '8'], [9, '9'], [10, 'A'], [11, 'B'], [12, 'C'],
|
||||
[13, 'D'], [14, 'E'], [15, 'F'], [16, 'G'], [17, 'H'], [18, 'I'],
|
||||
[19, 'J'], [20, 'K'], [21, 'L'], [22, 'M'], [23, 'N'], [24, 'O'],
|
||||
[25, 'P'], [26, 'Q'], [27, 'R'], [28, 'S'], [29, 'T'], [30, 'U'],
|
||||
[31, 'V'], [32, 'W'], [33, 'X'], [34, 'Y'], [35, 'Z']
|
||||
])
|
||||
],
|
||||
[
|
||||
'S3M',
|
||||
new Map([
|
||||
[163, 'A'], [11, 'B'], [13, 'C'], [10, 'D'], [2, 'E'], [1, 'F'],
|
||||
[3, 'G'], [4, 'H'], [29, 'I'], [180, 'J'], [6, 'K'], [5, 'L'],
|
||||
[9, 'O'], [27, 'Q'], [7, 'R'], [254, 'S'], [171, 'T'], [172, 'U'],
|
||||
[16, 'V'], [8, 'X'], [141, 'X'], [14, 'S']
|
||||
])
|
||||
],
|
||||
[
|
||||
'IT',
|
||||
new Map([
|
||||
[163, 'A'], [11, 'B'], [142, 'C'], [10, 'D'], [2, 'E'], [1, 'F'],
|
||||
[3, 'G'], [4, 'H'], [29, 'I'], [180, 'J'], [6, 'K'], [5, 'L'],
|
||||
[128, 'M'], [129, 'N'], [9, 'O'], [137, 'P'], [27, 'Q'], [7, 'R'],
|
||||
[254, 'S'], [135, 'T'], [172, 'U'], [16, 'V'], [17, 'W'], [8, 'X'],
|
||||
[138, 'Y'], [141, 'S'], [136, 'S'], [14, 'S'], [192, 'c'], [193, 'd'],
|
||||
[194, 'a'], [195, 'b'], [132, 'S'], [139, 'S'], [140, 'S']
|
||||
])
|
||||
],
|
||||
[
|
||||
'LIQ',
|
||||
new Map([
|
||||
[0, 'A'], [171, 'B'], [13, 'C'], [2, 'D'], [172, 'F'], [11, 'J'],
|
||||
[10, 'L'], [14, 'M'], [3, 'N'], [9, 'O'], [163, 'S'], [7, 'T'],
|
||||
[1, 'U'], [4, 'V'], [5, 'X'], [6, 'Y']
|
||||
])
|
||||
],
|
||||
[
|
||||
'Oktalyzer',
|
||||
new Map([
|
||||
[1, '1'], [2, '2'], [112, '0'], [113, '0'], [114, '0'], [115, '6'],
|
||||
[116, '5'], [156, '6'], [11, 'B'], [15, 'F'], [157, '5'], [12, 'C'],
|
||||
[10, 'A'], [174, 'E'], [17, 'E'], [0, '0']
|
||||
])
|
||||
],
|
||||
[
|
||||
'STX',
|
||||
new Map([
|
||||
[15, 'A'], [11, 'B'], [13, 'C'], [10, 'D'], [2, 'E'], [1, 'F'],
|
||||
[3, 'G'], [4, 'H'], [29, 'I'], [0, 'J']
|
||||
])
|
||||
],
|
||||
[
|
||||
'Funk',
|
||||
new Map([
|
||||
[121, 'A'], [120, 'B'], [122, 'C'], [123, 'D'], [124, 'G'], [125, 'H'],
|
||||
[0, 'L'], [12, 'N'], [127, 'O'], [14, 'O'], [15, 'O']
|
||||
])
|
||||
],
|
||||
[
|
||||
'Protracker',
|
||||
new Map([
|
||||
[0, '0'], [1, '1'], [2, '2'], [3, '3'], [4, '4'], [5, '5'],
|
||||
[6, '6'], [7, '7'], [8, '8'], [9, '9'], [10, 'A'], [11, 'B'],
|
||||
[12, 'C'], [13, 'D'], [14, 'E'], [15, 'F'], [16, 'G'], [27, 'Q'],
|
||||
[181, 'P'], [17, 'H'], [21, 'L'], [164, 'c'], [33, 'X'], [20, 'K'],
|
||||
[25, 'P'], [29, 'T'], [146, '4'], [160, 'x'], [161, 'x'], [171, 'F']
|
||||
])
|
||||
],
|
||||
]);
|
||||
|
||||
let lib = null, ctx = 0, audio = null, node = null, xmi = null;
|
||||
let playing = false, paused = false;
|
||||
let lastPattern = -1, lastPos = -1, lastRow = -1, lastSpeed = -1, lastBpm = -1;
|
||||
let volume = 100, stereo = 50;
|
||||
let efx_map = new Map();
|
||||
let meters = new Array();
|
||||
let active_meters = new Array();
|
||||
|
||||
let filename = 'unknown';
|
||||
let mode = 'tracker';
|
||||
|
||||
/* DOM Elements */
|
||||
const uiRoot = document.body.querySelector('.tracker');
|
||||
const uiHeader = uiRoot.querySelector(':scope > .header');
|
||||
const uiControls = uiHeader.querySelector(':scope > .holder > .controls');
|
||||
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 titleBar = uiControls.querySelector(':scope input[name=title]');
|
||||
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 typeText = uiControls.querySelector(':scope input[name=type]');
|
||||
const volumeBar = uiControls.querySelector(':scope input[name=volume]');
|
||||
const volumeText = uiControls.querySelector(':scope .vol-div .vol-text');
|
||||
const stereoBar = uiControls.querySelector(':scope input[name=stereo]');
|
||||
const stereoText = uiControls.querySelector(':scope .stsep-div .stsep-text');
|
||||
const uiOrder = uiHeader.querySelector(':scope > .order-box > .orders');
|
||||
const uiPatterns = uiRoot.querySelector(':scope > .patterns');
|
||||
const uiInstruments = uiControls.querySelector(':scope .instruments-div .instruments');
|
||||
const uiSamples = uiControls.querySelector(':scope .samples-div .samples');
|
||||
|
||||
let uiCurrentPattern = null, uiCurrentRow = null;
|
||||
/* End DOM Elements */
|
||||
|
||||
let headerHeight = 0, gapHeight = 0, rowHeight = 0;
|
||||
|
||||
let orderCells = [];
|
||||
let patternTables = [];
|
||||
let patternMeters = [];
|
||||
let patternRows = [];
|
||||
let instrumentRows = [];
|
||||
let sampleRows = [];
|
||||
|
||||
libxmp().then(m => {
|
||||
lib = m;
|
||||
if (!lib) throw new Error('libxmp module is null');
|
||||
|
||||
ctx = lib._xmp_create_context();
|
||||
if (!ctx) throw new Error('Failed to create context');
|
||||
|
||||
console.log('libxmp ready, ctx:', ctx);
|
||||
volumeBar.value = volume;
|
||||
stereoBar.value = stereo;
|
||||
['dragenter', 'dragover'].forEach(ev => {
|
||||
document.body.addEventListener(ev, e => {
|
||||
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 {
|
||||
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)));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function dispatch(name, detail) {
|
||||
window.dispatchEvent(new CustomEvent(name, { detail }));
|
||||
}
|
||||
|
||||
function readString(ptr, maxLen = 0) {
|
||||
if (!ptr) return null;
|
||||
return maxLen ? lib.UTF8ToString(ptr, maxLen).replace(/\0.*$/, '') : lib.UTF8ToString(ptr);
|
||||
}
|
||||
|
||||
function readEvent(ptr) {
|
||||
if (!ptr) return null;
|
||||
return {
|
||||
note : lib.getValue(ptr + 0, 'i8'),
|
||||
ins : lib.getValue(ptr + 1, 'i8'),
|
||||
vol : lib.getValue(ptr + 2, 'i8'),
|
||||
fxt : lib.getValue(ptr + 3, 'i8'),
|
||||
fxp : lib.getValue(ptr + 4, 'i8'),
|
||||
f2t : lib.getValue(ptr + 5, 'i8'),
|
||||
f2p : lib.getValue(ptr + 6, 'i8'),
|
||||
_flag: lib.getValue(ptr + 7, 'i8')
|
||||
};
|
||||
}
|
||||
|
||||
function readChannel(ptr) {
|
||||
return {
|
||||
pan : lib.getValue(ptr + 0, 'i32'),
|
||||
vol : lib.getValue(ptr + 4, 'i32'),
|
||||
flg : lib.getValue(ptr + 8, 'i32')
|
||||
};
|
||||
}
|
||||
|
||||
function readEnvelope(ptr) {
|
||||
if (!ptr) return null;
|
||||
const flg = lib.getValue(ptr + 0, 'i32');
|
||||
const npt = lib.getValue(ptr + 4, 'i32');
|
||||
const data = [];
|
||||
for (let i = 0; i < npt * 2 && i < XMP_MAX_ENV_POINTS * 2; i++) {
|
||||
data.push(lib.getValue(ptr + 28 + i * 2, 'i16'));
|
||||
}
|
||||
return {
|
||||
flg,
|
||||
npt,
|
||||
scl : lib.getValue(ptr + 8, 'i32'),
|
||||
sus : lib.getValue(ptr + 12, 'i32'),
|
||||
sue : lib.getValue(ptr + 16, 'i32'),
|
||||
lps : lib.getValue(ptr + 20, 'i32'),
|
||||
lpe : lib.getValue(ptr + 24, 'i32'),
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
function readSubinstrument(ptr) {
|
||||
if (!ptr) return null;
|
||||
return {
|
||||
vol : lib.getValue(ptr + 0, 'i32'),
|
||||
gvl : lib.getValue(ptr + 4, 'i32'),
|
||||
pan : lib.getValue(ptr + 8, 'i32'),
|
||||
xpo : lib.getValue(ptr + 12, 'i32'),
|
||||
fin : lib.getValue(ptr + 16, 'i32'),
|
||||
vwf : lib.getValue(ptr + 20, 'i32'),
|
||||
vde : lib.getValue(ptr + 24, 'i32'),
|
||||
vra : lib.getValue(ptr + 28, 'i32'),
|
||||
vsw : lib.getValue(ptr + 32, 'i32'),
|
||||
rvv : lib.getValue(ptr + 36, 'i32'),
|
||||
sid : lib.getValue(ptr + 40, 'i32'),
|
||||
nna : lib.getValue(ptr + 44, 'i32'),
|
||||
dct : lib.getValue(ptr + 48, 'i32'),
|
||||
dca : lib.getValue(ptr + 52, 'i32'),
|
||||
ifc : lib.getValue(ptr + 56, 'i32'),
|
||||
ifr : lib.getValue(ptr + 60, 'i32')
|
||||
};
|
||||
}
|
||||
|
||||
function readInstrument(basePtr, index) {
|
||||
const ptr = basePtr + index * 764;
|
||||
const name = readString(ptr, 32);
|
||||
const nsm = lib.getValue(ptr + 36, 'i32');
|
||||
const subPtr = lib.getValue(ptr + 752, 'i32');
|
||||
|
||||
const map = [];
|
||||
for (let k = 0; k < XMP_MAX_KEYS; k++) {
|
||||
const mptr = ptr + 512 + k * 2;
|
||||
map.push({
|
||||
ins : lib.getValue(mptr, 'i8'),
|
||||
xpo : lib.getValue(mptr + 1, 'i8')
|
||||
});
|
||||
}
|
||||
|
||||
const sub = [];
|
||||
if (nsm > 0 && nsm <= 255 && subPtr) {
|
||||
for (let s = 0; s < nsm; s++) {
|
||||
sub.push(readSubinstrument(subPtr + s * 64));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
vol : lib.getValue(ptr + 32, 'i32'),
|
||||
nsm,
|
||||
rls : lib.getValue(ptr + 40, 'i32'),
|
||||
aei : readEnvelope(ptr + 44),
|
||||
pei : readEnvelope(ptr + 200),
|
||||
fei : readEnvelope(ptr + 356),
|
||||
map,
|
||||
sub
|
||||
};
|
||||
}
|
||||
|
||||
function readSample(basePtr, index) {
|
||||
const ptr = basePtr + index * 52;
|
||||
return {
|
||||
name : readString(ptr, 32),
|
||||
len : lib.getValue(ptr + 32, 'i32'),
|
||||
lps : lib.getValue(ptr + 36, 'i32'),
|
||||
lpe : lib.getValue(ptr + 40, 'i32'),
|
||||
flg : lib.getValue(ptr + 44, 'i32'),
|
||||
data : lib.getValue(ptr + 48, 'i32')
|
||||
};
|
||||
}
|
||||
|
||||
function readTrack(ptr) {
|
||||
if (!ptr) return null;
|
||||
const rows = lib.getValue(ptr + 0, 'i32');
|
||||
const events = [];
|
||||
const eventPtr = ptr + 4;
|
||||
for (let r = 0; r < rows; r++) {
|
||||
events.push(readEvent(eventPtr + r * 8));
|
||||
}
|
||||
return { rows, events };
|
||||
}
|
||||
|
||||
function readPattern(ptr, chn) {
|
||||
if (!ptr) return null;
|
||||
const rows = lib.getValue(ptr + 0, 'i32');
|
||||
const index = [];
|
||||
for (let i = 0; i < chn; i++) {
|
||||
index.push(lib.getValue(ptr + 4 + i * 4, 'i32'));
|
||||
}
|
||||
return { rows, index };
|
||||
}
|
||||
|
||||
function get_module_info(context) {
|
||||
if (!lib || !context) return null;
|
||||
|
||||
const infoSize = 36;
|
||||
const infoPtr = lib._malloc(infoSize);
|
||||
lib._xmp_get_module_info(context, infoPtr);
|
||||
|
||||
const md5 = [];
|
||||
for (let i = 0; i < 16; i++) md5.push(lib.getValue(infoPtr + i, 'i8'));
|
||||
|
||||
const vol_base = lib.getValue(infoPtr + 16, 'i32');
|
||||
const mod_ptr = lib.getValue(infoPtr + 20, 'i32');
|
||||
const comment_ptr = lib.getValue(infoPtr + 24, 'i32');
|
||||
const num_sequences = lib.getValue(infoPtr + 28, 'i32');
|
||||
const seq_data_ptr = lib.getValue(infoPtr + 32, 'i32');
|
||||
|
||||
const comment = readString(comment_ptr);
|
||||
|
||||
const sequences = [];
|
||||
if (seq_data_ptr && num_sequences > 0) {
|
||||
for (let i = 0; i < num_sequences; i++) {
|
||||
const s = seq_data_ptr + i * 8;
|
||||
sequences.push({
|
||||
start: lib.getValue(s, 'i32'),
|
||||
length: lib.getValue(s + 4, 'i32')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mod = null;
|
||||
if (mod_ptr) {
|
||||
const name = readString(mod_ptr, XMP_NAME_SIZE);
|
||||
const type = readString(mod_ptr + XMP_NAME_SIZE, XMP_NAME_SIZE);
|
||||
|
||||
const base = mod_ptr + 2 * XMP_NAME_SIZE;
|
||||
const pat = lib.getValue(base + 0, 'i32');
|
||||
const trk = lib.getValue(base + 4, 'i32');
|
||||
const chn = lib.getValue(base + 8, 'i32');
|
||||
const ins = lib.getValue(base + 12, 'i32');
|
||||
const smp = lib.getValue(base + 16, 'i32');
|
||||
const spd = lib.getValue(base + 20, 'i32');
|
||||
const bpm = lib.getValue(base + 24, 'i32');
|
||||
const len = lib.getValue(base + 28, 'i32');
|
||||
const rst = lib.getValue(base + 32, 'i32');
|
||||
const gvl = lib.getValue(base + 36, 'i32');
|
||||
|
||||
const xxp = lib.getValue(base + 40, 'i32');
|
||||
const xxt = lib.getValue(base + 44, 'i32');
|
||||
const xxi = lib.getValue(base + 48, 'i32');
|
||||
const xxs = lib.getValue(base + 52, 'i32');
|
||||
const xxc = base + 56;
|
||||
const xxo = base + 56 + 64 * 12;
|
||||
|
||||
const patterns = [];
|
||||
if (xxp) {
|
||||
for (let i = 0; i < pat; i++) {
|
||||
const p = lib.getValue(xxp + i * 4, 'i32');
|
||||
patterns.push(readPattern(p, chn));
|
||||
}
|
||||
}
|
||||
|
||||
const tracks = [];
|
||||
if (xxt) {
|
||||
for (let i = 0; i < trk; i++) {
|
||||
const t = lib.getValue(xxt + i * 4, 'i32');
|
||||
tracks.push(readTrack(t));
|
||||
}
|
||||
}
|
||||
|
||||
const instruments = [];
|
||||
if (xxi && ins > 0) {
|
||||
for (let i = 0; i < ins; i++) {
|
||||
instruments.push(readInstrument(xxi, i));
|
||||
}
|
||||
}
|
||||
|
||||
const samples = [];
|
||||
if (xxs && smp > 0) {
|
||||
for (let i = 0; i < smp; i++) {
|
||||
samples.push(readSample(xxs, i));
|
||||
}
|
||||
}
|
||||
|
||||
const channels = [];
|
||||
for (let i = 0; i < chn; i++) {
|
||||
channels.push(readChannel(xxc + i * 16));
|
||||
}
|
||||
|
||||
const order = [];
|
||||
for (let i = 0; i < XMP_MAX_MOD_LENGTH && i < len; i++) {
|
||||
order.push(lib.getValue(xxo + i, 'i8') & 0xFF);
|
||||
}
|
||||
|
||||
mod = {
|
||||
name, type, pat, trk, chn, ins, smp, spd, bpm, len, rst, gvl,
|
||||
order, channels, patterns, tracks, instruments, samples
|
||||
};
|
||||
}
|
||||
|
||||
lib._free(infoPtr);
|
||||
return { md5, vol_base, mod, comment, num_sequences, sequences };
|
||||
}
|
||||
|
||||
function get_frame_info(context) {
|
||||
if (!lib || !context) return null;
|
||||
|
||||
const infoSize = 2048; // generous
|
||||
const ptr = lib._malloc(infoSize);
|
||||
lib._xmp_get_frame_info(context, ptr);
|
||||
|
||||
const info = {
|
||||
pos: lib.getValue(ptr + 0, 'i32'),
|
||||
pattern: lib.getValue(ptr + 4, 'i32'),
|
||||
row: lib.getValue(ptr + 8, 'i32'),
|
||||
num_rows: lib.getValue(ptr + 12, 'i32'),
|
||||
frame: lib.getValue(ptr + 16, 'i32'),
|
||||
speed: lib.getValue(ptr + 20, 'i32'),
|
||||
bpm: lib.getValue(ptr + 24, 'i32'),
|
||||
time: lib.getValue(ptr + 28, 'i32'),
|
||||
total_time: lib.getValue(ptr + 32, 'i32'),
|
||||
frame_time: lib.getValue(ptr + 36, 'i32'),
|
||||
buffer: lib.getValue(ptr + 40, 'i32'),
|
||||
buffer_size: lib.getValue(ptr + 44, 'i32'),
|
||||
total_size: lib.getValue(ptr + 48, 'i32'),
|
||||
volume: lib.getValue(ptr + 52, 'i32'),
|
||||
loop_count: lib.getValue(ptr + 56, 'i32'),
|
||||
virt_channels: lib.getValue(ptr + 60, 'i32'),
|
||||
virt_used: lib.getValue(ptr + 64, 'i32'),
|
||||
sequence: lib.getValue(ptr + 68, 'i32')
|
||||
};
|
||||
|
||||
const chanInfoPtr = ptr + 72;
|
||||
const chanSize = 24;
|
||||
info.channel_info = [];
|
||||
|
||||
for (let i = 0; i < XMP_MAX_CHANNELS; i++) {
|
||||
const c = chanInfoPtr + i * chanSize;
|
||||
info.channel_info.push({
|
||||
period: lib.getValue(c + 0, 'i32'),
|
||||
position: lib.getValue(c + 4, 'i32'),
|
||||
pitchbend: lib.getValue(c + 8, 'i16'),
|
||||
note: lib.getValue(c + 10, 'i8'),
|
||||
instrument: lib.getValue(c + 11, 'i8'),
|
||||
sample: lib.getValue(c + 12, 'i8'),
|
||||
volume: lib.getValue(c + 13, 'i8'),
|
||||
pan: lib.getValue(c + 14, 'i8'),
|
||||
reserved: lib.getValue(c + 15, 'i8'),
|
||||
event: readEvent(c + 16)
|
||||
});
|
||||
}
|
||||
|
||||
lib._free(ptr);
|
||||
return info;
|
||||
}
|
||||
|
||||
async function loadFromUrl(url) {
|
||||
const res = await fetch(url);
|
||||
console.log(res);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
filename = res.headers.get('content-disposition');
|
||||
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]) {
|
||||
filename = matches[1].replace(/['"]/g, '');
|
||||
}
|
||||
} else {
|
||||
filename = res.url.substring(res.url.lastIndexOf('/')+1)
|
||||
if (filename.indexOf('?') !== -1) {
|
||||
filename.substring(0, filename.lastIndexOf('?'));
|
||||
}
|
||||
}
|
||||
const buf = await res.arrayBuffer();
|
||||
loadModule(new Uint8Array(buf));
|
||||
}
|
||||
|
||||
async function loadModule(u8buf) {
|
||||
try {
|
||||
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);
|
||||
if (ret !== 0) throw new Error(`Load failed: ${ret}`);
|
||||
|
||||
const blob = new Blob([u8buf], { type: 'application/octet-stream' });
|
||||
buttonDownload.onclick = () => {
|
||||
const link = document.createElement('a');
|
||||
link.href = window.URL.createObjectURL(blob);
|
||||
link.download = filename;
|
||||
link.click();
|
||||
};
|
||||
buttonDownload.disabled = false;
|
||||
|
||||
xmi = get_module_info(ctx);
|
||||
efx_map = getEffectsMap();
|
||||
|
||||
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');
|
||||
if (mode == 'tracker') {
|
||||
preloadOrder();
|
||||
|
||||
loadInstruments();
|
||||
loadSamples();
|
||||
|
||||
// load first pattern
|
||||
let p = xmi.mod.order[0];
|
||||
uiNewPattern = createPatternTable(p);
|
||||
patternTables[p] = uiNewPattern;
|
||||
uiPatterns.appendChild(uiNewPattern);
|
||||
}
|
||||
console.log('Module loaded successfully');
|
||||
document.getElementById('play').disabled = false;
|
||||
dispatch('moduleReady');
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Load error:', err);
|
||||
alert('An error occured attempting to load the module.');
|
||||
}
|
||||
}
|
||||
|
||||
function getEffectsMap() {
|
||||
const type = xmi.mod.type;
|
||||
console.log(type);
|
||||
|
||||
for (const entry of EFFECTS_MAP.entries()) {
|
||||
if (type.includes(entry[0])) {
|
||||
return entry[1];
|
||||
}
|
||||
}
|
||||
return EFFECTS_MAP.get('Protracker');
|
||||
}
|
||||
|
||||
async function preloadOrder() {
|
||||
for (let o = 0; o < xmi.mod.len; o++) {
|
||||
let index = xmi.mod.order[o];
|
||||
if (index == 0xFF) continue;
|
||||
const indexBox = document.createElement('div');
|
||||
indexBox.textContent = toHex(index);
|
||||
indexBox.width = indexBox.height = '24px';
|
||||
uiOrder.appendChild(indexBox);
|
||||
orderCells.push(indexBox);
|
||||
}
|
||||
}
|
||||
|
||||
function loadInstruments() {
|
||||
for (let i = 0; i < xmi.mod.ins; i++) {
|
||||
const row = uiInstruments.insertRow();
|
||||
|
||||
const index = row.insertCell();
|
||||
index.width = 16;
|
||||
index.textContent = toHex(i);
|
||||
|
||||
const name = row.insertCell();
|
||||
name.textContent = xmi.mod.instruments[i].name;
|
||||
|
||||
instrumentRows.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
function loadSamples() {
|
||||
for (let s = 0; s < xmi.mod.smp; s++) {
|
||||
const row = uiSamples.insertRow();
|
||||
|
||||
const index = row.insertCell();
|
||||
index.width = 16;
|
||||
index.textContent = toHex(s);
|
||||
|
||||
const name = row.insertCell();
|
||||
name.textContent = xmi.mod.samples[s].name;
|
||||
|
||||
sampleRows.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
function createPatternTable(p) {
|
||||
const chn = xmi.mod.chn;
|
||||
const pattern = xmi.mod.patterns[p];
|
||||
if (!pattern) return null;
|
||||
|
||||
let html = `<table class="pattern" id="p${p}">`;
|
||||
|
||||
// Header
|
||||
html += '<tr><th></th>';
|
||||
for (let c = 0; c < chn; c++) {
|
||||
html += `<th class="channel">Channel ${c+1}<div class="visualizer"><meter class="left" max="64"></meter><meter class="right" max="64"></meter></div></th>`;
|
||||
}
|
||||
html += '</tr>';
|
||||
|
||||
// Top gap
|
||||
html += '<tr class="gap"><td></td>';
|
||||
for (let c = 0; c < chn; c++) html += '<td></td>';
|
||||
html += '</tr>';
|
||||
|
||||
// Rows
|
||||
for (let r = 0; r < pattern.rows; r++) {
|
||||
html += `<tr id="r${r}"><td id="no">${toHex(r)}</td>`;
|
||||
for (let c = 0; c < chn; c++) {
|
||||
const track = pattern.index[c];
|
||||
const event = xmi.mod.tracks[track].events[r];
|
||||
|
||||
let noteText = '...';
|
||||
if (event.note < 0) noteText = '===';
|
||||
else if (event.note > 0) noteText = NOTES[event.note % 12] + Math.floor(event.note / 12);
|
||||
|
||||
let insText = event.ins > 0 ? toHex(event.ins) : '..';
|
||||
|
||||
let volText = '..';
|
||||
if (event.vol != 0) volText = toHex(event.vol - 1);
|
||||
else if (event.note > 0) volText = '40';
|
||||
|
||||
const fxp = event.fxp & 0xFF;
|
||||
let efxType = '.';
|
||||
let efxParam = '..';
|
||||
if (efx_map.has(event.fxt) && fxp != 0) {
|
||||
efxType = efx_map.get(event.fxt);
|
||||
efxParam = toHex(fxp);
|
||||
}
|
||||
|
||||
html += `<td><a class="note">${noteText}</a> <a class="instrument">${insText}</a> <a class="volume">${volText}</a> `+
|
||||
`<a class="effect_type">${efxType}</a><a class="effect_param">${efxParam}</a></td>`;
|
||||
}
|
||||
html += '</tr>';
|
||||
}
|
||||
|
||||
// Bottom gap
|
||||
html += '<tr class="gap"><td></td>';
|
||||
for (let c = 0; c < chn; c++) html += '<td></td>';
|
||||
html += '</tr></table>';
|
||||
|
||||
const container = document.createElement('div'); // Temporary container to parse HTML
|
||||
container.innerHTML = html;
|
||||
const patternTable = container.firstChild;
|
||||
|
||||
// Extract meters
|
||||
patternMeters[p] = [];
|
||||
const headerRow = patternTable.rows[0];
|
||||
for (let c = 0; c < chn; c++) {
|
||||
const vis = headerRow.cells[c + 1].querySelector('.visualizer');
|
||||
patternMeters[p][c] = {
|
||||
l: vis.querySelector('.left'),
|
||||
r: vis.querySelector('.right')
|
||||
};
|
||||
}
|
||||
|
||||
// Extract rows
|
||||
patternRows[p] = [];
|
||||
for (let r = 0; r < pattern.rows; r++) {
|
||||
patternRows[p][r] = patternTable.rows[r + 2];
|
||||
}
|
||||
|
||||
return patternTable;
|
||||
}
|
||||
|
||||
async function play() {
|
||||
if (playing && !paused) return;
|
||||
let initalized = (audio != null && node != null);
|
||||
|
||||
if (!initalized) {
|
||||
console.log('Starting playback...');
|
||||
audio = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: SR });
|
||||
var soundBuffer = lib._malloc(BYTES+1);
|
||||
await audio.resume();
|
||||
console.log('AudioContext ready');
|
||||
|
||||
node = audio.createScriptProcessor(BUF, 0, 2);
|
||||
console.log('ScriptProcessorNode created');
|
||||
|
||||
node.onaudioprocess = (e) => {
|
||||
const leftChannel = e.outputBuffer.getChannelData(0);
|
||||
const rightChannel = e.outputBuffer.getChannelData(1);
|
||||
|
||||
if (!paused && lib._xmp_play_buffer(ctx, soundBuffer, BYTES, 0) == 0) {
|
||||
const heap = lib.HEAP16;
|
||||
const base = soundBuffer >> 1;
|
||||
for (let i = 0; i < BUF; i++) {
|
||||
leftChannel[i] = heap[base + i*2] / 32768;
|
||||
rightChannel[i] = heap[base + i*2 + 1] / 32768;
|
||||
}
|
||||
|
||||
const xfi = get_frame_info(ctx);
|
||||
if (xfi) {
|
||||
if (xfi.pos !== lastPos) {
|
||||
dispatch('patternChanged', {xfi});
|
||||
lastPattern = xfi.pattern;
|
||||
lastPos = xfi.pos;
|
||||
lastRow = -1;
|
||||
}
|
||||
if (xfi.row !== lastRow) {
|
||||
dispatch('rowChanged', {xfi});
|
||||
lastRow = xfi.row;
|
||||
}
|
||||
if (xfi.speed !== lastSpeed) {
|
||||
dispatch('speedChanged', {xfi});
|
||||
lastSpeed = xfi.speed;
|
||||
}
|
||||
if (xfi.bpm !== lastBpm) {
|
||||
dispatch('bpmChanged', {xfi});
|
||||
lastBpm = xfi.bpm;
|
||||
}
|
||||
|
||||
// set time
|
||||
curTimeText.textContent = (Math.floor(((xfi.time / 1000) / 60) % 60)).toString().padStart(2, '0') + ':' +
|
||||
(Math.floor((xfi.time / 1000) % 60)).toString().padStart(2, '0');
|
||||
|
||||
for (var c = 0; c < xmi.mod.chn; c++) {
|
||||
const chan = xfi.channel_info[c];
|
||||
const chanVol = chan.volume & 0xFF;
|
||||
const chanPan = chan.pan & 0xFF;
|
||||
const leftVol = Math.floor(chanVol * (256 - chanPan) / 256);
|
||||
const rightVol = Math.floor(chanVol * chanPan / 256);
|
||||
|
||||
if (active_meters[c].l) active_meters[c].l.value = leftVol;
|
||||
if (active_meters[c].r) active_meters[c].r.value = rightVol;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
node.connect(audio.destination);
|
||||
lib._xmp_start_player(ctx, SR, 0);
|
||||
lib._xmp_set_player(ctx, 7, volume);
|
||||
lib._xmp_set_player(ctx, 1, stereo);
|
||||
}
|
||||
|
||||
paused = false;
|
||||
playing = true;
|
||||
buttonPlay.disabled = true;
|
||||
buttonPause.disabled = buttonStop.disabled = false;
|
||||
console.log('Playback started');
|
||||
}
|
||||
|
||||
function pause() {
|
||||
paused = true;
|
||||
buttonPlay.disabled = false;
|
||||
buttonPause.disabled = buttonStop.disabled = true;
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (!playing) return;
|
||||
console.log('Stopping playback');
|
||||
lib._xmp_end_player(ctx);
|
||||
if (node) node.disconnect();
|
||||
audio = null;
|
||||
node = null;
|
||||
playing = false;
|
||||
lastPattern = lastRow = lastPos = -1;
|
||||
buttonPlay.disabled = buttonPause.disabled = false;
|
||||
buttonStop.disabled = true;
|
||||
}
|
||||
|
||||
function changeVolume() {
|
||||
volumeText.textContent = volumeBar.value+'%';
|
||||
volume = volumeBar.value;
|
||||
lib._xmp_set_player(ctx, 7, volume);
|
||||
}
|
||||
|
||||
function changeStereoSeparation() {
|
||||
stereoText.textContent = stereoBar.value+'%';
|
||||
stereo = stereoBar.value;
|
||||
lib._xmp_set_player(ctx, 1, stereo);
|
||||
}
|
||||
|
||||
buttonPlay.onclick = play;
|
||||
buttonPause.onclick = pause;
|
||||
buttonStop.onclick = stop;
|
||||
volumeBar.oninput = changeVolume;
|
||||
stereoBar.oninput = changeStereoSeparation;
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (ctx) lib._xmp_free_context(ctx);
|
||||
});
|
||||
|
||||
window.addEventListener('patternChanged', e => {
|
||||
if (mode != 'tracker') return;
|
||||
const xfi = e.detail.xfi;
|
||||
|
||||
if (lastPos >= 0) orderCells[lastPos].classList.remove('active');
|
||||
orderCells[xfi.pos].className = 'active';
|
||||
orderCells[xfi.pos].scrollIntoView();
|
||||
|
||||
if (xfi.pattern == lastPattern) return;
|
||||
const p = xfi.pattern;
|
||||
|
||||
if (uiCurrentPattern != null) {
|
||||
uiCurrentPattern.style.display = 'none';
|
||||
}
|
||||
|
||||
let uiNewPattern = patternTables[p];
|
||||
if (uiNewPattern == null) {
|
||||
uiNewPattern = createPatternTable(p);
|
||||
patternTables[p] = uiNewPattern;
|
||||
uiPatterns.appendChild(uiNewPattern);
|
||||
}
|
||||
|
||||
uiNewPattern.style.display = '';
|
||||
uiCurrentPattern = uiNewPattern;
|
||||
|
||||
if (rowHeight === 0) {
|
||||
headerHeight = uiNewPattern.rows[0].clientHeight;
|
||||
gapHeight = uiNewPattern.rows[1].clientHeight;
|
||||
rowHeight = uiNewPattern.rows[2].clientHeight;
|
||||
}
|
||||
|
||||
for (let c = 0; c < xmi.mod.chn; c++) {
|
||||
active_meters[c] = patternMeters[p][c];
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('rowChanged', e => {
|
||||
if (mode != 'tracker') return;
|
||||
const xfi = e.detail.xfi;
|
||||
|
||||
if (uiCurrentRow != null) uiCurrentRow.classList.remove('playhead');
|
||||
|
||||
const patternIndex = lastPattern;
|
||||
let uiNewRow = patternRows[patternIndex] ? patternRows[patternIndex][xfi.row] : null;
|
||||
if (uiNewRow != null) {
|
||||
uiNewRow.classList.add('playhead');
|
||||
uiCurrentRow = uiNewRow;
|
||||
|
||||
if (rowHeight > 0) {
|
||||
const container = uiPatterns;
|
||||
const rowPosition = headerHeight + gapHeight + xfi.row * rowHeight;
|
||||
const targetScroll = rowPosition + rowHeight / 2 - container.clientHeight / 2;
|
||||
container.scrollTop = Math.max(0, targetScroll);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('speedChanged', e => {
|
||||
speedText.textContent = e.detail.xfi.speed;
|
||||
});
|
||||
|
||||
window.addEventListener('bpmChanged', e => {
|
||||
bpmText.textContent = e.detail.xfi.bpm;
|
||||
});
|
||||
|
||||
HTMLTableRowElement.prototype.insertHeadCell = function() {
|
||||
var head = document.createElement('th');
|
||||
this.append(head);
|
||||
return head;
|
||||
};
|
||||
|
||||
function toHex(s) {
|
||||
return s.toString(16).toUpperCase().padStart(2, '0');
|
||||
};
|
||||
Reference in New Issue
Block a user