mindX / studio.js
Gregory-L's picture
mindX Space — rebuilt from mindx/godel/mindxtrain/hf/space_static
d5c1d05 verified
Raw History Blame Contribute Delete
112 kB
// mindX studio — the playdocs bench on a static Space.
//
// One <audio> element plays the document (its parts, one after another, as one timeline) or the
// edited take (encoded to WAV in this tab). It runs through one Web Audio graph:
//
// <audio> → EQ low · mid · high → low-pass → dry ┬→ sum → volume (GainNode, to 400 %) → balance → analyser → speakers
// delay (wet) ──┤
// reverb (wet) ─┘ (the reverb's impulse response is SYNTHESISED noise, said so)
//
// Everything the scope, spectrum and spectrogram draw is read from that analyser, after the gain
// stage. The senses (.pixel .gesture .mic .cam) are measured here too, dispatched on window as
// 'dv:pixel' 'dv:gesture' 'dv:mic' 'dv:cam' (detail = the feature object), and any knob can be
// mapped to any feature. The waveform editor ports DeltaVerse's wave-editor verbs (DVWaveEditor.ops)
// onto an AudioBuffer copy. No network code beyond reading this Space's own files.
//
// The dreamknob rack (studio_rack.js) owns no state: it reads mindxStudio.knobs() and writes
// mindxStudio.setKnob(), and repaints on the 'mindx:studio' event. Without it, plain sliders appear.
(function () {
'use strict';
var VERSION = '1.0.0';
var LS_KEY = 'mindx.studio.v1';
var EDIT_SR = 24000; // the editor decodes at 24 kHz: a voice, sample-accurate, a quarter of 48 kHz's memory
var OVERVIEW_SR = 8000; // the overview only needs peaks
var OV_RATE = 20; // overview peak buckets per second
var SAVANTE_CREDIT = 'SAVANTE — voice of mindX and DeltaVerse, evolved by survival of the fittest from NEURAL · JAIMLA · LEADER · OVERLORD';
// ---------------------------------------------------------------- helpers
function $(id) { return document.getElementById(id); }
function el(tag, cls, text) { var e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = String(text); return e; }
function clamp(v, a, b) { return v < a ? a : v > b ? b : v; }
function fmtT(s, dp) {
s = Math.max(0, +s || 0); var h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), r = s - h * 3600 - m * 60;
var sec = dp ? r.toFixed(dp) : String(Math.floor(r)); if (r < 10) sec = '0' + sec;
return (h ? h + ':' + (m < 10 ? '0' : '') : '') + m + ':' + sec;
}
function css(name) { return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || '#888'; }
function emit(name, detail) { try { window.dispatchEvent(new CustomEvent(name, { detail: detail })); } catch (e) {} }
function store(read, val) {
try {
if (read) { var s = localStorage.getItem(LS_KEY); return s ? JSON.parse(s) : {}; }
localStorage.setItem(LS_KEY, JSON.stringify(val));
} catch (e) { return read ? {} : undefined; }
}
var saved = store(true) || {};
var saveTimer = 0;
function save() {
clearTimeout(saveTimer);
saveTimer = setTimeout(function () { store(false, { values: K.values, maps: K.maps, theme: saved.theme || null, pitchLock: $('pitch-lock').checked }); }, 300);
}
function sizeCanvas(c) {
if (!c.dataset.h) { c.dataset.h = c.getAttribute('height') || '120'; c.style.height = c.dataset.h + 'px'; }
var ch = +c.dataset.h, dpr = Math.min(window.devicePixelRatio || 1, 2), w = Math.max(1, Math.round(c.clientWidth * dpr)), h = Math.max(1, Math.round(ch * dpr));
if (c.width !== w || c.height !== h) { c.width = w; c.height = h; return true; }
return false;
}
// ---------------------------------------------------------------- theme
var COL = {};
function readColors() {
['--bg', '--canvas', '--grid', '--text', '--text2', '--text3', '--gold', '--green', '--amber', '--red', '--scope', '--spec', '--sel', '--wave', '--cursor', '--marker', '--start', '--line2']
.forEach(function (k) { COL[k.slice(2)] = css(k); });
buildPalette();
emit('mindx:studio-theme', COL);
}
function applyTheme(t) {
if (t) document.documentElement.setAttribute('data-theme', t); else document.documentElement.removeAttribute('data-theme');
readColors(); drawOverview(); edDraw();
}
$('theme-btn').addEventListener('click', function () {
var dark = (document.documentElement.getAttribute('data-theme') || (matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark')) === 'dark';
saved.theme = dark ? 'light' : 'dark'; applyTheme(saved.theme); save();
});
try { matchMedia('(prefers-color-scheme: light)').addEventListener('change', function () { if (!saved.theme) applyTheme(null); }); } catch (e) {}
// ---------------------------------------------------------------- the index (both shapes)
// old: {DOC: {parts:[{file,seconds}], voice, seconds, start?}}
// new: {DOC: {voices: {id: {label, parts, start?, recipe?, fitness?…}}, default_voice, start?}}
var DOCS = {}, DOC_ORDER = [];
function normDoc(name, d) {
if (!d || typeof d !== 'object') return null;
var voices = {}, order = [];
function addVoice(id, v, fallbackLabel) {
var parts = (v.parts || []).filter(function (p) { return p && p.file; }).map(function (p) { return { file: p.file, seconds: +p.seconds || 0, words: p.words, bytes: p.bytes }; });
if (!parts.length) return;
var start = v.start || d.start || null;
if (start && d.start && start !== d.start) start = Object.assign({}, d.start, start); // the doc's anchor text, the voice's seconds
var secs = parts.reduce(function (a, p) { return a + p.seconds; }, 0);
voices[id] = { id: id, label: v.label || fallbackLabel || id, parts: parts, seconds: secs || +v.seconds || 0, start: start, raw: v };
order.push(id);
}
if (d.voices && typeof d.voices === 'object') {
Object.keys(d.voices).forEach(function (id) { addVoice(id, d.voices[id], id.toUpperCase()); });
} else if (d.parts) {
var label = String(d.voice || 'voice').replace(/^piper:/, 'piper · ');
addVoice(String(d.voice || 'default').replace(/[^\w-]+/g, '_'), d, label);
}
if (!order.length) return null;
var dv = d.default_voice && voices[d.default_voice] ? d.default_voice : order[0];
return { name: name, voices: voices, order: order, defaultVoice: dv, raw: d, listen_url: d.listen_url };
}
// ---------------------------------------------------------------- audio graph
var au = $('au');
var A = null; // {ctx, src, nodes…, an}
function makeImpulse(ctx, secs, decay) {
// SYNTHESISED: exponentially decaying noise, two channels. Not a measured room.
var n = Math.floor(ctx.sampleRate * secs), b = ctx.createBuffer(2, n, ctx.sampleRate);
for (var c = 0; c < 2; c++) { var d = b.getChannelData(c); for (var i = 0; i < n; i++) d[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / n, decay); }
return b;
}
function ensureCtx() {
if (A) { if (A.ctx.state === 'suspended') A.ctx.resume().catch(function () {}); return A; }
var AC = window.AudioContext || window.webkitAudioContext;
if (!AC) { $('out-note').textContent = 'this browser has no Web Audio: the knobs past volume 100 % and the visuals cannot work'; return null; }
var ctx = new AC();
var a = { ctx: ctx };
try { a.src = ctx.createMediaElementSource(au); } catch (e) { a.src = null; }
a.low = ctx.createBiquadFilter(); a.low.type = 'lowshelf'; a.low.frequency.value = 250;
a.mid = ctx.createBiquadFilter(); a.mid.type = 'peaking'; a.mid.frequency.value = 1500; a.mid.Q.value = 0.9;
a.high = ctx.createBiquadFilter(); a.high.type = 'highshelf'; a.high.frequency.value = 4500;
a.lp = ctx.createBiquadFilter(); a.lp.type = 'lowpass'; a.lp.Q.value = 0.707;
a.sum = ctx.createGain();
a.delay = ctx.createDelay(2); a.fb = ctx.createGain(); a.fb.gain.value = 0.35; a.delayWet = ctx.createGain(); a.delayWet.gain.value = 0;
a.conv = ctx.createConvolver(); a.conv.buffer = makeImpulse(ctx, 2.4, 2.6); a.revWet = ctx.createGain(); a.revWet.gain.value = 0;
a.vol = ctx.createGain();
a.pan = ctx.createStereoPanner ? ctx.createStereoPanner() : null;
a.an = ctx.createAnalyser(); a.an.fftSize = 4096; a.an.smoothingTimeConstant = 0.6;
if (a.src) a.src.connect(a.low);
a.low.connect(a.mid); a.mid.connect(a.high); a.high.connect(a.lp);
a.lp.connect(a.sum);
a.lp.connect(a.delay); a.delay.connect(a.fb); a.fb.connect(a.delay); a.delay.connect(a.delayWet); a.delayWet.connect(a.sum);
a.lp.connect(a.conv); a.conv.connect(a.revWet); a.revWet.connect(a.sum);
a.sum.connect(a.vol);
if (a.pan) { a.vol.connect(a.pan); a.pan.connect(a.an); } else a.vol.connect(a.an);
a.an.connect(ctx.destination);
a.f32 = new Float32Array(a.an.fftSize); a.fq = new Float32Array(a.an.frequencyBinCount);
A = a;
KNOBS.forEach(function (k) { applyKnob(k.id); });
return a;
}
function setP(param, v) { if (!A) return; try { param.setTargetAtTime(v, A.ctx.currentTime, 0.015); } catch (e) { param.value = v; } }
// ---------------------------------------------------------------- knobs
var TAPER = {
lin: { toN: function (v, a, b) { return (v - a) / (b - a); }, fromN: function (n, a, b) { return a + n * (b - a); } },
log: { toN: function (v, a, b) { return Math.log(v / a) / Math.log(b / a); }, fromN: function (n, a, b) { return a * Math.pow(b / a, n); } },
pow2: { toN: function (v, a, b) { return Math.sqrt(Math.max(0, (v - a) / (b - a))); }, fromN: function (n, a, b) { return a + n * n * (b - a); } }
};
function pct(v) { return Math.round(v * 100) + '%'; }
function db(v) { return (v > 0 ? '+' : '') + v.toFixed(1) + ' dB'; }
var KNOBS = [
{ id: 'vol', label: 'VOLUME', sec: 'OUTPUT', kind: 'fader', min: 0, max: 4, def: 1, step: 0.01, taper: 'pow2', fmt: pct, detents: [1] },
{ id: 'bal', label: 'BALANCE', sec: 'OUTPUT', kind: 'pan', min: -1, max: 1, def: 0, step: 0.01, taper: 'lin', origin: 0, fmt: function (v) { return Math.abs(v) < 0.005 ? 'C' : (v < 0 ? 'L' : 'R') + Math.round(Math.abs(v) * 100); } },
{ id: 'speed', label: 'SPEED', sec: 'OUTPUT', min: 0.5, max: 2, def: 1, step: 0.01, taper: 'log', fmt: function (v) { return v.toFixed(2) + '×'; }, detents: [1] },
{ id: 'low', label: 'LOW', sec: 'EQ', min: -18, max: 18, def: 0, step: 0.5, taper: 'lin', origin: 0, fmt: db, sub: '250 Hz shelf' },
{ id: 'mid', label: 'MID', sec: 'EQ', min: -18, max: 18, def: 0, step: 0.5, taper: 'lin', origin: 0, fmt: db, sub: '1.5 kHz peak' },
{ id: 'high', label: 'HIGH', sec: 'EQ', min: -18, max: 18, def: 0, step: 0.5, taper: 'lin', origin: 0, fmt: db, sub: '4.5 kHz shelf' },
{ id: 'cutoff', label: 'FILTER', sec: 'FX', min: 200, max: 20000, def: 20000, step: 0, taper: 'log', fmt: function (v) { return v >= 1000 ? (v / 1000).toFixed(1) + ' kHz' : Math.round(v) + ' Hz'; }, sub: 'low-pass' },
{ id: 'delay', label: 'DELAY', sec: 'FX', min: 0, max: 1, def: 0, step: 0.01, taper: 'lin', fmt: pct, sub: 'wet' },
{ id: 'dtime', label: 'TIME', sec: 'FX', min: 0.05, max: 1, def: 0.3, step: 0.01, taper: 'log', fmt: function (v) { return Math.round(v * 1000) + ' ms'; }, sub: 'delay' },
{ id: 'reverb', label: 'REVERB', sec: 'FX', min: 0, max: 1, def: 0, step: 0.01, taper: 'lin', fmt: pct, sub: 'synth IR' },
{ id: 'szoom', label: 'ZOOM', sec: 'SCOPE', min: 1, max: 16, def: 2, step: 0, taper: 'log', fmt: function (v) { return v.toFixed(1) + '×'; }, sub: 'scope time' },
{ id: 'sgain', label: 'GAIN', sec: 'SCOPE', min: 0.5, max: 8, def: 1, step: 0, taper: 'log', fmt: function (v) { return v.toFixed(1) + '×'; }, sub: 'scope height' },
{ id: 'contrast', label: 'CONTRAST', sec: 'SCOPE', min: 0.4, max: 4, def: 1.4, step: 0, taper: 'log', fmt: function (v) { return v.toFixed(2); }, sub: 'spectrogram' }
];
var KBY = {}; KNOBS.forEach(function (k) { KBY[k.id] = k; });
var K = { values: {}, maps: {}, smooth: {}, live: {} };
KNOBS.forEach(function (k) {
var v = saved.values && typeof saved.values[k.id] === 'number' ? saved.values[k.id] : k.def;
K.values[k.id] = clamp(v, k.min, k.max);
var m = saved.maps && saved.maps[k.id];
K.maps[k.id] = m && typeof m === 'object' ? { src: String(m.src || ''), lo: clamp(+m.lo || 0, 0, 1), hi: clamp(m.hi == null ? 1 : +m.hi, 0, 1), inv: !!m.inv, smooth: clamp(m.smooth == null ? 0.8 : +m.smooth, 0, 0.99) }
: { src: '', lo: 0, hi: 1, inv: false, smooth: 0.8 };
});
function applyKnob(id) {
var v = K.values[id];
if (id === 'speed') { try { au.playbackRate = v; } catch (e) {} return; }
if (!A) return;
switch (id) {
case 'vol': setP(A.vol.gain, v); break;
case 'bal': if (A.pan) setP(A.pan.pan, v); break;
case 'low': setP(A.low.gain, v); break;
case 'mid': setP(A.mid.gain, v); break;
case 'high': setP(A.high.gain, v); break;
case 'cutoff': setP(A.lp.frequency, Math.min(v, A.ctx.sampleRate / 2 - 1)); break;
case 'delay': setP(A.delayWet.gain, v * 0.8); break;
case 'dtime': setP(A.delay.delayTime, v); break;
case 'reverb': setP(A.revWet.gain, v * 1.2); break;
}
}
var notifyQueued = false;
function notify() {
if (notifyQueued) return; notifyQueued = true;
requestAnimationFrame(function () { notifyQueued = false; emit('mindx:studio', null); paintFallback(); });
}
function setKnob(id, v, why) {
var k = KBY[id]; if (!k || typeof v !== 'number' || !isFinite(v)) return;
v = clamp(v, k.min, k.max);
if (why !== 'map') K.smooth[id] = null;
if (K.values[id] === v) return;
K.values[id] = v; applyKnob(id); notify();
if (why !== 'map') save();
}
function knobN(id) { var k = KBY[id]; return clamp(TAPER[k.taper].toN(K.values[id], k.min, k.max), 0, 1); }
// ---------------------------------------------------------------- senses (measured here, dispatched as dv:*)
var S = {
pixel: { active: false, x: 0, y: 0, speed: 0, pressure: 0, luma: 0, target: '', t: 0, _px: null, _canvas: null, _dirty: false },
gesture: { any: false, pinch: 0.5, rotate: 0.5, swipeX: 0.5, swipeY: 0.5, last: null },
mic: { on: false, level: 0, peak: 0, pitch: 0, voiced: false, centroid: 0, onset: 0, speaking: false },
cam: { on: false, brightness: 0, motion: 0, hue: 0, saturation: 0, cx: 0.5, cy: 0.5 }
};
function lognorm(v, a, b) { return v > 0 ? clamp(Math.log(v / a) / Math.log(b / a), 0, 1) : 0; }
var FEATURES = [
{ id: 'pixel.x', src: 'pixel', label: 'x', get: function () { return S.pixel.x; }, show: function (v) { return v.toFixed(2); } },
{ id: 'pixel.y', src: 'pixel', label: 'y', get: function () { return S.pixel.y; }, show: function (v) { return v.toFixed(2); } },
{ id: 'pixel.speed', src: 'pixel', label: 'speed', get: function () { return clamp(S.pixel.speed / 3, 0, 1); }, show: function () { return S.pixel.speed.toFixed(2) + ' px/ms'; } },
{ id: 'pixel.pressure', src: 'pixel', label: 'pressure', get: function () { return S.pixel.pressure; }, show: function (v) { return v.toFixed(2); } },
{ id: 'pixel.luma', src: 'pixel', label: 'luminance', get: function () { return S.pixel.luma; }, show: function (v) { return v.toFixed(2); } },
{ id: 'gesture.pinch', src: 'gesture', label: 'pinch', get: function () { return S.gesture.pinch; }, show: function (v) { return v.toFixed(2); } },
{ id: 'gesture.rotate', src: 'gesture', label: 'rotate', get: function () { return S.gesture.rotate; }, show: function (v) { return Math.round((v - 0.5) * 360) + '°'; } },
{ id: 'gesture.swipeX', src: 'gesture', label: 'swipe ↔', get: function () { return S.gesture.swipeX; }, show: function (v) { return v.toFixed(1); } },
{ id: 'gesture.swipeY', src: 'gesture', label: 'swipe ↕', get: function () { return S.gesture.swipeY; }, show: function (v) { return v.toFixed(1); } },
{ id: 'mic.level', src: 'mic', label: 'level', get: function () { return clamp(S.mic.level * 5, 0, 1); }, show: function () { var d = S.mic.level > 0 ? 20 * Math.log10(S.mic.level) : -Infinity; return isFinite(d) ? d.toFixed(0) + ' dBFS' : '−∞'; } },
{ id: 'mic.pitch', src: 'mic', label: 'pitch', get: function () { return lognorm(S.mic.pitch, 70, 500); }, show: function () { return S.mic.pitch ? Math.round(S.mic.pitch) + ' Hz' + (S.mic.voiced ? '' : ' (held)') : '—'; } },
{ id: 'mic.centroid', src: 'mic', label: 'centroid', get: function () { return lognorm(S.mic.centroid, 200, 8000); }, show: function () { return Math.round(S.mic.centroid) + ' Hz'; } },
{ id: 'mic.onset', src: 'mic', label: 'onset', get: function () { return S.mic.onset; }, show: function (v) { return v.toFixed(2); } },
{ id: 'cam.brightness', src: 'cam', label: 'brightness', get: function () { return S.cam.brightness; }, show: function (v) { return v.toFixed(2); } },
{ id: 'cam.motion', src: 'cam', label: 'motion', get: function () { return clamp(S.cam.motion * 8, 0, 1); }, show: function () { return S.cam.motion.toFixed(3); } },
{ id: 'cam.hue', src: 'cam', label: 'hue', get: function () { return S.cam.hue / 360; }, show: function () { return Math.round(S.cam.hue) + '°'; } },
{ id: 'cam.cx', src: 'cam', label: 'motion x', get: function () { return S.cam.cx; }, show: function (v) { return v.toFixed(2); } },
{ id: 'cam.cy', src: 'cam', label: 'motion y', get: function () { return S.cam.cy; }, show: function (v) { return v.toFixed(2); } }
];
var FBY = {}; FEATURES.forEach(function (f) { FBY[f.id] = f; });
function srcActive(src) {
if (src === 'pixel') return S.pixel.active;
if (src === 'gesture') return S.gesture.any;
if (src === 'mic') return S.mic.on;
if (src === 'cam') return S.cam.on;
return false;
}
// meters
var METER = {};
Array.prototype.forEach.call(document.querySelectorAll('.meters[data-src]'), function (box) {
var src = box.getAttribute('data-src');
FEATURES.filter(function (f) { return f.src === src; }).forEach(function (f) {
var row = el('div', 'meter'), bar = el('span', 'bar'), fill = el('i'), val = el('span', 'val', '—');
bar.appendChild(fill); row.appendChild(el('span', 'lab', f.label)); row.appendChild(bar); row.appendChild(val);
box.appendChild(row); METER[f.id] = { row: row, fill: fill, val: val };
});
});
function paintMeters() {
FEATURES.forEach(function (f) {
var m = METER[f.id]; if (!m) return;
var on = srcActive(f.src), v = f.get();
m.row.classList.toggle('on', on);
m.fill.style.width = (clamp(v, 0, 1) * 100).toFixed(1) + '%';
m.val.textContent = (on || f.src === 'gesture') ? f.show(v) : '—';
});
['pixel', 'gesture', 'mic', 'cam'].forEach(function (s) { var c = $('s-' + s); if (c) c.classList.toggle('live', srcActive(s)); });
}
// .pixel + .gesture: pointer events over the visual canvases (the "pads")
var pads = Array.prototype.slice.call(document.querySelectorAll('canvas.pad'));
var ptrs = {}, two = null, lastMove = null;
function gestureEvent(d) {
d.t = Math.round(performance.now()); S.gesture.any = true; S.gesture.last = d;
$('last-gesture').textContent = 'last: ' + d.name + ' · ' + d.source + (d.value != null ? ' · ' + (+d.value).toFixed(2) : '');
emit('dv:gesture', d);
}
pads.forEach(function (c) {
c.addEventListener('pointerdown', function (e) {
ensureCtx();
try { c.setPointerCapture(e.pointerId); } catch (x) {}
ptrs[e.pointerId] = { x0: e.clientX, y0: e.clientY, x: e.clientX, y: e.clientY, t0: performance.now(), c: c };
var ids = Object.keys(ptrs);
if (ids.length === 2) {
var p = ptrs[ids[0]], q = ptrs[ids[1]];
two = { d0: Math.hypot(q.x - p.x, q.y - p.y) || 1, a0: Math.atan2(q.y - p.y, q.x - p.x), pinch0: S.gesture.pinch, rot0: S.gesture.rotate };
}
pixelAt(c, e);
});
c.addEventListener('pointermove', function (e) {
if (ptrs[e.pointerId]) { ptrs[e.pointerId].x = e.clientX; ptrs[e.pointerId].y = e.clientY; }
var ids = Object.keys(ptrs);
if (two && ids.length >= 2) {
var p = ptrs[ids[0]], q = ptrs[ids[1]];
var d = Math.hypot(q.x - p.x, q.y - p.y), a = Math.atan2(q.y - p.y, q.x - p.x), da = a - two.a0;
while (da > Math.PI) da -= 2 * Math.PI; while (da < -Math.PI) da += 2 * Math.PI;
var scale = d / two.d0;
S.gesture.pinch = clamp(two.pinch0 + Math.log(scale) / Math.LN2 / 4, 0, 1);
S.gesture.rotate = clamp(two.rot0 + da / (2 * Math.PI), 0, 1);
S.gesture.any = true;
two.dirty = { scale: scale, angle: da };
}
pixelAt(c, e);
});
function up(e) {
var p = ptrs[e.pointerId]; delete ptrs[e.pointerId];
if (two) {
if (Object.keys(ptrs).length < 2) {
if (two.moved) gestureEvent({ name: 'pinch-end', source: 'pointer', value: S.gesture.pinch, rotate: S.gesture.rotate });
two = null;
}
return;
}
if (!p) return;
var dx = e.clientX - p.x0, dy = e.clientY - p.y0, dt = performance.now() - p.t0, dist = Math.hypot(dx, dy);
if (dt < 700 && dist > 50 && dist / dt > 0.3) {
var name = Math.abs(dx) > Math.abs(dy) ? (dx > 0 ? 'swipe-right' : 'swipe-left') : (dy > 0 ? 'swipe-down' : 'swipe-up');
if (name === 'swipe-right') S.gesture.swipeX = clamp(S.gesture.swipeX + 0.1, 0, 1);
if (name === 'swipe-left') S.gesture.swipeX = clamp(S.gesture.swipeX - 0.1, 0, 1);
if (name === 'swipe-up') S.gesture.swipeY = clamp(S.gesture.swipeY + 0.1, 0, 1);
if (name === 'swipe-down') S.gesture.swipeY = clamp(S.gesture.swipeY - 0.1, 0, 1);
gestureEvent({ name: name, source: 'pointer', value: +(dist / dt).toFixed(3), dx: Math.round(dx), dy: Math.round(dy), ms: Math.round(dt), target: c.id });
} else if (dist < 6 && dt < 400) {
gestureEvent({ name: 'tap', source: 'pointer', value: 1, target: c.id });
}
}
c.addEventListener('pointerup', up); c.addEventListener('pointercancel', up);
c.addEventListener('pointerleave', function () { if (!Object.keys(ptrs).length) { S.pixel.active = false; S.pixel._canvas = null; } });
c.addEventListener('wheel', function (e) {
if (!e.ctrlKey) return; // a trackpad pinch arrives as ctrl + wheel
e.preventDefault();
S.gesture.pinch = clamp(S.gesture.pinch - e.deltaY * 0.004, 0, 1); S.gesture.any = true;
two = two || null; pinchWheel = true;
}, { passive: false });
});
var pinchWheel = false;
function pixelAt(c, e) {
var r = c.getBoundingClientRect(), t = performance.now();
var x = clamp((e.clientX - r.left) / r.width, 0, 1), y = clamp((e.clientY - r.top) / r.height, 0, 1);
var P = S.pixel;
if (lastMove && lastMove.c === c) {
var dt = Math.max(1, t - lastMove.t);
var sp = Math.hypot(e.clientX - lastMove.x, e.clientY - lastMove.y) / dt;
P.speed = P.speed * 0.6 + sp * 0.4;
}
lastMove = { c: c, x: e.clientX, y: e.clientY, t: t };
P.active = true; P.x = x; P.y = y; P.t = t; P.target = c.id;
P.pressure = e.pressure != null ? (e.pointerType === 'mouse' ? (e.buttons ? 0.5 : 0) : e.pressure) : 0;
P._px = { x: Math.floor(x * (c.width - 1)), y: Math.floor(y * (c.height - 1)) }; P._canvas = c; P._dirty = true;
}
var pixG = null;
function pixelFrame() {
var P = S.pixel;
if (P.active && P._canvas && P._px) {
try {
if (!pixG) { var pc = document.createElement('canvas'); pc.width = pc.height = 1; pixG = pc.getContext('2d', { willReadFrequently: true }); }
pixG.clearRect(0, 0, 1, 1); pixG.drawImage(P._canvas, P._px.x, P._px.y, 1, 1, 0, 0, 1, 1);
var d = pixG.getImageData(0, 0, 1, 1).data;
P.luma = (0.2126 * d[0] + 0.7152 * d[1] + 0.0722 * d[2]) / 255;
} catch (e) {}
if (performance.now() - P.t > 120) P.speed *= 0.85;
}
if (P._dirty) {
P._dirty = false;
emit('dv:pixel', { x: +P.x.toFixed(4), y: +P.y.toFixed(4), speed: +P.speed.toFixed(4), pressure: +P.pressure.toFixed(3), luma: +P.luma.toFixed(4), target: P.target, t: Math.round(P.t) });
}
if (two && two.dirty) {
two.moved = true;
gestureEvent({ name: 'pinch', source: 'pointer', value: +S.gesture.pinch.toFixed(4), scale: +two.dirty.scale.toFixed(4), rotation: +two.dirty.angle.toFixed(4), rotate: +S.gesture.rotate.toFixed(4) });
two.dirty = null;
}
if (pinchWheel) { pinchWheel = false; gestureEvent({ name: 'pinch', source: 'wheel', value: +S.gesture.pinch.toFixed(4) }); }
}
// .mic
var MIC = null;
$('mic-on').addEventListener('click', function () {
var a = ensureCtx();
if (!a) return;
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { $('mic-note').textContent = 'this browser offers no microphone API here'; return; }
$('mic-note').textContent = 'asking for the microphone…';
navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: false } }).then(function (stream) {
var src = a.ctx.createMediaStreamSource(stream), an = a.ctx.createAnalyser();
an.fftSize = 2048; an.smoothingTimeConstant = 0.5;
src.connect(an); // to an analyser only: never to the speakers, never anywhere else
MIC = { stream: stream, src: src, an: an, td: new Float32Array(2048), fq: new Float32Array(1024), prev: null, flux: [], onsetAt: 0, frame: 0 };
S.mic.on = true; $('mic-on').hidden = true; $('mic-off').hidden = false;
$('mic-note').textContent = 'on · measured in this tab · nothing recorded, nothing sent';
}).catch(function (e) { $('mic-note').textContent = 'microphone not available: ' + (e && e.name || e) + (window.top !== window ? ' (inside the Hugging Face frame the browser may refuse; open the studio full-window)' : ''); });
});
function micOff() {
if (MIC) { try { MIC.stream.getTracks().forEach(function (t) { t.stop(); }); MIC.src.disconnect(); } catch (e) {} }
MIC = null; S.mic.on = false; S.mic.level = S.mic.peak = S.mic.onset = 0; S.mic.voiced = false;
$('mic-on').hidden = false; $('mic-off').hidden = true; $('mic-note').textContent = 'off · the microphone is released';
}
$('mic-off').addEventListener('click', micOff);
function pitchACF(buf, sr) {
// normalised autocorrelation over 70–500 Hz on the newest 1024 samples; the first peak past the first dip
var n = 1024, off = buf.length - n, minLag = Math.floor(sr / 500), maxLag = Math.min(n - 1, Math.floor(sr / 70));
var e0 = 0; for (var i = 0; i < n; i++) e0 += buf[off + i] * buf[off + i];
if (e0 < 1e-4) return { f: 0, c: 0 };
var best = 0, bestLag = 0, dipped = false, prev = 1;
for (var lag = minLag; lag <= maxLag; lag++) {
var s = 0, e1 = 0;
for (var j = 0; j < n - lag; j++) { s += buf[off + j] * buf[off + j + lag]; e1 += buf[off + j + lag] * buf[off + j + lag]; }
var r = s / Math.sqrt(e0 * e1 + 1e-12);
if (!dipped && r < prev && r < 0.3) dipped = true;
if (dipped && r > best) { best = r; bestLag = lag; }
prev = r;
}
if (!bestLag) return { f: 0, c: 0 };
return { f: sr / bestLag, c: best };
}
function micFrame() {
if (!MIC) return;
var M = S.mic, an = MIC.an, sr = A.ctx.sampleRate;
an.getFloatTimeDomainData(MIC.td);
var sum = 0, pk = 0;
for (var i = 0; i < MIC.td.length; i++) { var v = MIC.td[i]; sum += v * v; if (v > pk) pk = v; else if (-v > pk) pk = -v; }
M.level = Math.sqrt(sum / MIC.td.length); M.peak = pk;
an.getFloatFrequencyData(MIC.fq);
var ws = 0, ms = 0, flux = 0, nb = MIC.fq.length;
if (!MIC.prev) MIC.prev = new Float32Array(nb);
for (var b = 1; b < nb; b++) {
var mag = Math.pow(10, MIC.fq[b] / 20); if (!isFinite(mag)) mag = 0;
ws += mag * b; ms += mag;
var d = mag - MIC.prev[b]; if (d > 0) flux += d; MIC.prev[b] = mag;
}
M.centroid = ms > 0 ? (ws / ms) * sr / (2 * nb) : 0;
MIC.flux.push(flux); if (MIC.flux.length > 43) MIC.flux.shift();
var med = MIC.flux.slice().sort(function (x, y) { return x - y; })[MIC.flux.length >> 1] || 0;
var t = performance.now();
M.onset *= 0.85;
if (flux > med * 2.2 + 0.002 && M.level > 0.01 && t - MIC.onsetAt > 90) { MIC.onsetAt = t; M.onset = 1; }
M.speaking = M.speaking ? M.level > 0.006 : M.level > 0.015;
if ((MIC.frame++ & 1) === 0) {
var p = pitchACF(MIC.td, sr);
M.voiced = p.c > 0.55 && M.level > 0.008;
if (M.voiced) M.pitch = M.pitch ? M.pitch * 0.5 + p.f * 0.5 : p.f;
}
emit('dv:mic', { level: +M.level.toFixed(5), peak: +M.peak.toFixed(5), pitch: M.voiced ? +M.pitch.toFixed(1) : 0, voiced: M.voiced, centroid: Math.round(M.centroid), onset: +M.onset.toFixed(3), speaking: M.speaking, t: Math.round(t) });
}
// .cam — frame differencing on a 64 × 48 copy; no model, no upload
var CAM = null;
$('cam-on').addEventListener('click', function () {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { $('cam-note').textContent = 'this browser offers no camera API here'; return; }
$('cam-note').textContent = 'asking for the camera…';
navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user', width: { ideal: 320 }, height: { ideal: 240 } }, audio: false }).then(function (stream) {
var v = $('cam-video'); v.srcObject = stream; v.hidden = false; v.play().catch(function () {});
var c = document.createElement('canvas'); c.width = 64; c.height = 48;
CAM = { stream: stream, v: v, c: c, g: c.getContext('2d', { willReadFrequently: true }), prev: null, last: 0, hist: [], cool: 0 };
S.cam.on = true; $('cam-on').hidden = true; $('cam-off').hidden = false;
$('cam-note').textContent = 'on · each frame is reduced to 64 × 48 here and thrown away · nothing sent';
}).catch(function (e) { $('cam-note').textContent = 'camera not available: ' + (e && e.name || e) + (window.top !== window ? ' (inside the Hugging Face frame the browser may refuse; open the studio full-window)' : ''); });
});
function camOff() {
if (CAM) { try { CAM.stream.getTracks().forEach(function (t) { t.stop(); }); } catch (e) {} CAM.v.srcObject = null; CAM.v.hidden = true; }
CAM = null; S.cam.on = false; S.cam.motion = 0;
$('cam-on').hidden = false; $('cam-off').hidden = true; $('cam-note').textContent = 'off · the camera is released';
}
$('cam-off').addEventListener('click', camOff);
function camFrame() {
if (!CAM || !CAM.v.videoWidth) return;
var t = performance.now(); if (t - CAM.last < 60) return; CAM.last = t;
var g = CAM.g, W = 64, H = 48;
g.drawImage(CAM.v, 0, 0, W, H);
var px = g.getImageData(0, 0, W, H).data, n = W * H, luma = new Float32Array(n);
var lsum = 0, hue = new Float32Array(36), satSum = 0, satN = 0;
for (var i = 0; i < n; i++) {
var r = px[i * 4] / 255, gg = px[i * 4 + 1] / 255, b = px[i * 4 + 2] / 255;
var l = 0.2126 * r + 0.7152 * gg + 0.0722 * b; luma[i] = l; lsum += l;
var mx = Math.max(r, gg, b), mn = Math.min(r, gg, b), d = mx - mn;
if (mx > 0.2 && d / (mx || 1) > 0.25) {
var h = mx === r ? ((gg - b) / d) % 6 : mx === gg ? (b - r) / d + 2 : (r - gg) / d + 4;
h = (h * 60 + 360) % 360; hue[Math.floor(h / 10) % 36] += d; satSum += d / mx; satN++;
}
}
var C = S.cam; C.brightness = lsum / n;
var bi = 0; for (var k = 1; k < 36; k++) if (hue[k] > hue[bi]) bi = k;
if (satN > n * 0.02) C.hue = bi * 10 + 5;
C.saturation = satN ? satSum / satN : 0;
if (CAM.prev) {
var msum = 0, wx = 0, wy = 0, ws = 0;
for (var j = 0; j < n; j++) {
var dd = Math.abs(luma[j] - CAM.prev[j]); msum += dd;
if (dd > 0.09) { var x = 1 - (j % W) / (W - 1), y = Math.floor(j / W) / (H - 1); wx += x * dd; wy += y * dd; ws += dd; } // x mirrored: as the preview shows it
}
C.motion = msum / n;
if (ws > 0.5) { C.cx = C.cx * 0.5 + (wx / ws) * 0.5; C.cy = C.cy * 0.5 + (wy / ws) * 0.5; }
// a swipe: the centre of motion travelling > 0.35 of the frame inside 450 ms
if (C.motion > 0.012 && ws > 0.5) CAM.hist.push({ t: t, x: wx / ws, y: wy / ws });
while (CAM.hist.length && t - CAM.hist[0].t > 450) CAM.hist.shift();
if (CAM.hist.length >= 3 && t > CAM.cool) {
var h0 = CAM.hist[0], h1 = CAM.hist[CAM.hist.length - 1], dx = h1.x - h0.x, dy = h1.y - h0.y;
if (Math.max(Math.abs(dx), Math.abs(dy)) > 0.35) {
var name = Math.abs(dx) > Math.abs(dy) ? (dx > 0 ? 'swipe-right' : 'swipe-left') : (dy > 0 ? 'swipe-down' : 'swipe-up');
if (name === 'swipe-right') S.gesture.swipeX = clamp(S.gesture.swipeX + 0.1, 0, 1);
if (name === 'swipe-left') S.gesture.swipeX = clamp(S.gesture.swipeX - 0.1, 0, 1);
if (name === 'swipe-up') S.gesture.swipeY = clamp(S.gesture.swipeY + 0.1, 0, 1);
if (name === 'swipe-down') S.gesture.swipeY = clamp(S.gesture.swipeY - 0.1, 0, 1);
gestureEvent({ name: name, source: 'cam', value: +Math.max(Math.abs(dx), Math.abs(dy)).toFixed(3), dx: +dx.toFixed(3), dy: +dy.toFixed(3), ms: Math.round(h1.t - h0.t) });
CAM.cool = t + 700; CAM.hist = [];
}
}
}
CAM.prev = luma;
emit('dv:cam', { brightness: +C.brightness.toFixed(4), motion: +C.motion.toFixed(5), hue: C.hue, saturation: +C.saturation.toFixed(3), cx: +C.cx.toFixed(3), cy: +C.cy.toFixed(3), t: Math.round(t) });
}
// mappings: feature → knob, every frame
function mapFrame() {
KNOBS.forEach(function (k) {
var m = K.maps[k.id]; K.live[k.id] = false;
if (!m.src || !FBY[m.src]) return;
var f = FBY[m.src]; if (!srcActive(f.src)) return;
var x = clamp(f.get(), 0, 1); if (m.inv) x = 1 - x;
var s = K.smooth[k.id]; s = s == null ? x : s * m.smooth + x * (1 - m.smooth); K.smooth[k.id] = s;
var nrm = m.lo + (m.hi - m.lo) * s;
var v = TAPER[k.taper].fromN(nrm, k.min, k.max);
if (k.step) v = Math.round(v / k.step) * k.step;
K.live[k.id] = true;
setKnob(k.id, v, 'map');
});
}
// mapping table
var mapRows = {};
(function buildMapTable() {
var tb = document.querySelector('#map-table tbody');
KNOBS.forEach(function (k) {
var tr = el('tr'), m = K.maps[k.id];
tr.appendChild(el('td', 'mono', k.label));
var sel = el('select'); sel.setAttribute('aria-label', k.label + ' source');
sel.appendChild(new Option('— by hand —', ''));
['pixel', 'gesture', 'mic', 'cam'].forEach(function (src) {
var og = document.createElement('optgroup'); og.label = '.' + src;
FEATURES.filter(function (f) { return f.src === src; }).forEach(function (f) { og.appendChild(new Option(f.id, f.id)); });
sel.appendChild(og);
});
sel.value = m.src;
var lo = el('input'); lo.type = 'number'; lo.min = 0; lo.max = 100; lo.step = 1; lo.value = Math.round(m.lo * 100); lo.setAttribute('aria-label', k.label + ' from %');
var hi = el('input'); hi.type = 'number'; hi.min = 0; hi.max = 100; hi.step = 1; hi.value = Math.round(m.hi * 100); hi.setAttribute('aria-label', k.label + ' to %');
var inv = el('input'); inv.type = 'checkbox'; inv.checked = m.inv; inv.setAttribute('aria-label', k.label + ' invert');
var sm = el('input'); sm.type = 'number'; sm.min = 0; sm.max = 0.99; sm.step = 0.05; sm.value = m.smooth; sm.setAttribute('aria-label', k.label + ' smoothing');
var now = el('td', 'now', '');
function upd() {
m.src = sel.value; m.lo = clamp((+lo.value || 0) / 100, 0, 1); m.hi = clamp((+hi.value || 0) / 100, 0, 1); m.inv = inv.checked; m.smooth = clamp(+sm.value || 0, 0, 0.99);
K.smooth[k.id] = null; save(); notify();
}
[sel, lo, hi, inv, sm].forEach(function (x) { x.addEventListener('change', upd); });
function td(x, suffix) { var c = el('td'); c.appendChild(x); if (suffix) c.appendChild(document.createTextNode(suffix)); return c; }
tr.appendChild(td(sel)); tr.appendChild(td(lo, ' %')); tr.appendChild(td(hi, ' %')); tr.appendChild(td(inv)); tr.appendChild(td(sm)); tr.appendChild(now);
tb.appendChild(tr);
mapRows[k.id] = { tr: tr, sel: sel, lo: lo, hi: hi, inv: inv, sm: sm, now: now };
});
})();
function syncMapRows() {
KNOBS.forEach(function (k) {
var r = mapRows[k.id], m = K.maps[k.id];
r.sel.value = m.src; r.lo.value = Math.round(m.lo * 100); r.hi.value = Math.round(m.hi * 100); r.inv.checked = m.inv; r.sm.value = m.smooth;
});
}
function paintMapNow() {
KNOBS.forEach(function (k) { var r = mapRows[k.id]; r.now.textContent = k.fmt(K.values[k.id]); r.tr.classList.toggle('live', !!K.live[k.id]); });
}
$('map-clear').addEventListener('click', function () { KNOBS.forEach(function (k) { K.maps[k.id] = { src: '', lo: 0, hi: 1, inv: false, smooth: 0.8 }; }); syncMapRows(); save(); notify(); });
$('map-demo').addEventListener('click', function () {
K.maps.bal = { src: 'pixel.x', lo: 0, hi: 1, inv: false, smooth: 0.7 };
K.maps.cutoff = { src: 'pixel.y', lo: 0.15, hi: 1, inv: true, smooth: 0.8 };
syncMapRows(); save(); notify(); $('scope').scrollIntoView({ behavior: 'smooth', block: 'center' });
});
function focusMapping(id) {
var r = mapRows[id]; if (!r) return;
r.tr.scrollIntoView({ behavior: 'smooth', block: 'center' });
r.tr.classList.add('flash'); setTimeout(function () { r.tr.classList.remove('flash'); }, 1400);
try { r.sel.focus({ preventScroll: true }); } catch (e) { r.sel.focus(); }
}
// fallback sliders (only if the dreamknob island never mounts)
var fb = null;
function buildFallback() {
if (window.__mindxStudioRack || fb) return;
var box = $('rack-fallback'); box.hidden = false; fb = {};
box.appendChild(el('p', 'fine', 'The dreamknob rack did not load; these sliders drive the same settings.'));
KNOBS.forEach(function (k) {
var lab = el('label'), inp = el('input'), out = el('span', 'mono', '');
inp.type = 'range'; inp.min = 0; inp.max = 1000; inp.setAttribute('aria-label', k.label);
inp.addEventListener('input', function () { setKnob(k.id, TAPER[k.taper].fromN(inp.value / 1000, k.min, k.max), 'hand'); });
lab.appendChild(el('span', null, k.label)); lab.appendChild(inp); lab.appendChild(out); box.appendChild(lab);
fb[k.id] = { inp: inp, out: out };
});
paintFallback();
}
function paintFallback() {
if (!fb) return;
KNOBS.forEach(function (k) { var f = fb[k.id]; if (document.activeElement !== f.inp) f.inp.value = Math.round(knobN(k.id) * 1000); f.out.textContent = k.fmt(K.values[k.id]); });
}
setTimeout(buildFallback, 3000);
$('rack-reset').addEventListener('click', function () { KNOBS.forEach(function (k) { setKnob(k.id, k.def, 'hand'); }); });
$('pitch-lock').checked = saved.pitchLock !== false;
function applyPitchLock() { var on = $('pitch-lock').checked; try { au.preservesPitch = on; au.mozPreservesPitch = on; au.webkitPreservesPitch = on; } catch (e) {} }
$('pitch-lock').addEventListener('change', function () { applyPitchLock(); save(); }); applyPitchLock();
// ---------------------------------------------------------------- player
var P = { doc: null, voice: null, part: 0, mode: 'doc', pendingSeek: null, offsets: [], total: 0, started: false, lastDocT: 0 };
var bytesCache = {};
var GEN = 0; // bumped by clear / shred: a fetch or decode still in flight then keeps nothing
var OBJURLS = {}; // every object URL this page made and has not revoked
function mkURL(blob) { var u = URL.createObjectURL(blob); OBJURLS[u] = blob.size; return u; }
function rmURL(u) { try { URL.revokeObjectURL(u); } catch (e) {} delete OBJURLS[u]; }
function V() { return P.doc && P.doc.voices[P.voice]; }
function startOf() { var v = V(); return v && v.start && isFinite(+v.start.seconds) && +v.start.seconds >= 0 && +v.start.seconds < P.total ? +v.start.seconds : null; } // a start past this voice's end is not a start
function startLabel() {
var v = V(), s = v && v.start; if (!s) return '';
return s.label || (P.doc.name === 'THESIS' ? 'start of thesis statement' : 'default start');
}
function layoutTimeline() {
var v = V(); P.offsets = []; var acc = 0;
v.parts.forEach(function (p) { P.offsets.push(acc); acc += p.seconds; });
P.total = acc;
}
function locate(T) {
var v = V(), i = 0;
for (var k = 0; k < v.parts.length; k++) if (T >= P.offsets[k]) i = k;
return { i: i, t: clamp(T - P.offsets[i], 0, v.parts[i].seconds) };
}
function docTime() {
if (P.mode !== 'doc' || !V()) return P.lastDocT;
var lt = P.pendingSeek != null ? P.pendingSeek : (au.currentTime || 0);
return (P.offsets[P.part] || 0) + lt;
}
function loadPart(i, local, play) {
var v = V(); if (!v || !v.parts[i]) return;
P.mode = 'doc'; P.part = i;
var url = v.parts[i].file;
if (au.getAttribute('data-src') !== url) {
au.setAttribute('data-src', url); au.src = url; P.pendingSeek = local || 0;
} else if (au.readyState >= 1) { au.currentTime = local || 0; P.pendingSeek = null; }
else P.pendingSeek = local || 0;
applyKnob('speed');
if (play) doPlay();
}
au.addEventListener('loadedmetadata', function () {
if (P.pendingSeek != null) { try { au.currentTime = P.pendingSeek; } catch (e) {} P.pendingSeek = null; }
applyKnob('speed'); applyPitchLock();
});
au.addEventListener('ended', function () {
if (P.mode === 'doc') { var v = V(); if (v && P.part + 1 < v.parts.length) loadPart(P.part + 1, 0, true); else paintTransport(); }
else paintTransport();
});
au.addEventListener('play', paintTransport); au.addEventListener('pause', paintTransport);
au.addEventListener('error', function () { $('viz-state').textContent = 'this browser could not play ' + (au.getAttribute('data-src') || 'the file') + (P.mode === 'doc' ? ' (Ogg Opus: Safari before 18.4 cannot)' : ''); });
function doPlay() {
ensureCtx();
var p = au.play(); if (p && p.catch) p.catch(function (e) { $('viz-state').textContent = 'playback did not start: ' + (e && e.name || e); });
}
function seekDoc(T, play) {
if (!V()) return;
T = clamp(T, 0, Math.max(0, P.total - 0.05));
var L = locate(T); P.started = true;
loadPart(L.i, L.t, play);
P.lastDocT = T;
}
function paintTransport() {
$('t-play').textContent = au.paused ? '▶ play' : '❚❚ pause';
$('t-play').setAttribute('aria-label', au.paused ? 'play' : 'pause');
}
$('t-play').addEventListener('click', function () {
if (!au.paused) { au.pause(); return; }
if (P.mode === 'doc' && !P.started) { var s = startOf(); seekDoc(s != null ? s : 0, true); return; }
doPlay();
});
$('t-back').addEventListener('click', function () { if (P.mode === 'doc') seekDoc(docTime() - 10, !au.paused); else au.currentTime = Math.max(0, au.currentTime - 10); });
$('t-fwd').addEventListener('click', function () { if (P.mode === 'doc') seekDoc(docTime() + 10, !au.paused); else au.currentTime = Math.min(au.duration || 0, au.currentTime + 10); });
$('t-begin').addEventListener('click', function () { seekDoc(0, true); });
$('t-start').addEventListener('click', function () { var s = startOf(); if (s != null) seekDoc(s, true); });
function chipsFor(box, items, on, click) {
while (box.firstChild) box.removeChild(box.firstChild);
items.forEach(function (it) {
var b = el('button', 'chip' + (it.id === on ? ' on' : ''), it.label); b.type = 'button';
b.addEventListener('click', function () { click(it.id); }); box.appendChild(b);
});
}
function selectDoc(name, voice, T) {
var d = DOCS[name]; if (!d) return;
var wasPlaying = !au.paused && P.mode === 'doc';
P.doc = d; P.voice = voice && d.voices[voice] ? voice : d.defaultVoice; P.started = false;
layoutTimeline();
chipsFor($('doc-chips'), DOC_ORDER.map(function (n) { return { id: n, label: n.charAt(0) + n.slice(1).toLowerCase() }; }), name, function (n) { selectDoc(n, P.voice, null); });
chipsFor($('voice-chips'), d.order.map(function (id) { return { id: id, label: d.voices[id].label }; }), P.voice, function (id) { selectDoc(name, id, null); });
$('voice-chips').hidden = d.order.length < 2;
var s = startOf();
$('t-start').hidden = s == null;
$('t-start-label').textContent = s != null ? startLabel() + ' · ' + fmtT(s) : '';
var v = V(), anchor = s != null && v.start && (v.start.anchor || v.start.note);
$('anchor-line').hidden = !anchor;
if (anchor) $('anchor-line').textContent = '⚑ ' + startLabel() + (v.start.anchor ? ': “' + v.start.anchor + '…”' : '') + (v.start.note && v.start.anchor ? ' · ' + v.start.note : (v.start.note && !v.start.anchor ? ': ' + v.start.note : ''));
renderCredit();
// park the playhead at the default start (or the requested time) without playing
var T0 = T != null ? T : (s != null ? s : 0);
var L = locate(T0); P.mode = 'doc';
loadPart(L.i, L.t, false); P.lastDocT = T0;
if (T != null) P.started = true;
if (wasPlaying) { P.started = true; doPlay(); }
try { history.replaceState(null, '', 'studio.html?doc=' + encodeURIComponent(name) + (d.order.length > 1 ? '&voice=' + encodeURIComponent(P.voice) : '')); } catch (e) {}
OV.peaks = null; OV.sel = null; $('ov-edit').disabled = true;
decodeOverview();
}
function renderCredit() {
var box = $('credit'); while (box.firstChild) box.removeChild(box.firstChild);
var v = V(), raw = v.raw || {}, isSav = /savante/i.test(v.id + ' ' + v.label);
box.appendChild(el('p', 'who', isSav ? SAVANTE_CREDIT : 'Read by ' + v.label));
var dl = el('dl'), rows = 0;
function add(k, val) { dl.appendChild(el('dt', null, k)); dl.appendChild(el('dd', null, val)); rows++; }
add('parts', v.parts.length + ' · ' + fmtT(v.seconds) + ' (durations as listed in audio/index.json)');
// whatever the index says about this voice, rendered as given: no number on this page is typed by hand
function walk(prefix, o, depth) {
Object.keys(o).forEach(function (key) {
if (/^(parts|label|start)$/.test(key) && !prefix) return;
var val = o[key], name = prefix ? prefix + '.' + key : key;
if (val && typeof val === 'object' && !Array.isArray(val) && depth < 2) walk(name, val, depth + 1);
else if (Array.isArray(val)) add(name, val.map(function (x) { return typeof x === 'object' ? JSON.stringify(x) : String(x); }).join(' · '));
else if (val != null && val !== '') add(name, String(val));
});
}
var src = P.doc.raw.voices ? raw : { voice: raw.voice, codec: raw.codec, words: raw.words, key: raw.key };
walk('', src, 0);
if (P.doc.listen_url) { var dd = el('dd'), a = el('a', null, P.doc.listen_url.replace(/^https:\/\//, '') + ' ↗'); a.href = P.doc.listen_url; a.rel = 'noopener'; dd.appendChild(a); dl.appendChild(el('dt', null, 'on my node')); dl.appendChild(dd); }
box.appendChild(dl);
}
// ---------------------------------------------------------------- overview (whole recording, decoded here)
var OV = { peaks: null, token: 0, sel: null, drag: null };
function fetchBytes(url) {
if (bytesCache[url]) return Promise.resolve(bytesCache[url]);
var gen = GEN;
return fetch(url, { cache: 'no-store' }).then(function (r) { // no-store: never wait on the HTTP-cache lock the playing <audio> holds on the same URL
if (!r.ok) throw new Error('HTTP ' + r.status); return r.arrayBuffer(); }).then(function (b) {
var keys = Object.keys(bytesCache); if (keys.length > 8) delete bytesCache[keys[0]];
if (gen === GEN) bytesCache[url] = b; return b;
});
}
function decodeAt(bytes, sr) {
var OAC = window.OfflineAudioContext || window.webkitOfflineAudioContext;
if (!OAC) return Promise.reject(new Error('no OfflineAudioContext'));
var oc = new OAC(1, 1, sr);
return new Promise(function (res, rej) {
var p = oc.decodeAudioData(bytes.slice(0), res, rej);
if (p && p.then) p.then(res, rej);
});
}
// Decoded parts, shared by the overview and the editor. Opus has to be decoded from the start of a
// file, so an 18-minute part costs seconds of CPU; when this device says it has the memory
// (navigator.deviceMemory, 4 % of it), the overview decodes at the editor's rate and keeps the
// result, and opening a span is then instant. Unknown memory: nothing is kept, the overview decodes at 8 kHz.
var DEC = { cache: {}, order: [], bytes: 0, inflight: {}, budget: 0 };
try { if (navigator.deviceMemory) DEC.budget = navigator.deviceMemory * 1073741824 * 0.04; } catch (e) {}
function canKeep(seconds) { return seconds * EDIT_SR * 4 <= DEC.budget; }
function decodePart(file, sr, keep) {
var key = file + '@' + sr;
if (DEC.cache[key]) return Promise.resolve(DEC.cache[key]);
if (DEC.inflight[key]) return DEC.inflight[key];
var gen = GEN;
var p = fetchBytes(file).then(function (b) { return decodeAt(b, sr); }).then(function (buf) {
delete DEC.inflight[key];
if (gen !== GEN) return buf;
var size = buf.length * buf.numberOfChannels * 4;
if (keep && size <= DEC.budget) {
while (DEC.order.length && DEC.bytes + size > DEC.budget) { var k = DEC.order.shift(), old = DEC.cache[k]; DEC.bytes -= old.length * old.numberOfChannels * 4; delete DEC.cache[k]; }
DEC.cache[key] = buf; DEC.order.push(key); DEC.bytes += size;
}
return buf;
}, function (e) { delete DEC.inflight[key]; throw e; });
DEC.inflight[key] = p;
return p;
}
function decodeOverview() {
var tok = ++OV.token, v = V(), doc = P.doc.name;
var peaks = { min: new Float32Array(Math.ceil(P.total * OV_RATE) + 2), max: new Float32Array(Math.ceil(P.total * OV_RATE) + 2), upto: 0 };
OV.peaks = peaks;
$('ov-status').textContent = 'decoding the overview in your browser… 0/' + v.parts.length;
var i = 0;
(function next() {
if (tok !== OV.token) return;
if (i >= v.parts.length) { $('ov-status').textContent = 'overview: ' + v.parts.length + ' parts decoded here at ' + (canKeep(Math.max.apply(null, v.parts.map(function (p) { return p.seconds; }))) ? EDIT_SR / 1000 + ' kHz and kept for the editor' : OVERVIEW_SR / 1000 + ' kHz') + ' · peaks every ' + (1000 / OV_RATE) + ' ms · click to seek, drag to choose a span'; drawOverview(); return; }
var part = v.parts[i], off = P.offsets[i];
var keep = canKeep(part.seconds), osr = keep ? EDIT_SR : OVERVIEW_SR;
decodePart(part.file, osr, keep).then(function (buf) {
if (tok !== OV.token) return;
var d = buf.getChannelData(0), per = osr / OV_RATE, b0 = Math.floor(off * OV_RATE);
for (var k = 0; k * per < d.length; k++) {
var lo = 1, hi = -1, e = Math.min(d.length, (k + 1) * per);
for (var s = k * per; s < e; s++) { var x = d[s]; if (x < lo) lo = x; if (x > hi) hi = x; }
var idx = b0 + k; if (idx < peaks.min.length) { peaks.min[idx] = lo; peaks.max[idx] = hi; }
}
peaks.upto = Math.max(peaks.upto, Math.ceil((off + part.seconds) * OV_RATE));
i++; $('ov-status').textContent = 'decoding the overview in your browser… ' + i + '/' + v.parts.length; drawOverview();
setTimeout(next, 0);
}).catch(function (e) {
if (tok !== OV.token) return;
OV.peaks = peaks.upto ? peaks : null;
$('ov-status').textContent = 'overview: this browser could not decode ' + doc + ' part ' + (i + 1) + ' (' + (e && e.message || e) + '). Ogg Opus decodes in Chrome and Firefox; Safari before 18.4 cannot. Playback may still work, and the editor opens files from this device.';
drawOverview();
});
})();
}
var ovC = $('overview');
function ovX(T, w) { return P.total ? T / P.total * w : 0; }
function drawOverview() {
if (!P.doc) return;
sizeCanvas(ovC);
var g = ovC.getContext('2d'), w = ovC.width, h = ovC.height, dpr = w / ovC.clientWidth || 1, mid = h / 2;
g.fillStyle = COL.canvas; g.fillRect(0, 0, w, h);
// edit span (dragged, or the take open in the editor)
var span = OV.sel || (E.origin && E.origin.doc === P.doc.name && E.origin.voice === P.voice ? [E.origin.t0, E.origin.t1] : null);
if (span) { g.fillStyle = COL.sel; g.fillRect(ovX(span[0], w), 0, Math.max(1, ovX(span[1], w) - ovX(span[0], w)), h); }
var pk = OV.peaks;
if (pk) {
g.fillStyle = COL.wave;
for (var x = 0; x < w; x++) {
var a = Math.floor(x / w * P.total * OV_RATE), b = Math.max(a + 1, Math.floor((x + 1) / w * P.total * OV_RATE));
if (a >= pk.upto) break;
var lo = 0, hi = 0; for (var k = a; k < b && k < pk.min.length; k++) { if (pk.min[k] < lo) lo = pk.min[k]; if (pk.max[k] > hi) hi = pk.max[k]; }
var y0 = mid - hi * mid * 0.95, y1 = mid - lo * mid * 0.95; g.fillRect(x, y0, 1, Math.max(1, y1 - y0));
}
} else { g.strokeStyle = COL.grid; g.beginPath(); g.moveTo(0, mid); g.lineTo(w, mid); g.stroke(); }
// part boundaries
g.strokeStyle = COL.text3; g.setLineDash([3 * dpr, 3 * dpr]); g.lineWidth = 1;
P.offsets.forEach(function (o, i) { if (!i) return; var x = Math.round(ovX(o, w)) + 0.5; g.beginPath(); g.moveTo(x, 0); g.lineTo(x, h); g.stroke(); });
g.setLineDash([]);
g.font = (10 * dpr) + 'px "IBM Plex Mono", monospace'; g.textBaseline = 'top';
P.offsets.forEach(function (o, i) { g.fillStyle = COL.text3; g.fillText('part ' + (i + 1), ovX(o, w) + 4 * dpr, h - 13 * dpr); });
// the default start
var s = startOf();
if (s != null) {
var sx = Math.round(ovX(s, w)) + 0.5; g.strokeStyle = COL.start; g.lineWidth = 2 * dpr; g.beginPath(); g.moveTo(sx, 0); g.lineTo(sx, h); g.stroke(); g.lineWidth = 1;
var lab = '⚑ ' + startLabel() + ' · ' + fmtT(s), tw = g.measureText(lab).width;
g.fillStyle = COL.start; g.fillText(lab, sx + 4 * dpr + tw > w ? sx - tw - 4 * dpr : sx + 4 * dpr, 3 * dpr);
}
// playhead
if (P.mode === 'doc') { var px = Math.round(ovX(docTime(), w)) + 0.5; g.strokeStyle = COL.cursor; g.beginPath(); g.moveTo(px, 0); g.lineTo(px, h); g.stroke(); }
}
function ovTime(e) { var r = ovC.getBoundingClientRect(); return clamp((e.clientX - r.left) / r.width, 0, 1) * P.total; }
ovC.addEventListener('pointerdown', function (e) { if (!P.doc) return; ensureCtx(); try { ovC.setPointerCapture(e.pointerId); } catch (x) {} OV.drag = { t0: ovTime(e), x0: e.clientX, moved: false }; });
ovC.addEventListener('pointermove', function (e) {
if (!OV.drag) return;
if (Math.abs(e.clientX - OV.drag.x0) > 5) OV.drag.moved = true;
if (OV.drag.moved) { var t = ovTime(e); OV.sel = [Math.min(t, OV.drag.t0), Math.max(t, OV.drag.t0)]; drawOverview(); }
});
ovC.addEventListener('pointerup', function (e) {
var d = OV.drag; OV.drag = null; if (!d) return;
if (!d.moved) { OV.sel = null; $('ov-edit').disabled = true; seekDoc(ovTime(e), P.mode === 'doc' ? !au.paused : true); }
else { $('ov-edit').disabled = !(OV.sel && OV.sel[1] - OV.sel[0] > 0.05); $('ov-edit').textContent = 'open ' + fmtT(OV.sel[0]) + '–' + fmtT(OV.sel[1]) + ' in the editor ↓'; }
drawOverview();
});
$('ov-edit').addEventListener('click', function () { if (OV.sel) loadSpan(OV.sel[0], OV.sel[1]); });
$('ov-edit-here').addEventListener('click', function () { var t = docTime(); loadSpan(t, Math.min(P.total, t + 60)); });
// ---------------------------------------------------------------- visuals
var scopeC = $('scope'), specC = $('spectrum'), sgC = $('sgram');
var SG = { off: null, palette: null };
function hexToRgb(c) {
var m = /^#?([0-9a-f]{6})$/i.exec(c.trim()); if (m) { var n = parseInt(m[1], 16); return [n >> 16 & 255, n >> 8 & 255, n & 255]; }
m = /rgba?\(([^)]+)\)/.exec(c); if (m) { var p = m[1].split(',').map(parseFloat); return [p[0], p[1], p[2]]; }
return [128, 128, 128];
}
function buildPalette() {
var bg = hexToRgb(COL.canvas || '#0a0d0c'), mid = hexToRgb(COL.spec || '#d6b25e'), top = hexToRgb(COL.text || '#fff'), pal = new Uint8ClampedArray(256 * 3);
for (var i = 0; i < 256; i++) {
var t = i / 255, a, b, u;
if (t < 0.6) { a = bg; b = mid; u = t / 0.6; } else { a = mid; b = top; u = (t - 0.6) / 0.4; }
for (var c = 0; c < 3; c++) pal[i * 3 + c] = a[c] + (b[c] - a[c]) * u;
}
SG.palette = pal;
}
function vizState() {
if (!A) return 'idle';
if (A.ctx.state !== 'running') return 'suspended';
return au.paused ? 'resting' : 'live';
}
function drawScope(st) {
sizeCanvas(scopeC);
var g = scopeC.getContext('2d'), w = scopeC.width, h = scopeC.height, mid = h / 2, dpr = w / scopeC.clientWidth || 1;
g.fillStyle = COL.canvas; g.fillRect(0, 0, w, h);
g.strokeStyle = COL.grid; g.lineWidth = 1; g.beginPath();
for (var i = 1; i < 8; i++) { var x = Math.round(i * w / 8) + 0.5; g.moveTo(x, 0); g.lineTo(x, h); }
for (var j = 1; j < 4; j++) { var y = Math.round(j * h / 4) + 0.5; g.moveTo(0, y); g.lineTo(w, y); }
g.stroke();
var buf = A && A.f32, N = buf ? buf.length : 0, gain = K.values.sgain;
var win = Math.max(64, Math.floor(2048 / K.values.szoom));
g.strokeStyle = COL.scope; g.lineWidth = 1.5 * dpr; g.beginPath();
if (st === 'live' || st === 'resting') {
A.an.getFloatTimeDomainData(buf);
var pk = 0; for (var q = 0; q < N; q++) { var av = buf[q] < 0 ? -buf[q] : buf[q]; if (av > pk) pk = av; }
var thr = pk * 0.05, trig = -1, armed = false;
for (var s = 1; s < N - win; s++) {
if (buf[s] < -thr) armed = true;
if (armed && buf[s - 1] <= 0 && buf[s] > 0) { trig = s; break; }
}
var s0 = trig >= 0 ? trig : N - win;
for (var k = 0; k < win; k++) {
var xx = k / (win - 1) * w, yy = mid - clamp(buf[s0 + k] * gain, -1, 1) * mid * 0.95;
if (k) g.lineTo(xx, yy); else g.moveTo(xx, yy);
}
g.stroke();
var sr = A.ctx.sampleRate;
$('scope-note').textContent = (trig >= 0 ? 'triggered' : 'free-running') + ' · ' + (win / sr * 1000).toFixed(1) + ' ms across · peak ' + (pk > 0 ? (20 * Math.log10(pk)).toFixed(1) : '−∞') + ' dBFS';
} else { g.moveTo(0, mid); g.lineTo(w, mid); g.stroke(); }
}
var SPEC_LO = 40;
function drawSpectrum(st) {
sizeCanvas(specC);
var g = specC.getContext('2d'), w = specC.width, h = specC.height, dpr = w / specC.clientWidth || 1;
g.fillStyle = COL.canvas; g.fillRect(0, 0, w, h);
g.font = (9 * dpr) + 'px "IBM Plex Mono", monospace'; g.fillStyle = COL.text3; g.strokeStyle = COL.grid; g.beginPath();
var nyq = A ? A.ctx.sampleRate / 2 : 24000;
[100, 1000, 10000].forEach(function (f) { var x = Math.log(f / SPEC_LO) / Math.log(nyq / SPEC_LO) * w; g.moveTo(x, 0); g.lineTo(x, h); g.fillText(f >= 1000 ? f / 1000 + 'k' : String(f), x + 3, h - 4); });
g.stroke();
if (st !== 'live' && st !== 'resting') return;
A.an.getFloatFrequencyData(A.fq);
var n = A.fq.length, binHz = nyq / n;
g.fillStyle = COL.spec;
var step = Math.max(1, Math.round(2 * dpr));
for (var x = 0; x < w; x += step) {
var f0 = SPEC_LO * Math.pow(nyq / SPEC_LO, x / w), f1 = SPEC_LO * Math.pow(nyq / SPEC_LO, (x + step) / w);
var b0 = Math.floor(f0 / binHz), b1 = Math.max(b0 + 1, Math.floor(f1 / binHz)), m = -200;
for (var b = b0; b < b1 && b < n; b++) if (A.fq[b] > m) m = A.fq[b];
var v = clamp((m + 100) / 90, 0, 1);
g.fillRect(x, h - v * h, step - (step > 1 ? 1 : 0), v * h);
}
}
function drawSgram(st) {
var resized = sizeCanvas(sgC);
var w = sgC.width, h = sgC.height;
if (!SG.off || resized || SG.off.width !== w || SG.off.height !== h) {
SG.off = document.createElement('canvas'); SG.off.width = w; SG.off.height = h;
var og = SG.off.getContext('2d'); og.fillStyle = COL.canvas; og.fillRect(0, 0, w, h);
SG.col = og.createImageData(1, h);
}
var g = sgC.getContext('2d');
if (st === 'live') {
var o = SG.off.getContext('2d');
o.drawImage(SG.off, -1, 0);
var nyq = A.ctx.sampleRate / 2, n = A.fq.length, binHz = nyq / n, d = SG.col.data, pal = SG.palette, c = K.values.contrast;
for (var y = 0; y < h; y++) {
var f = SPEC_LO * Math.pow(nyq / SPEC_LO, 1 - y / (h - 1)), b = Math.min(n - 1, Math.floor(f / binHz));
var v = clamp((A.fq[b] + 100) / 90, 0, 1); v = Math.pow(v, 1 / c);
var idx = Math.round(v * 255) * 3, p = y * 4; d[p] = pal[idx]; d[p + 1] = pal[idx + 1]; d[p + 2] = pal[idx + 2]; d[p + 3] = 255;
}
o.putImageData(SG.col, w - 1, 0); SG.cols = (SG.cols || 0) + 1;
}
g.drawImage(SG.off, 0, 0);
}
// ---------------------------------------------------------------- editor
var E = { take: null, origin: null, edited: false, view: { start: 0, spp: 1 }, sel: null, cursor: 0, markers: [], undo: [], redo: [], clipboard: null,
peaks: null, loop: false, wavUrl: null, wavDirty: true, playEnd: null, playStart: 0, budget: 64 * 1048576 };
try { if (navigator.deviceMemory) E.budget = clamp(navigator.deviceMemory * 1073741824 * 0.1, 64 * 1048576, 512 * 1048576); } catch (e) {}
var edC = $('ed-canvas'), RULER = 16;
// the pure operations, ported from DeltaVerse engine/ngn/wave-editor.js (DVWaveEditor.ops)
function mk(channels, sr) { return { channels: channels, sampleRate: sr, length: channels[0] ? channels[0].length : 0 }; }
function rng(len, a, b) { a = clamp(Math.round(a), 0, len); b = clamp(Math.round(b), 0, len); return a <= b ? [a, b] : [b, a]; }
var ops = {
slice: function (t, a, b) { var r = rng(t.length, a, b); return mk(t.channels.map(function (c) { return c.slice(r[0], r[1]); }), t.sampleRate); },
remove: function (t, a, b) { var r = rng(t.length, a, b); return mk(t.channels.map(function (c) { var o = new Float32Array(c.length - (r[1] - r[0])); o.set(c.subarray(0, r[0]), 0); o.set(c.subarray(r[1]), r[0]); return o; }), t.sampleRate); },
silence: function (t, a, b) { var r = rng(t.length, a, b); return mk(t.channels.map(function (c) { var o = c.slice(); o.fill(0, r[0], r[1]); return o; }), t.sampleRate); },
insert: function (t, clip, at, until) {
var r = rng(t.length, at, until == null ? at : until), src = t.channels.map(function (_, i) { return clip.channels[Math.min(i, clip.channels.length - 1)]; }), cl = src[0].length;
return mk(t.channels.map(function (c, i) { var o = new Float32Array(c.length - (r[1] - r[0]) + cl); o.set(c.subarray(0, r[0]), 0); o.set(src[i], r[0]); o.set(c.subarray(r[1]), r[0] + cl); return o; }), t.sampleRate);
},
fade: function (t, a, b, dir) { var r = rng(t.length, a, b), n = r[1] - r[0]; return mk(t.channels.map(function (c) { var o = c.slice(); for (var i = 0; i < n; i++) { var g = n > 1 ? i / (n - 1) : 1; o[r[0] + i] *= dir === 'out' ? 1 - g : g; } return o; }), t.sampleRate); },
amplify: function (t, a, b, dB) { var r = rng(t.length, a, b), g = Math.pow(10, dB / 20); return mk(t.channels.map(function (c) { var o = c.slice(); for (var i = r[0]; i < r[1]; i++) { var v = o[i] * g; o[i] = v > 1 ? 1 : v < -1 ? -1 : v; } return o; }), t.sampleRate); },
normalize: function (t, a, b, peakDb) { var r = rng(t.length, a, b), pk = 0; t.channels.forEach(function (c) { for (var i = r[0]; i < r[1]; i++) { var v = Math.abs(c[i]); if (v > pk) pk = v; } }); if (pk <= 0) return null; return ops.amplify(t, r[0], r[1], 20 * Math.log10(Math.pow(10, peakDb / 20) / pk)); },
reverse: function (t, a, b) { var r = rng(t.length, a, b); return mk(t.channels.map(function (c) { var o = c.slice(); o.subarray(r[0], r[1]).reverse(); return o; }), t.sampleRate); },
bytes: function (t) { return t.channels.reduce(function (a, c) { return a + c.byteLength; }, 0); }
};
function wavBlob(t) {
var ch = t.channels.length, n = t.length, sr = t.sampleRate, bytes = n * ch * 2, buf = new ArrayBuffer(44 + bytes), dv = new DataView(buf), o = 0;
function str(s) { for (var i = 0; i < s.length; i++) dv.setUint8(o++, s.charCodeAt(i)); }
str('RIFF'); dv.setUint32(o, 36 + bytes, true); o += 4; str('WAVE'); str('fmt ');
dv.setUint32(o, 16, true); o += 4; dv.setUint16(o, 1, true); o += 2; dv.setUint16(o, ch, true); o += 2;
dv.setUint32(o, sr, true); o += 4; dv.setUint32(o, sr * ch * 2, true); o += 4; dv.setUint16(o, ch * 2, true); o += 2; dv.setUint16(o, 16, true); o += 2;
str('data'); dv.setUint32(o, bytes, true); o += 4;
var pcm = new Int16Array(buf, 44, n * ch);
for (var c = 0; c < ch; c++) { var d = t.channels[c]; for (var i = 0; i < n; i++) { var v = d[i]; v = v > 1 ? 1 : v < -1 ? -1 : v; pcm[i * ch + c] = v < 0 ? v * 32768 : v * 32767; } }
var blob = new Blob([buf], { type: 'audio/wav' });
new Uint8Array(buf).fill(0); // the Blob holds its own copy; this scratch copy of the audio does not linger
return blob;
}
function say(msg) { $('ed-msg').textContent = msg; }
function computePeaks() {
var t = E.take; if (!t) { E.peaks = null; return; }
var B = 256, nb = Math.ceil(t.length / B);
E.peaks = t.channels.map(function (c) {
var mn = new Float32Array(nb), mx = new Float32Array(nb);
for (var k = 0; k < nb; k++) { var lo = 1, hi = -1, e = Math.min(c.length, (k + 1) * B); for (var i = k * B; i < e; i++) { var v = c[i]; if (v < lo) lo = v; if (v > hi) hi = v; } mn[k] = lo; mx[k] = hi; }
return { min: mn, max: mx };
});
}
function edW() { sizeCanvas(edC); return edC.width; }
function maxSpp() { return E.take ? Math.max(1 / 32, E.take.length / edW()) : 1; }
function clampView() {
var v = E.view; v.spp = clamp(v.spp, 1 / 32, maxSpp());
v.start = clamp(v.start, 0, Math.max(0, E.take.length - edW() * v.spp));
var range = Math.max(0, E.take.length - edW() * v.spp);
$('ed-scroll').disabled = range <= 0; $('ed-scroll').value = range > 0 ? Math.round(v.start / range * 1000) : 0;
}
function setTake(t, origin, keepView) {
E.take = t; E.origin = origin || E.origin; computePeaks(); E.wavDirty = true;
if (!keepView) { E.view.start = 0; E.view.spp = maxSpp(); E.sel = null; E.cursor = 0; }
clampView(); edDraw(); edStatus(); drawOverview();
}
function edStatus() {
var t = E.take; if (!t) { $('ed-status').textContent = 'no take loaded'; return; }
var o = E.origin, src = o && o.doc ? o.doc + ' · ' + o.voiceLabel + ' · ' + fmtT(o.t0, 2) + '–' + fmtT(o.t1, 2) : (o && o.file ? o.file : 'take');
var visible = edW() * E.view.spp / t.sampleRate;
$('ed-status').textContent = src + (E.edited ? ' · edited' : '') + ' · ' + fmtT(t.length / t.sampleRate, 3) + ' · ' + t.sampleRate + ' Hz · ' + t.channels.length + ' ch · view ' + (visible < 1 ? (visible * 1000).toFixed(1) + ' ms' : fmtT(visible, 2)) +
' · ' + (E.view.spp >= 1 ? E.view.spp.toFixed(E.view.spp < 10 ? 2 : 0) + ' samples/px' : (1 / E.view.spp).toFixed(1) + ' px/sample') +
(E.sel ? ' · sel ' + fmtT(E.sel[0] / t.sampleRate, 3) + '–' + fmtT(E.sel[1] / t.sampleRate, 3) : ' · cursor ' + fmtT(E.cursor / t.sampleRate, 3)) +
' · undo ' + E.undo.length;
}
function loadSpan(t0, t1) {
var v = V(); if (!v) return;
t0 = clamp(t0, 0, P.total); t1 = clamp(t1, 0, P.total); if (t1 - t0 < 0.05) return say('the span is too short');
if (t1 - t0 > 1200) t1 = t0 + 1200;
say('decoding ' + fmtT(t0) + '–' + fmtT(t1) + ' at ' + EDIT_SR / 1000 + ' kHz in your browser… (Opus decodes from the start of its part, so a long part takes a few seconds)');
var pieces = [], jobs = [];
v.parts.forEach(function (p, i) {
var a = P.offsets[i], b = a + p.seconds; if (b <= t0 || a >= t1) return;
jobs.push(decodePart(p.file, EDIT_SR, canKeep(p.seconds)).then(function (buf) {
var s0 = Math.floor((Math.max(t0, a) - a) * EDIT_SR), s1 = Math.min(buf.length, Math.ceil((Math.min(t1, b) - a) * EDIT_SR));
var chs = []; for (var c = 0; c < Math.min(2, buf.numberOfChannels); c++) chs.push(buf.getChannelData(c).slice(s0, s1));
pieces[i] = chs;
}));
});
var doc = P.doc.name, voice = P.voice, vlabel = v.label, gen = GEN;
Promise.all(jobs).then(function () {
if (gen !== GEN) return say('cleared while decoding: nothing kept');
var list = pieces.filter(Boolean); if (!list.length) throw new Error('nothing decoded');
var nch = Math.min.apply(null, list.map(function (x) { return x.length; }));
var chans = []; for (var c = 0; c < nch; c++) { var len = list.reduce(function (s, x) { return s + x[c].length; }, 0), o = new Float32Array(len), at = 0; list.forEach(function (x) { o.set(x[c], at); at += x[c].length; }); chans.push(o); }
E.undo = []; E.redo = []; E.markers = []; E.edited = false;
setTake(mk(chans, EDIT_SR), { doc: doc, voice: voice, voiceLabel: vlabel, t0: t0, t1: t0 + chans[0].length / EDIT_SR });
renderMarkers();
say('opened · ' + (ops.bytes(E.take) / 1048576).toFixed(1) + ' MB in memory · undo keeps up to ' + Math.round(E.budget / 1048576) + ' MB');
$('editor').scrollIntoView({ behavior: 'smooth', block: 'start' });
}).catch(function (e) {
say('could not decode: ' + (e && e.message || e) + '. Ogg Opus decodes in Chrome and Firefox; Safari before 18.4 cannot. You can open a file from this device instead.');
});
}
$('ed-load-start').addEventListener('click', function () { var s = startOf(); s = s == null ? 0 : s; loadSpan(s, Math.min(P.total, s + 90)); });
$('ed-load-part').addEventListener('click', function () { var v = V(), i = P.mode === 'doc' ? P.part : locate(P.lastDocT).i; loadSpan(P.offsets[i], P.offsets[i] + v.parts[i].seconds); });
$('ed-file').addEventListener('change', function (e) {
var f = e.target.files && e.target.files[0]; if (!f) return;
if (f.size > 200 * 1048576) return say('that file is over 200 MB; open something smaller');
say('decoding ' + f.name + ' in your browser…');
f.arrayBuffer().then(function (b) { return decodeAt(b, 44100); }).then(function (buf) {
var chs = []; for (var c = 0; c < Math.min(2, buf.numberOfChannels); c++) chs.push(buf.getChannelData(c).slice());
E.undo = []; E.redo = []; E.markers = []; E.edited = false;
setTake(mk(chs, buf.sampleRate), { file: f.name }); renderMarkers();
say('opened ' + f.name + ' · decoded here at 44.1 kHz · it stays on this device');
}).catch(function (err) { say('this browser could not decode ' + f.name + ': ' + (err && err.message || err)); });
e.target.value = '';
});
function shiftMarkers(kind, a, b, len) {
E.markers = E.markers.map(function (m) {
var s = m.s;
if (kind === 'remove') { if (s >= b) s -= (b - a); else if (s >= a) return null; }
else if (kind === 'insert') { if (s >= b) s = s - (b - a) + len; else if (s >= a) s = a; }
else if (kind === 'trim') { if (s < a || s >= b) return null; s -= a; }
return { s: s, label: m.label };
}).filter(Boolean);
}
function snapshot() { return { take: E.take, markers: E.markers.slice(), sel: E.sel, cursor: E.cursor }; }
function boundHistory() {
var tot = function () { return E.undo.concat(E.redo).reduce(function (s, x) { return s + ops.bytes(x.take); }, 0); };
while (E.undo.length && tot() > E.budget) E.undo.shift();
}
function commit(next, label, sel, cursor) {
if (!next) return say('nothing to do');
E.undo.push(snapshot()); E.redo = [];
E.edited = true; setTake(next, null, true);
E.sel = sel || null; if (cursor != null) E.cursor = cursor;
E.cursor = clamp(E.cursor, 0, E.take.length);
boundHistory(); clampView(); edDraw(); edStatus(); renderMarkers();
say(label);
emit('dv:wave-edited', { op: label, duration: E.take.length / E.take.sampleRate, sampleRate: E.take.sampleRate, origin: E.origin });
}
function restore(s) { E.take = s.take; E.markers = s.markers; E.sel = s.sel; E.cursor = s.cursor; computePeaks(); E.wavDirty = true; clampView(); edDraw(); edStatus(); renderMarkers(); }
function needSel(what) { if (!E.sel || E.sel[1] - E.sel[0] < 1) { say('select something to ' + what + ' (drag on the waveform)'); return null; } return E.sel; }
function rangeOrAll() { return E.sel && E.sel[1] - E.sel[0] >= 1 ? E.sel : [0, E.take.length]; }
var ACT = {
zoomIn: function () { zoomAt(2, edW() / 2); }, zoomOut: function () { zoomAt(0.5, edW() / 2); },
fit: function () { E.view.start = 0; E.view.spp = maxSpp(); },
zoomSel: function () { var s = needSel('zoom to'); if (!s) return; E.view.spp = (s[1] - s[0]) / edW() * 1.05; E.view.start = s[0] - (s[1] - s[0]) * 0.025; },
zoomSample: function () { var c = E.sel ? (E.sel[0] + E.sel[1]) / 2 : E.cursor; E.view.spp = 1 / 16; E.view.start = c - edW() / 2 * E.view.spp; },
cut: function () { var s = needSel('cut'); if (!s) return; E.clipboard = ops.slice(E.take, s[0], s[1]); var nx = ops.remove(E.take, s[0], s[1]); shiftMarkers('remove', s[0], s[1]); commit(nx, 'cut ' + fmtT((s[1] - s[0]) / E.take.sampleRate, 3), null, s[0]); },
copy: function () { var s = needSel('copy'); if (!s) return; E.clipboard = ops.slice(E.take, s[0], s[1]); say('copied ' + fmtT((s[1] - s[0]) / E.take.sampleRate, 3)); },
paste: function () {
if (!E.clipboard) return say('nothing to paste: cut or copy first');
var a = E.sel ? E.sel[0] : E.cursor, b = E.sel ? E.sel[1] : E.cursor, len = E.clipboard.length;
var nx = ops.insert(E.take, E.clipboard, a, b); shiftMarkers('insert', a, b, len); commit(nx, 'pasted ' + fmtT(len / E.take.sampleRate, 3), [a, a + len], a + len);
},
del: function () { var s = needSel('delete'); if (!s) return; var nx = ops.remove(E.take, s[0], s[1]); shiftMarkers('remove', s[0], s[1]); commit(nx, 'deleted', null, s[0]); },
trim: function () { var s = needSel('keep'); if (!s) return; var nx = ops.slice(E.take, s[0], s[1]); shiftMarkers('trim', s[0], s[1]); E.view.start = 0; commit(nx, 'trimmed to the selection', null, 0); E.view.spp = maxSpp(); clampView(); edDraw(); },
silence: function () { var s = needSel('silence'); if (!s) return; commit(ops.silence(E.take, s[0], s[1]), 'silenced', s); },
fadeIn: function () { var r = rangeOrAll(); commit(ops.fade(E.take, r[0], r[1], 'in'), 'faded in', E.sel); },
fadeOut: function () { var r = rangeOrAll(); commit(ops.fade(E.take, r[0], r[1], 'out'), 'faded out', E.sel); },
normalize: function () { var r = rangeOrAll(); commit(ops.normalize(E.take, r[0], r[1], -1), 'normalized to −1 dBFS', E.sel); },
reverse: function () { var r = rangeOrAll(); commit(ops.reverse(E.take, r[0], r[1]), 'reversed', E.sel); },
gain: function () { var d = clamp(+$('ed-gain').value || 0, -48, 24), r = rangeOrAll(); commit(ops.amplify(E.take, r[0], r[1], d), (d > 0 ? '+' : '') + d + ' dB (clipped at ±1)', E.sel); },
undo: function () { if (!E.undo.length) return say('nothing to undo'); E.redo.push(snapshot()); restore(E.undo.pop()); E.edited = E.undo.length > 0; say('undone'); emit('dv:wave-edited', { op: 'undo', duration: E.take.length / E.take.sampleRate }); },
redo: function () { if (!E.redo.length) return say('nothing to redo'); E.undo.push(snapshot()); restore(E.redo.pop()); E.edited = true; say('redone'); emit('dv:wave-edited', { op: 'redo', duration: E.take.length / E.take.sampleRate }); },
marker: function () { var s = E.sel ? E.sel[0] : E.cursor; E.markers.push({ s: s, label: 'M' + (E.markers.length + 1) }); E.markers.sort(function (a, b) { return a.s - b.s; }); renderMarkers(); say('marker at ' + fmtT(s / E.take.sampleRate, 3)); },
play: function () { playTake(); },
loop: function () { E.loop = !E.loop; document.querySelector('[data-ed="loop"]').setAttribute('aria-pressed', String(E.loop)); say(E.loop ? 'looping the selection' : 'loop off'); },
stop: function () { if (P.mode === 'take') au.pause(); E.playEnd = null; },
export: function () {
var s = E.sel && E.sel[1] - E.sel[0] >= 1 ? E.sel : null, t = s ? ops.slice(E.take, s[0], s[1]) : E.take;
var o = E.origin || {}, name = (o.doc ? o.doc + '-' + o.voice + '-' + Math.round(o.t0) + 's' : (o.file || 'take').replace(/\.[^.]+$/, '')) + (E.edited ? '-edited' : '') + (s ? '-selection' : '') + '.wav';
var url = mkURL(wavBlob(t)), a = document.createElement('a'); a.href = url; a.download = name; a.target = '_self'; document.body.appendChild(a); a.click(); a.remove();
setTimeout(function () { rmURL(url); }, 60000);
say('exported ' + name + ' · 16-bit PCM · ' + t.sampleRate + ' Hz · made in this tab');
},
toDoc: function () { if (P.mode !== 'take') return say('the player is already on the document'); var T = P.lastDocT; au.pause(); P.mode = 'doc'; au.removeAttribute('data-src'); seekDoc(T, false); say('the player is back on the document at ' + fmtT(T)); }
};
function runEd(name) {
if (!E.take && !/^(toDoc)$/.test(name)) return say('open a take first: one of the buttons above, or drag a span on the overview');
ACT[name](); if (E.take) { clampView(); edDraw(); edStatus(); }
}
Array.prototype.forEach.call(document.querySelectorAll('[data-ed]'), function (b) { b.addEventListener('click', function () { runEd(b.getAttribute('data-ed')); }); });
function renderMarkers() {
var ul = $('ed-markers'); while (ul.firstChild) ul.removeChild(ul.firstChild);
E.markers.forEach(function (m, i) {
var li = el('li'), go = el('button', null, '⚑ ' + m.label + ' ' + fmtT(m.s / E.take.sampleRate, 2)), x = el('button', 'x', '×');
go.type = x.type = 'button'; x.setAttribute('aria-label', 'remove marker ' + m.label);
go.addEventListener('click', function () { E.cursor = m.s; E.sel = null; E.view.start = m.s - edW() / 2 * E.view.spp; clampView(); edDraw(); edStatus(); if (P.mode === 'take') au.currentTime = m.s / E.take.sampleRate; });
x.addEventListener('click', function () { E.markers.splice(i, 1); renderMarkers(); edDraw(); });
li.appendChild(go); li.appendChild(x); ul.appendChild(li);
});
}
function ensureTakeAudio() {
if (P.mode === 'doc') P.lastDocT = docTime();
if (E.wavDirty || !E.wavUrl) {
if (E.wavUrl) rmURL(E.wavUrl);
E.wavUrl = mkURL(wavBlob(E.take)); E.wavDirty = false;
au.setAttribute('data-src', E.wavUrl); au.src = E.wavUrl;
} else if (P.mode !== 'take') { au.setAttribute('data-src', E.wavUrl); au.src = E.wavUrl; }
P.mode = 'take';
}
function playTake() {
var sr = E.take.sampleRate, s = E.sel && E.sel[1] - E.sel[0] >= 1 ? E.sel : null;
var from = s ? s[0] / sr : E.cursor / sr;
E.playStart = from; E.playEnd = s ? s[1] / sr : null;
var wasTake = P.mode === 'take' && !E.wavDirty;
ensureTakeAudio();
if (wasTake && au.readyState >= 1) au.currentTime = from; else P.pendingSeek = from;
doPlay(); say(s ? 'playing the selection' + (E.loop ? ', looped' : '') : 'playing from the cursor');
}
function takeFrame() {
if (P.mode !== 'take' || au.paused || E.playEnd == null) return;
if (au.currentTime >= E.playEnd) { if (E.loop) au.currentTime = E.playStart; else { au.pause(); E.playEnd = null; } }
}
function zoomAt(f, x) {
var v = E.view, s = v.start + x * v.spp; v.spp = clamp(v.spp / f, 1 / 32, maxSpp()); v.start = s - x * v.spp; clampView(); edDraw(); edStatus();
}
$('ed-scroll').addEventListener('input', function (e) { if (!E.take) return; var range = Math.max(0, E.take.length - edW() * E.view.spp); E.view.start = e.target.value / 1000 * range; edDraw(); edStatus(); });
function edX(e) { var r = edC.getBoundingClientRect(); return (e.clientX - r.left) / r.width * edC.width; }
function edSample(x) { return clamp(E.view.start + x * E.view.spp, 0, E.take ? E.take.length : 0); }
var ed = { ptrs: {}, drag: null, pinch: null };
edC.addEventListener('pointerdown', function (e) {
if (!E.take) return; edC.focus({ preventScroll: true });
try { edC.setPointerCapture(e.pointerId); } catch (x) {}
ed.ptrs[e.pointerId] = { x: edX(e) };
var ids = Object.keys(ed.ptrs);
if (ids.length === 2) {
var a = ed.ptrs[ids[0]].x, b = ed.ptrs[ids[1]].x; ed.drag = null;
ed.pinch = { d0: Math.abs(b - a) || 1, spp0: E.view.spp, mid0: (a + b) / 2, s0: edSample((a + b) / 2) };
return;
}
var s = edSample(edX(e));
if (e.shiftKey && (E.sel || E.cursor != null)) { var anchor = E.sel ? (Math.abs(s - E.sel[0]) < Math.abs(s - E.sel[1]) ? E.sel[1] : E.sel[0]) : E.cursor; ed.drag = { anchor: anchor, x0: edX(e), moved: true }; E.sel = [Math.min(anchor, s), Math.max(anchor, s)]; }
else ed.drag = { anchor: s, x0: edX(e), moved: false };
edDraw();
});
edC.addEventListener('pointermove', function (e) {
if (!E.take || !ed.ptrs[e.pointerId]) return;
ed.ptrs[e.pointerId].x = edX(e);
var ids = Object.keys(ed.ptrs);
if (ed.pinch && ids.length >= 2) {
var a = ed.ptrs[ids[0]].x, b = ed.ptrs[ids[1]].x, d = Math.abs(b - a) || 1, mid = (a + b) / 2;
E.view.spp = clamp(ed.pinch.spp0 * ed.pinch.d0 / d, 1 / 32, maxSpp()); E.view.start = ed.pinch.s0 - mid * E.view.spp;
clampView(); edDraw(); edStatus(); return;
}
if (!ed.drag) return;
var x = edX(e); if (Math.abs(x - ed.drag.x0) > 4) ed.drag.moved = true;
if (ed.drag.moved) {
var s = edSample(x); E.sel = [Math.min(ed.drag.anchor, s), Math.max(ed.drag.anchor, s)];
var w = edC.width; if (x > w - 10) E.view.start += E.view.spp * 12; else if (x < 10) E.view.start -= E.view.spp * 12; // drag past the edge scrolls
clampView(); edDraw(); edStatus();
}
});
function edUp(e) {
delete ed.ptrs[e.pointerId];
if (ed.pinch) { if (Object.keys(ed.ptrs).length < 2) ed.pinch = null; return; }
var d = ed.drag; ed.drag = null; if (!d || !E.take) return;
if (!d.moved) { E.sel = null; E.cursor = Math.round(d.anchor); if (P.mode === 'take') { au.currentTime = E.cursor / E.take.sampleRate; E.playEnd = null; } }
else if (E.sel) { E.sel = [Math.round(E.sel[0]), Math.round(E.sel[1])]; E.cursor = E.sel[0]; }
edDraw(); edStatus();
}
edC.addEventListener('pointerup', edUp); edC.addEventListener('pointercancel', edUp);
edC.addEventListener('wheel', function (e) {
if (!E.take) return; e.preventDefault();
var x = edX(e);
if (e.shiftKey || Math.abs(e.deltaX) > Math.abs(e.deltaY)) { E.view.start += (e.deltaX || e.deltaY) * E.view.spp * (window.devicePixelRatio || 1); clampView(); edDraw(); edStatus(); }
else zoomAt(Math.exp(-e.deltaY * (e.deltaMode === 1 ? 0.05 : 0.0025)), x);
}, { passive: false });
edC.addEventListener('keydown', function (e) {
if (!E.take) return;
var k = e.key, mod = e.ctrlKey || e.metaKey, act = null;
if (mod && (k === 'z' || k === 'Z')) act = e.shiftKey ? 'redo' : 'undo';
else if (mod && (k === 'y' || k === 'Y')) act = 'redo';
else if (mod && k === 'x') act = 'cut'; else if (mod && k === 'c') act = 'copy'; else if (mod && k === 'v') act = 'paste';
else if (mod && k === 't') act = 'trim';
else if (mod && k === 'a') { E.sel = [0, E.take.length]; edDraw(); edStatus(); e.preventDefault(); return; }
else if (!mod && (k === 'Delete' || k === 'Backspace')) act = 'del';
else if (!mod && k === ' ') act = (P.mode === 'take' && !au.paused) ? 'stop' : 'play';
else if (!mod && (k === '+' || k === '=')) act = 'zoomIn'; else if (!mod && (k === '-' || k === '_')) act = 'zoomOut';
else if (!mod && (k === 'f' || k === 'F')) act = 'fit'; else if (!mod && (k === 'e' || k === 'E')) act = 'zoomSel';
else if (!mod && (k === 's' || k === 'S')) act = 'zoomSample'; else if (!mod && (k === 'm' || k === 'M')) act = 'marker';
else if (!mod && (k === 'l' || k === 'L')) act = 'loop';
else if (!mod && k === 'Escape') { E.sel = null; edDraw(); edStatus(); return; }
else if (!mod && (k === 'ArrowLeft' || k === 'ArrowRight')) { E.view.start += (k === 'ArrowLeft' ? -1 : 1) * edC.width * E.view.spp * 0.1; clampView(); edDraw(); edStatus(); e.preventDefault(); return; }
if (act) { e.preventDefault(); runEd(act); }
});
function niceStep(secPerPx) {
var target = secPerPx * 90, steps = [0.00005, 0.0001, 0.0002, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600];
for (var i = 0; i < steps.length; i++) if (steps[i] >= target) return steps[i];
return 1200;
}
function edDraw() {
var w = edW(), h = edC.height, g = edC.getContext('2d'), dpr = w / (edC.clientWidth || 1) || 1, rh = RULER * dpr;
g.fillStyle = COL.canvas || '#000'; g.fillRect(0, 0, w, h);
var t = E.take;
g.font = (10 * dpr) + 'px "IBM Plex Mono", monospace'; g.textBaseline = 'top';
if (!t) { g.fillStyle = COL.text3; g.fillText('open a take above, or drag a span on the overview', 10 * dpr, h / 2 - 6 * dpr); return; }
var v = E.view, sr = t.sampleRate, spp = v.spp, s0 = v.start;
// ruler
var step = niceStep(spp / sr), first = Math.ceil(s0 / sr / step) * step;
g.strokeStyle = COL.grid; g.fillStyle = COL.text3; g.beginPath();
for (var ts = first; (ts * sr - s0) / spp < w; ts += step) {
var x = Math.round((ts * sr - s0) / spp) + 0.5; g.moveTo(x, 0); g.lineTo(x, h);
var dp = step < 0.001 ? 4 : step < 0.01 ? 3 : step < 1 ? 2 : 0;
g.fillText(fmtT(ts + (E.origin && E.origin.doc && !E.edited ? E.origin.t0 : 0), dp), x + 3 * dpr, 2 * dpr);
}
g.stroke();
// selection
if (E.sel) { var xa = (E.sel[0] - s0) / spp, xb = (E.sel[1] - s0) / spp; g.fillStyle = COL.sel; g.fillRect(xa, rh, Math.max(1, xb - xa), h - rh); }
// waveform, one lane per channel
var nch = t.channels.length, laneH = (h - rh) / nch;
for (var c = 0; c < nch; c++) {
var d = t.channels[c], mid = rh + laneH * (c + 0.5), amp = laneH * 0.46;
g.strokeStyle = COL.grid; g.beginPath(); g.moveTo(0, Math.round(mid) + 0.5); g.lineTo(w, Math.round(mid) + 0.5); g.stroke();
g.fillStyle = COL.wave; g.strokeStyle = COL.wave;
if (spp >= 1) {
var pk = E.peaks && E.peaks[c];
for (var px = 0; px < w; px++) {
var a = s0 + px * spp, b = a + spp; if (a >= d.length) break;
var lo = 1, hi = -1;
if (spp >= 512 && pk) { for (var k = Math.floor(a / 256), ke = Math.min(pk.min.length, Math.ceil(b / 256)); k < ke; k++) { if (pk.min[k] < lo) lo = pk.min[k]; if (pk.max[k] > hi) hi = pk.max[k]; } }
else { for (var i = Math.floor(a), ie = Math.min(d.length, Math.ceil(b)); i < ie; i++) { var val = d[i]; if (val < lo) lo = val; if (val > hi) hi = val; } }
if (hi < lo) continue;
var y0 = mid - hi * amp, y1 = mid - lo * amp; g.fillRect(px, y0, 1, Math.max(1, y1 - y0));
}
} else {
// sample level: a line through the samples, each one a dot
var i0 = Math.max(0, Math.floor(s0)), i1 = Math.min(d.length - 1, Math.ceil(s0 + w * spp));
g.lineWidth = 1 * dpr; g.beginPath();
for (var j = i0; j <= i1; j++) { var xx = (j - s0) / spp, yy = mid - d[j] * amp; if (j === i0) g.moveTo(xx, yy); else g.lineTo(xx, yy); }
g.stroke();
if (1 / spp >= 6) { var r = Math.min(3 * dpr, 0.25 / spp); for (var q = i0; q <= i1; q++) { g.beginPath(); g.arc((q - s0) / spp, mid - d[q] * amp, r, 0, 6.283); g.fill(); } }
}
}
// markers
g.strokeStyle = COL.marker; g.fillStyle = COL.marker;
E.markers.forEach(function (m) { var x = Math.round((m.s - s0) / spp) + 0.5; if (x < 0 || x > w) return; g.beginPath(); g.moveTo(x, rh); g.lineTo(x, h); g.stroke(); g.fillText(m.label, x + 3 * dpr, rh + 2 * dpr); });
// cursor
var cx = Math.round((E.cursor - s0) / spp) + 0.5; g.strokeStyle = COL.text2; g.setLineDash([2 * dpr, 3 * dpr]); g.beginPath(); g.moveTo(cx, rh); g.lineTo(cx, h); g.stroke(); g.setLineDash([]);
edPlayhead(g, w, h, rh);
}
var lastPlayX = null;
function edPlayhead(g, w, h, rh) {
var t = E.take; if (!t) return;
var pos = null;
if (P.mode === 'take') pos = au.currentTime * t.sampleRate;
else if (E.origin && E.origin.doc === (P.doc && P.doc.name) && E.origin.voice === P.voice && !E.edited) { var T = docTime(); if (T >= E.origin.t0 && T <= E.origin.t1) pos = (T - E.origin.t0) * t.sampleRate; }
if (pos == null) return;
var x = (pos - E.view.start) / E.view.spp; if (x < 0 || x > w) return;
g.strokeStyle = COL.cursor; g.lineWidth = 1.5; g.beginPath(); g.moveTo(x, rh); g.lineTo(x, h); g.stroke(); g.lineWidth = 1;
}
// ---------------------------------------------------------------- the frame loop
var frameN = 0, lastClock = '';
function frame() {
requestAnimationFrame(frame);
if (document.hidden) return;
frameN++;
var st = vizState();
drawScope(st); drawSpectrum(st); drawSgram(st);
pixelFrame(); micFrame(); camFrame(); mapFrame(); takeFrame();
if (P.doc) {
var T = P.mode === 'doc' ? docTime() : au.currentTime || 0, D = P.mode === 'doc' ? P.total : (E.take ? E.take.length / E.take.sampleRate : 0);
var clk = (P.mode === 'take' ? 'take · ' : '') + fmtT(T) + ' / ' + fmtT(D);
if (clk !== lastClock) { $('t-clock').textContent = clk; lastClock = clk; }
if (frameN % 4 === 0) drawOverview();
}
if (E.take && !au.paused) edDraw();
if (frameN % 4 === 0) { paintMeters(); paintMapNow(); }
if (frameN % 15 === 0) {
$('viz-state').textContent = st === 'live' ? 'live: every pixel above is measured from the analyser after the gain stage' + (MIC ? ' (the microphone feeds only its own meters, never the speakers)' : '')
: st === 'resting' ? 'resting: the analyser is attached and the audio is paused, so the scope is flat and the spectrogram holds still'
: st === 'suspended' ? 'the audio context is suspended: press play' : 'idle: press play. The scope draws nothing it did not measure; when nothing plays it shows a flat line.';
var outG = K.values.vol;
$('out-note').textContent = 'output gain ' + Math.round(outG * 100) + '% ' + (outG > 1 ? '(' + (20 * Math.log10(outG)).toFixed(1) + ' dB over the file, carried by a GainNode)' : 'of the file') + (A ? ' · ' + A.ctx.sampleRate + ' Hz context' : '');
}
}
// ---------------------------------------------------------------- your data: inventory · clear · shred
// Everything the page keeps, counted from the live objects (not from a list of what it meant to keep).
var OWN = 'mindx.studio';
function owns(k) { return typeof k === 'string' && k.indexOf(OWN) === 0; }
function mb(n) { return n >= 1048576 ? (n / 1048576).toFixed(2) + ' MB' : n >= 1024 ? (n / 1024).toFixed(1) + ' KB' : n + ' B'; }
function lsKeys(store) { var out = []; try { for (var i = 0; i < store.length; i++) out.push(store.key(i)); } catch (e) {} return out; }
function lsBytes(store, k) { try { var v = store.getItem(k) || ''; return (k.length + v.length) * 2; } catch (e) { return 0; } } // UTF-16, as the browser counts the quota
function idbOwned() {
try { if (!window.indexedDB || !indexedDB.databases) return Promise.resolve(null); return indexedDB.databases().then(function (l) { return l.filter(function (d) { return owns(d.name); }).map(function (d) { return d.name; }); }, function () { return null; }); }
catch (e) { return Promise.resolve(null); }
}
function cachesOwned() {
try { if (!window.caches) return Promise.resolve(null); return caches.keys().then(function (l) { return l.filter(owns); }, function () { return null; }); }
catch (e) { return Promise.resolve(null); }
}
function abBytes(x) { return x ? x.byteLength : 0; }
function chBytes(chs) { return (chs || []).reduce(function (a, c) { return a + abBytes(c); }, 0); }
function snapsBytes(list) { return list.reduce(function (a, sn) { return a + chBytes(sn.take && sn.take.channels); }, 0); }
function decBytes() { return Object.keys(DEC.cache).reduce(function (a, k) { var b = DEC.cache[k]; return a + b.length * b.numberOfChannels * 4; }, 0); }
function rawBytes() { return Object.keys(bytesCache).reduce(function (a, k) { return a + abBytes(bytesCache[k]); }, 0); }
function urlBytes(exceptTake) { return Object.keys(OBJURLS).reduce(function (a, u) { return a + (exceptTake && u === E.wavUrl ? 0 : OBJURLS[u]); }, 0); }
function sgPixels() { return SG.off ? SG.off.width * SG.off.height * 4 : 0; }
// every typed array / ArrayBuffer the page holds audio or sense data in, each once
function ownedArrays() {
var seen = new Set(), out = [];
function add(kind, x) {
if (!x) return; var buf = x.buffer || x; if (!buf.byteLength || seen.has(buf)) return; seen.add(buf);
out.push({ kind: kind, view: new Uint8Array(buf, x.byteOffset || 0, x.byteLength) });
}
Object.keys(bytesCache).forEach(function (k) { add('downloaded audio bytes', bytesCache[k]); });
Object.keys(DEC.cache).forEach(function (k) { var b = DEC.cache[k]; for (var c = 0; c < b.numberOfChannels; c++) add('decoded parts', b.getChannelData(c)); });
if (OV.peaks) { add('overview peaks', OV.peaks.min); add('overview peaks', OV.peaks.max); }
if (E.take) E.take.channels.forEach(function (c) { add('edited take', c); });
E.undo.concat(E.redo).forEach(function (sn) { (sn.take ? sn.take.channels : []).forEach(function (c) { add('undo / redo history', c); }); });
if (E.clipboard) E.clipboard.channels.forEach(function (c) { add('clipboard', c); });
(E.peaks || []).forEach(function (pk) { add('editor peaks', pk.min); add('editor peaks', pk.max); });
if (MIC) { add('microphone buffers', MIC.td); add('microphone buffers', MIC.fq); add('microphone buffers', MIC.prev); }
if (CAM) add('camera buffers', CAM.prev);
if (A) { add('analyser buffers', A.f32); add('analyser buffers', A.fq); }
return out;
}
function inventory() {
var rows = [];
function row(where, what, bytes, clear, opts) { rows.push({ where: where, what: what, bytes: bytes, clear: clear, empty: !!(opts && opts.empty), info: !!(opts && opts.info) }); }
// storage
var lk = lsKeys(window.localStorage || { length: 0 }).filter(owns);
if (lk.length) lk.forEach(function (k) { row('localStorage', k + ' (knob values, mappings, theme, pitch lock)', lsBytes(localStorage, k), function () { removeStored(k); }); });
else row('localStorage', 'none', 0, null, { empty: true });
var sk = []; try { sk = lsKeys(sessionStorage).filter(owns); } catch (e) {}
if (sk.length) sk.forEach(function (k) { row('sessionStorage', k, lsBytes(sessionStorage, k), function () { try { sessionStorage.removeItem(k); } catch (e) {} }); });
else row('sessionStorage', 'none', 0, null, { empty: true });
var others = lsKeys(window.localStorage || { length: 0 }).filter(function (k) { return !owns(k); }).length;
if (others) row('localStorage', others + ' other key' + (others > 1 ? 's' : '') + ' on this origin, not written by the studio: left alone', 0, null, { info: true });
var ck = document.cookie ? document.cookie.split(';').length : 0;
row('cookies', ck ? ck + ' on this origin, not set by this page: left alone' : 'none (this page sets none)', 0, null, { empty: !ck, info: !!ck });
// memory
var raw = Object.keys(bytesCache).length;
row('memory', raw ? raw + ' downloaded audio file' + (raw > 1 ? 's' : '') + ' (Ogg Opus bytes)' : 'downloaded audio: none', rawBytes(), raw ? function () { dropRaw(); } : null, { empty: !raw });
var dc = Object.keys(DEC.cache).length;
row('memory', dc ? dc + ' decoded part' + (dc > 1 ? 's' : '') + ' (kept for the editor)' : 'decoded parts: none', decBytes(), dc ? function () { dropDecoded(); } : null, { empty: !dc });
row('memory', OV.peaks ? 'overview peaks' : 'overview peaks: none', OV.peaks ? abBytes(OV.peaks.min) * 2 : 0, OV.peaks ? function () { OV.token++; OV.peaks = null; drawOverview(); $('ov-status').textContent = 'overview cleared · choose the document again to redraw it'; } : null, { empty: !OV.peaks });
row('memory', E.take ? 'edited take: ' + fmtT(E.take.length / E.take.sampleRate, 2) + (E.edited ? ', edited' : '') + (E.peaks ? ' (+ its peaks)' : '') : 'edited take: none', E.take ? chBytes(E.take.channels) + (E.peaks ? E.peaks.reduce(function (a, p) { return a + abBytes(p.min) * 2; }, 0) : 0) : 0, E.take ? function () { dropTake(); } : null, { empty: !E.take });
row('memory', E.undo.length + E.redo.length ? 'edit history: ' + E.undo.length + ' undo, ' + E.redo.length + ' redo' : 'edit history: none', snapsBytes(E.undo) + snapsBytes(E.redo), E.undo.length + E.redo.length ? function () { E.undo = []; E.redo = []; edStatus(); } : null, { empty: !(E.undo.length + E.redo.length) });
row('memory', E.clipboard ? 'clipboard: ' + fmtT(E.clipboard.length / E.clipboard.sampleRate, 3) : 'clipboard: none', E.clipboard ? chBytes(E.clipboard.channels) : 0, E.clipboard ? function () { E.clipboard = null; } : null, { empty: !E.clipboard });
row('memory', E.markers.length ? E.markers.length + ' marker' + (E.markers.length > 1 ? 's' : '') : 'markers: none', E.markers.length * 16, E.markers.length ? function () { E.markers = []; renderMarkers(); edDraw(); } : null, { empty: !E.markers.length });
var urls = Object.keys(OBJURLS), takeUrl = E.wavUrl && OBJURLS[E.wavUrl] != null;
row('memory', takeUrl ? 'the take as WAV, for playback (object URL)' : 'take playback WAV: none', takeUrl ? OBJURLS[E.wavUrl] : 0, takeUrl ? function () { dropTakeAudio(); } : null, { empty: !takeUrl });
var ex = urls.filter(function (u) { return u !== E.wavUrl; });
row('memory', ex.length ? ex.length + ' exported WAV' + (ex.length > 1 ? 's' : '') + ' still referenced (object URL, revoked after 60 s)' : 'exports still referenced: none', urlBytes(true), ex.length ? function () { ex.forEach(rmURL); } : null, { empty: !ex.length });
var sgHeld = SG.off && SG.cols > 0;
row('memory', sgHeld ? 'spectrogram picture (' + SG.off.width + ' × ' + SG.off.height + ', ' + Math.min(SG.cols, SG.off.width) + ' columns of what played)' : 'spectrogram picture: none (blank)', sgHeld ? sgPixels() : 0, sgHeld ? function () { wipeCanvases(); } : null, { empty: !sgHeld });
// devices
row('device', MIC ? 'microphone: ON (analysed here, never recorded)' : 'microphone: off', MIC ? abBytes(MIC.td) + abBytes(MIC.fq) + abBytes(MIC.prev) : 0, MIC ? micOff : null, { empty: !MIC });
row('device', CAM ? 'camera: ON (64 × 48 per frame, never recorded)' : 'camera: off', CAM ? abBytes(CAM.prev) + 64 * 48 * 4 : 0, CAM ? camOff : null, { empty: !CAM });
if (A) row('memory', 'analyser working buffers: the last ' + A.f32.length + ' samples and spectrum of what is playing, refilled every frame while audio plays (overwritten by SHRED)', abBytes(A.f32) + abBytes(A.fq), null, { info: true });
row('browser', 'the browser’s own HTTP cache of the audio files and the audio element’s buffers: this page cannot see or clear them; your browser’s "clear browsing data" can', 0, null, { info: true });
return Promise.all([idbOwned(), cachesOwned()]).then(function (r) {
var idb = r[0], cs = r[1];
if (idb === null) row('IndexedDB', 'cannot be listed in this browser; the studio never opens one', 0, null, { empty: true });
else if (idb.length) idb.forEach(function (n) { row('IndexedDB', n, 0, function () { return delIDB(n); }); });
else row('IndexedDB', 'none', 0, null, { empty: true });
if (cs === null) row('Cache Storage', 'not available here; the studio never opens one', 0, null, { empty: true });
else if (cs.length) cs.forEach(function (n) { row('Cache Storage', n, 0, function () { return caches['delete'](n); }); });
else row('Cache Storage', 'none', 0, null, { empty: true });
return rows;
});
}
function renderInventory() {
return inventory().then(function (rows) {
var tb = document.querySelector('#inv-table tbody'); while (tb.firstChild) tb.removeChild(tb.firstChild);
var total = 0, held = 0;
rows.forEach(function (r) {
var tr = el('tr', r.empty ? 'empty' : r.info ? 'info' : '');
tr.appendChild(el('td', null, r.where)); tr.appendChild(el('td', null, r.what)); tr.appendChild(el('td', 'sz', r.bytes ? mb(r.bytes) : r.empty ? '—' : ''));
var td = el('td');
if (r.clear) { var b = el('button', null, 'clear'); b.type = 'button'; b.addEventListener('click', function () { Promise.resolve(r.clear()).then(function () { return renderInventory(); }); }); td.appendChild(b); held++; }
tr.appendChild(td); tb.appendChild(tr); if (!r.info) total += r.bytes || 0;
});
$('inv-total').textContent = held ? held + ' item' + (held > 1 ? 's' : '') + ' held · ' + mb(total) + ' in all' : 'nothing held: no stored keys, no buffers, no devices open';
$('inv-when').textContent = 'inventory taken ' + new Date().toLocaleTimeString();
return { rows: rows, held: held, bytes: total };
});
}
// clearing
function removeStored(k) { clearTimeout(saveTimer); try { localStorage.removeItem(k); } catch (e) {} if (k === LS_KEY) resetSettings(); }
function resetSettings() {
// what you see matches what is stored, which is now nothing; nothing is written back until you change something
clearTimeout(saveTimer); saved = {};
KNOBS.forEach(function (k) { K.values[k.id] = k.def; K.maps[k.id] = { src: '', lo: 0, hi: 1, inv: false, smooth: 0.8 }; K.smooth[k.id] = null; K.live[k.id] = false; applyKnob(k.id); });
$('pitch-lock').checked = true; applyPitchLock();
document.documentElement.removeAttribute('data-theme'); readColors(); drawOverview(); edDraw();
syncMapRows(); notify();
}
function dropRaw() { Object.keys(bytesCache).forEach(function (k) { delete bytesCache[k]; }); }
function dropDecoded() { DEC.cache = {}; DEC.order = []; DEC.bytes = 0; }
function dropTakeAudio() {
if (P.mode === 'take') { au.pause(); var T = P.lastDocT; P.mode = 'doc'; au.removeAttribute('data-src'); au.removeAttribute('src'); try { au.load(); } catch (e) {} if (P.doc) seekDoc(T, false); }
if (E.wavUrl) rmURL(E.wavUrl); E.wavUrl = null; E.wavDirty = true;
}
function dropTake() {
dropTakeAudio();
E.take = null; E.peaks = null; E.undo = []; E.redo = []; E.clipboard = null; E.markers = []; E.sel = null; E.cursor = 0; E.edited = false; E.origin = null;
renderMarkers(); edDraw(); edStatus(); drawOverview(); say('the take is gone from memory');
}
function wipeCanvases() {
var n = 0;
[SG.off, scopeC, specC, sgC].forEach(function (c) { if (!c) return; var g = c.getContext('2d'); g.fillStyle = COL.canvas; g.fillRect(0, 0, c.width, c.height); n++; });
SG.cols = 0;
return n;
}
function delIDB(name) { return new Promise(function (res) { try { var r = indexedDB.deleteDatabase(name); r.onsuccess = r.onerror = r.onblocked = function () { res(); }; } catch (e) { res(); } }); }
function clearAll() {
GEN++; DEC.inflight = {};
au.pause(); // playback would refill the analyser and the spectrogram at once
if (MIC) micOff(); if (CAM) camOff();
dropTake(); dropRaw(); dropDecoded();
Object.keys(OBJURLS).forEach(rmURL);
OV.token++; OV.peaks = null; drawOverview(); $('ov-status').textContent = 'overview cleared · choose the document again to redraw it';
wipeCanvases(); SG.off = null;
S.pixel.active = false; S.pixel._canvas = null;
var stored = lsKeys(window.localStorage || { length: 0 }).filter(owns), sess = []; try { sess = lsKeys(sessionStorage).filter(owns); } catch (e) {}
stored.forEach(function (k) { try { localStorage.removeItem(k); } catch (e) {} });
sess.forEach(function (k) { try { sessionStorage.removeItem(k); } catch (e) {} });
resetSettings();
return Promise.all([idbOwned(), cachesOwned()]).then(function (r) {
return Promise.all((r[0] || []).map(delIDB).concat((r[1] || []).map(function (n) { return caches['delete'](n); })))
.then(function () { return { localStorage: stored, sessionStorage: sess, indexedDB: r[0] || [], cacheStorage: r[1] || [] }; });
});
}
$('inv-refresh').addEventListener('click', renderInventory);
$('clear-all').addEventListener('click', function () {
clearAll().then(renderInventory).then(function (inv) { $('inv-when').textContent = 'cleared · inventory taken again ' + new Date().toLocaleTimeString() + (inv.held ? ' · ' + inv.held + ' still held' : ' · empty'); });
});
// shredding
function randFill(view) {
var c = window.crypto || window.msCrypto;
for (var i = 0; i < view.length; i += 65536) c.getRandomValues(view.subarray(i, Math.min(view.length, i + 65536)));
}
function randString(n) {
var b = new Uint8Array(n), out = ''; randFill(b);
for (var i = 0; i < n; i++) out += String.fromCharCode(33 + (b[i] % 94));
return out;
}
function sha256hex(text) {
if (!window.crypto || !crypto.subtle) return Promise.resolve(null);
return crypto.subtle.digest('SHA-256', new TextEncoder().encode(text)).then(function (d) { return Array.prototype.map.call(new Uint8Array(d), function (x) { return ('0' + x.toString(16)).slice(-2); }).join(''); });
}
function shred() {
var t0 = performance.now(), byKind = {}, arrays = 0, bytes = 0;
// 1. every in-memory buffer: zeros, then random
ownedArrays().forEach(function (a) {
a.view.fill(0); randFill(a.view);
arrays++; bytes += a.view.byteLength;
var k = byKind[a.kind] || (byKind[a.kind] = { arrays: 0, bytes: 0 }); k.arrays++; k.bytes += a.view.byteLength;
});
// 2. every owned stored value: random of equal length, then removed (by clearAll)
var lsOver = [], lsBytesN = 0;
[['localStorage', window.localStorage], ['sessionStorage', window.sessionStorage]].forEach(function (pair) {
try {
lsKeys(pair[1]).filter(owns).forEach(function (k) { var v = pair[1].getItem(k) || ''; pair[1].setItem(k, randString(v.length)); lsOver.push(pair[0] + ':' + k); lsBytesN += v.length * 2; });
} catch (e) {}
});
var urls = Object.keys(OBJURLS).length, devices = [];
if (MIC) devices.push('microphone'); if (CAM) devices.push('camera');
clearTimeout(saveTimer);
// 3. release it all, delete IDB / Cache Storage, revoke URLs, wipe the pictures
return clearAll().then(function (deleted) {
return renderInventory().then(function (inv) {
var leftLS = lsKeys(window.localStorage || { length: 0 }).filter(owns).length;
return Promise.all([idbOwned(), cachesOwned()]).then(function (r) {
var receipt = {
page: 'mindX studio ' + VERSION + ' · ' + location.origin + location.pathname,
at: new Date().toISOString(),
overwritten: { method: 'zeros, then crypto.getRandomValues', arrays: arrays, bytes: bytes, byKind: byKind },
storage: { overwrittenWithRandom: lsOver, bytes: lsBytesN, removed: deleted.localStorage.map(function (k) { return 'localStorage:' + k; }).concat(deleted.sessionStorage.map(function (k) { return 'sessionStorage:' + k; })) },
deleted: { indexedDB: deleted.indexedDB, cacheStorage: deleted.cacheStorage },
objectURLsRevoked: urls,
devicesStopped: devices,
canvasesWiped: 4,
verified: { ownedStorageKeys: leftLS, ownedIndexedDB: r[0] === null ? 'not listable' : r[0].length, ownedCaches: r[1] === null ? 'not available' : r[1].length, itemsStillHeld: inv.held, bytesStillHeld: inv.bytes, objectURLs: Object.keys(OBJURLS).length },
tookMs: Math.round(performance.now() - t0),
limit: 'best-effort over what the page owns: the browser and the disk (SSD wear levelling, browser internals, the audio element and HTTP cache, OS swap) can keep copies the page cannot reach; Blob contents cannot be overwritten by a page, only revoked'
};
var json = JSON.stringify(receipt, null, 2);
$('receipt').hidden = false; $('receipt-json').textContent = json;
return sha256hex(json).then(function (h) {
$('receipt-hash').textContent = h ? 'sha256 of the receipt JSON above: ' + h : 'sha256 unavailable here (not a secure context)';
window.__lastShred = { receipt: receipt, sha256: h, json: json };
return window.__lastShred;
});
});
});
});
}
$('shred-ask').addEventListener('click', function () { $('shred-confirm').hidden = false; $('shred-yes').focus(); });
$('shred-no').addEventListener('click', function () { $('shred-confirm').hidden = true; });
$('shred-yes').addEventListener('click', function () {
$('shred-confirm').hidden = true; $('inv-when').textContent = 'shredding…';
shred().then(function () { $('receipt').scrollIntoView({ behavior: 'smooth', block: 'start' }); $('inv-when').textContent = 'shredded · inventory taken again ' + new Date().toLocaleTimeString(); });
});
setInterval(function () { if (!document.hidden && $('shred-confirm').hidden) renderInventory(); }, 3000);
// ---------------------------------------------------------------- public API (the rack island reads this)
window.mindxStudio = {
version: VERSION,
knobs: function () {
return KNOBS.map(function (k) {
var m = K.maps[k.id];
return { id: k.id, label: k.label, sec: k.sec, kind: k.kind || 'knob', min: k.min, max: k.max, step: k.step, def: k.def, taper: k.taper, origin: k.origin, detents: k.detents,
sub: k.sub || '', value: K.values[k.id], text: k.fmt(K.values[k.id]), mapped: m.src || null, live: !!K.live[k.id] };
});
},
setKnob: function (id, v) { setKnob(id, v, 'hand'); },
focusMapping: focusMapping,
colors: function () { return COL; },
senses: S,
state: function () {
return { mode: P.mode, doc: P.doc && P.doc.name, voice: P.voice, docTime: docTime(), total: P.total, start: startOf(), paused: au.paused, part: P.part,
take: E.take ? { length: E.take.length, sampleRate: E.take.sampleRate, duration: E.take.length / E.take.sampleRate, channels: E.take.channels.length, edited: E.edited, view: { start: E.view.start, spp: E.view.spp, seconds: edW() * E.view.spp / E.take.sampleRate }, sel: E.sel, markers: E.markers.length, undo: E.undo.length } : null,
overview: OV.peaks ? { upto: OV.peaks.upto / OV_RATE } : null, ctx: A ? A.ctx.state : null };
},
data: { inventory: renderInventory, clearAll: function () { return clearAll().then(renderInventory); }, shred: shred },
editor: { run: runEd, select: function (a, b) { if (!E.take) return; E.sel = [Math.round(a * E.take.sampleRate), Math.round(b * E.take.sampleRate)]; edDraw(); edStatus(); }, zoom: zoomAt, loadSpan: function (a, b) { loadSpan(a, b); } }
};
// ---------------------------------------------------------------- boot
if (saved.theme) document.documentElement.setAttribute('data-theme', saved.theme);
readColors();
window.addEventListener('resize', function () { if (E.take) { clampView(); edDraw(); } drawOverview(); });
fetch('audio/index.json', { cache: 'no-cache' }).then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }).then(function (idx) {
Object.keys(idx).forEach(function (n) { var d = normDoc(n, idx[n]); if (d) { DOCS[n] = d; DOC_ORDER.push(n); } });
if (!DOC_ORDER.length) throw new Error('no playable documents in the index');
var q = new URLSearchParams(location.search), want = (q.get('doc') || '').toUpperCase(), t = q.get('t');
selectDoc(DOCS[want] ? want : DOC_ORDER[0], q.get('voice'), t != null && isFinite(+t) ? +t : null);
edDraw();
}).catch(function (e) {
$('ov-status').textContent = 'audio/index.json did not load (' + (e && e.message || e) + '). The editor still opens files from this device.';
edDraw();
});
renderInventory();
requestAnimationFrame(frame);
})();