Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527// dj.mjs — Turntable piece for AC Native// One big record, one track, live scratching with mouse/touchpad.// Auto-scans USB for audio files. Simple, visual, interactive.
const AUDIO_EXTS = new Set(["mp3", "wav", "flac", "ogg", "aac", "m4a", "opus", "wma"]);let T = __theme.update();
// Statelet files = []; // discovered audio [{path, name}]let trackIdx = 0; // current track indexlet mounted = false;let dragging = false;let dragLastX = 0;let dragLastY = 0;let angle = 0; // visual rotation anglelet message = "";let messageFrame = 0;let frame = 0;let lastUsbCheck = 0;let usbConnected = false;let spinSpeed = 0; // record spin rate (0=stopped, 1=playing)let scratchSpeed = 0; // current scratch velocity (-2 to 2)let wasPlaying = false; // was playing before scratch started
// Waveform overview — whole-track peaks fetched once per load.let peaks = null; // Float array (0..1) or null until decoded
// Hold-space + trackpad-X scratch.let spaceHeld = false; // space key currently downlet spaceScratchActive = false; // X movement exceeded the tap thresholdlet spaceMoved = 0; // accumulated |dx| since space pressedlet scratchPrevX = 0; // last pen.x sampled in simlet wasPlayingBeforeScratch = false;
// Auto-advance guard — advance once per track end, not every frame.let advancedForIdx = -1;
// Button layout (computed in paint, used in act)let buttons = []; // [{x, y, w, h, id, label}]
function isAudio(name) { if (name.startsWith(".")) return false; // skip metadata (._xxx.m4a etc) const dot = name.lastIndexOf("."); return dot >= 0 && AUDIO_EXTS.has(name.slice(dot + 1).toLowerCase());}
function scanDir(system, path, results, depth) { if (depth > 4) return; const listing = system?.listDir?.(path); if (!listing) return; for (const f of listing) { const full = path + "/" + f.name; if (f.isDir && !f.name.startsWith(".")) scanDir(system, full, results, depth + 1); else if (isAudio(f.name)) results.push({ path: full, name: f.name }); }}
function say(sound, text) { sound?.speak?.(text); }
function scan(system, sound) { files = []; const dirs = ["/media", "/mnt/samples", "/mnt"]; for (const d of dirs) scanDir(system, d, files, 0); files.sort((a, b) => a.name.localeCompare(b.name)); say(sound, files.length > 0 ? `${files.length} tracks` : "no tracks"); msg(files.length > 0 ? `${files.length} tracks` : "no tracks found");}
function msg(t) { message = t; messageFrame = frame; }
function fmt(s) { if (!s || s < 0) return "0:00"; return `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, "0")}`;}
function loadTrack(sound) { if (files.length === 0) return; if (trackIdx >= files.length) trackIdx = 0; const f = files[trackIdx]; const ok = sound?.deck?.load(0, f.path); if (ok) { peaks = null; // re-fetch overview for the new track advancedForIdx = -1; // re-arm auto-advance for this track msg(f.name.replace(/\.[^.]+$/, "")); say(sound, f.name.replace(/\.[^.]+$/, "")); sound.deck.play(0); spinSpeed = 1; } else { msg("failed: " + f.name); }}
function boot({ system, sound }) { system?.mountMusic?.(); mounted = !!system?.mountMusicMounted; usbConnected = mounted; scan(system, sound); const d = sound?.deck?.decks?.[0]; if (d?.loaded) { msg("resumed"); if (d.playing) spinSpeed = 1; } else if (files.length > 0) { loadTrack(sound); }}
function act({ event: e, sound, system, screen }) { const dk = sound?.deck; const d = dk?.decks?.[0]; const w = screen?.width || 320; const h = screen?.height || 240;
// --- Button tap check (before scratch) --- if (e.is("touch")) { for (const b of buttons) { if (e.x >= b.x && e.x <= b.x + b.w && e.y >= b.y && e.y <= b.y + b.h) { if (b.id === "play") { if (d?.loaded) { if (d.playing) { dk.pause(0); spinSpeed = 0; msg("paused"); } else { dk.play(0); spinSpeed = 1; msg("playing"); } } } else if (b.id === "next") { trackIdx++; loadTrack(sound); } else if (b.id === "prev") { trackIdx = Math.max(0, trackIdx - 1); loadTrack(sound); } else if (b.id === "reset") { if (d?.loaded) { dk.setSpeed(0, 1); msg("1.00x"); } } else if (b.id === "scan") { system?.mountMusic?.(); mounted = !!system?.mountMusicMounted; usbConnected = mounted; if (mounted) { scan(system, sound); if (files.length > 0) { trackIdx = 0; loadTrack(sound); } } else { msg(system?.mountMusicPending ? "checking usb" : "no usb"); } } return; } } }
// --- Mouse/touchpad scratch (radial around platter center) --- if (e.is("touch")) { dragging = true; dragLastX = e.x; dragLastY = e.y; wasPlaying = d?.playing || false; scratchSpeed = 0; return; } if (e.is("draw") && dragging) { if (d?.loaded) { const cx = (screen?.width || 320) / 2; const cy = (screen?.height || 240) / 2 - 10; // Compute angular change around platter center const prevAngle = Math.atan2(dragLastY - cy, dragLastX - cx); const curAngle = Math.atan2(e.y - cy, e.x - cx); let delta = curAngle - prevAngle; // Normalize to [-PI, PI] if (delta > Math.PI) delta -= Math.PI * 2; if (delta < -Math.PI) delta += Math.PI * 2; // Convert angular delta to speed: positive = forward, negative = reverse scratchSpeed = delta * 8; // sensitivity multiplier // Clamp if (scratchSpeed > 3) scratchSpeed = 3; if (scratchSpeed < -3) scratchSpeed = -3; dk.setSpeed(0, scratchSpeed); if (!d.playing) dk.play(0); angle += delta; dragLastX = e.x; dragLastY = e.y; } return; } if (e.is("lift")) { if (dragging && d?.loaded) { // Release: resume normal speed or stop if (wasPlaying) { dk.setSpeed(0, 1); dk.play(0); } else { dk.setSpeed(0, 1); dk.pause(0); } scratchSpeed = 0; } dragging = false; return; }
// --- Hold-space + trackpad-X scratch --- // Press space to enter scratch mode; sliding the pointer left/right // scrubs the deck (sim() drives the speed from pen.x). A space tap with // no slide falls through to play/pause on release. if (e.is("keyboard:down:space")) { if (!spaceHeld && d?.loaded) { spaceHeld = true; spaceScratchActive = false; spaceMoved = 0; scratchPrevX = -1; // sim seeds this on its first frame wasPlayingBeforeScratch = d.playing || false; } return; // ignore autorepeat; release handles the tap case } if (e.is("keyboard:up:space")) { if (spaceHeld) { spaceHeld = false; if (spaceScratchActive) { // Was scratching — restore prior transport. if (wasPlayingBeforeScratch) { dk?.setSpeed(0, 1); dk?.play(0); spinSpeed = 1; } else { dk?.setSpeed(0, 1); dk?.pause(0); spinSpeed = 0; } } else if (d?.loaded) { // Plain tap — play/pause toggle. if (d.playing) { dk.pause(0); spinSpeed = 0; msg("paused"); } else { dk.play(0); spinSpeed = 1; msg("playing"); } } spaceScratchActive = false; } return; }
if (!e.is("keyboard:down")) return;
if (e.is("keyboard:down:escape")) { system?.jump?.("prompt"); return; }
// N: next track if (e.is("keyboard:down:n")) { trackIdx++; loadTrack(sound); return; } // P: prev track if (e.is("keyboard:down:p")) { trackIdx = Math.max(0, trackIdx - 1); loadTrack(sound); return; }
// R: rescan if (e.is("keyboard:down:r")) { system?.mountMusic?.(); mounted = !!system?.mountMusicMounted; usbConnected = mounted; if (mounted) { scan(system, sound); if (files.length > 0) { trackIdx = 0; loadTrack(sound); } } else { msg(system?.mountMusicPending ? "checking usb" : "no usb"); } return; }
// Arrow keys: speed/stretch (like a pitch fader) if (e.is("keyboard:down:arrowleft")) { if (d?.loaded) dk.setSpeed(0, Math.max(-2, (d.speed || 1) - 0.1)); return; } if (e.is("keyboard:down:arrowright")) { if (d?.loaded) dk.setSpeed(0, Math.min(3, (d.speed || 1) + 0.1)); return; } // Arrow up/down: seek if (e.is("keyboard:down:arrowup")) { if (d?.loaded) dk.seek(0, Math.min(d.duration, d.position + 10)); return; } if (e.is("keyboard:down:arrowdown")) { if (d?.loaded) dk.seek(0, Math.max(0, d.position - 10)); return; }
// Volume if (e.is("keyboard:down:-")) { if (d) dk.setVolume(0, Math.max(0, d.volume - 0.05)); return; } if (e.is("keyboard:down:=")) { if (d) dk.setVolume(0, Math.min(1, d.volume + 0.05)); return; }
// Z: reset speed to 1x if (e.is("keyboard:down:z")) { if (d?.loaded) dk.setSpeed(0, 1); msg("1.00x"); return; }}
function paint({ wipe, ink, box, line, write, circle, screen, sound }) { frame++; T = __theme.update(); const w = screen.width, h = screen.height; const F = "font_1"; const CW = 6; wipe(T.bg[0], T.bg[1], T.bg[2]);
const d = sound?.deck?.decks?.[0] || {};
// --- Big turntable --- const cx = Math.floor(w / 2); const cy = Math.floor(h / 2) - 10; const r = Math.min(cx - 8, cy - 16);
// Rotate angle — follows playback speed (including negative for reverse scratch) if (d.playing && !dragging) { angle += (d.speed || 1) * 0.05; }
// Theme-derived helpers const fg = T.fg || 200; const dim = T.fgMute || 60; const s = T.dark ? 1 : -1; // sign: +brightness for dark, -brightness for light
// Platter ink(T.bg[0] + 8 * s, T.bg[1] + 8 * s, T.bg[2] + 12 * s); circle(cx, cy, r, true);
// Grooves for (let g = Math.floor(r * 0.3); g < r; g += 2) { const b = dragging ? 10 : 6; ink(T.bg[0] + b * s, T.bg[1] + b * s, T.bg[2] + (b + 4) * s); circle(cx, cy, g, false); }
// Rim ink(T.border[0], T.border[1], T.border[2]); circle(cx, cy, r, false);
// Label (center area) const labelR = Math.floor(r * 0.25); const lf = T.dark ? 1 : -1; ink(T.bg[0] + 20 * lf, T.bg[1] + 15 * lf, T.bg[2] + 10 * lf); circle(cx, cy, labelR, true); ink(T.bg[0] + 40 * lf, T.bg[1] + 30 * lf, T.bg[2] + 25 * lf); circle(cx, cy, labelR, false);
// Center dot ink(T.accent[0], T.accent[1], T.accent[2]); circle(cx, cy, 3, true);
// Spinning needle line const needleLen = r - 6; const nx = cx + Math.cos(angle) * needleLen; const ny = cy + Math.sin(angle) * needleLen; const nOn = T.ok, nOff = [dim, dim, dim]; const nc = d.playing ? nOn : nOff; ink(nc[0], nc[1], nc[2]); line(cx, cy, Math.floor(nx), Math.floor(ny)); // Needle tip ink(d.playing ? nOn[0] + 40 : dim + 20, d.playing ? nOn[1] + 40 : dim + 20, d.playing ? nOn[2] + 40 : dim + 20); circle(Math.floor(nx), Math.floor(ny), Math.max(2, Math.floor(r * 0.04)), true);
// --- Track info --- const title = d.loaded ? (d.title || "?").replace(/\.[^.]+$/, "") : "no track"; const maxC = Math.floor(w / CW) - 2;
// Title at top ink(fg, fg, fg); write(title.slice(0, maxC), { x: 4, y: 3, size: 1, font: F });
// Track counter + USB (top right) if (files.length > 0) { ink(dim, dim, dim); const tc = `${trackIdx + 1}/${files.length}`; write(tc, { x: w - tc.length * CW - 4, y: 3, size: 1, font: F }); } const uc = usbConnected ? T.ok : [dim, dim, dim]; ink(uc[0], uc[1], uc[2]); write(usbConnected ? "USB" : "---", { x: w - 22, y: 14, size: 1, font: F });
// --- Bottom panel (no overlaps) --- // Layout from bottom up: // h-12: buttons row // h-24: time / speed // h-32: progress bar
// Waveform overview + playhead (falls back to a thin bar until the // whole-track peaks finish decoding). const barY = h - 34; const barW = w - 8; const progress = d.duration > 0 ? d.position / d.duration : 0; if (!peaks && d.loaded) peaks = sound?.deck?.getPeaks?.(0) || null; const playedX = 4 + Math.floor(barW * progress); if (peaks && peaks.length > 0) { const cyW = barY + 2; // vertical center of the strip const HH = 11; // half-height (strip spans ~22px) const pb = d.playing ? T.ok : [dim + 30, dim + 30, dim + 30]; for (let x = 0; x < barW; x++) { const pk = peaks[Math.floor((x / barW) * peaks.length)] || 0; const colH = Math.max(1, Math.floor(pk * HH)); const played = 4 + x < playedX; if (played) ink(pb[0], pb[1], pb[2]); else ink(T.bar[0], T.bar[1], T.bar[2]); box(4 + x, cyW - colH, 1, colH * 2); } // Playhead. ink(T.accent[0], T.accent[1], T.accent[2]); box(Math.min(4 + barW - 1, playedX), cyW - HH, 1, HH * 2); } else { // Decoding (or stream without peaks) — thin progress bar. ink(T.bar[0], T.bar[1], T.bar[2]); box(4, barY, barW, 4); const pb = d.playing ? T.ok : [dim, dim + 20, dim]; ink(pb[0], pb[1], pb[2]); box(4, barY, Math.max(1, Math.floor(barW * progress)), 4); }
// Time + speed ink(T.fgDim, T.fgDim, T.fgDim); write(`${fmt(d.position)} / ${fmt(d.duration)}`, { x: 4, y: barY + 7, size: 1, font: F }); if (d.loaded) { const spd = `${(d.speed || 1).toFixed(2)}x`; write(spd, { x: w - spd.length * CW - 4, y: barY + 7, size: 1, font: F }); }
// --- Button row --- buttons = []; const btnY = h - 14; const btnH = 12; const btnDefs = [ { id: "prev", label: "< Prev" }, { id: "play", label: d.playing ? "Pause" : "Play" }, { id: "next", label: "Next >" }, { id: "reset", label: "1x" }, { id: "scan", label: "Scan" }, ]; const gap = 3; const totalGap = gap * (btnDefs.length - 1); const btnW = Math.floor((w - 8 - totalGap) / btnDefs.length); for (let i = 0; i < btnDefs.length; i++) { const bx = 4 + i * (btnW + gap); const bd = btnDefs[i]; buttons.push({ x: bx, y: btnY, w: btnW, h: btnH, id: bd.id, label: bd.label }); // Button background ink(T.bar[0], T.bar[1], T.bar[2]); box(bx, btnY, btnW, btnH); // Button border ink(T.border[0], T.border[1], T.border[2]); box(bx, btnY, btnW, btnH, "outline"); // Button label (centered) const lx = bx + Math.floor((btnW - bd.label.length * CW) / 2); const ly = btnY + 2; ink(fg, fg, fg); write(bd.label, { x: lx, y: ly, size: 1, font: F }); }
// Scratch state — radial drag or hold-space + trackpad-X. if (dragging || spaceScratchActive) { ink(T.accent[0], T.accent[1], T.accent[2]); write("SCRATCH", { x: cx - 21, y: cy - 5, size: 1, font: F }); } else if (spaceHeld) { ink(T.warn[0], T.warn[1], T.warn[2]); write("scrub ↔", { x: cx - 18, y: cy - 5, size: 1, font: F }); }
// Message toast if (message && frame - messageFrame < 120) { const fade = Math.max(0, 255 - Math.floor((frame - messageFrame) * 2.5)); ink(T.warn[0], T.warn[1], T.warn[2], fade); write(message.slice(0, maxC), { x: 4, y: 16, size: 1, font: F }); }}
function sim({ system, sound, pen }) { const dk0 = sound?.deck; const d0 = dk0?.decks?.[0];
// --- Hold-space + trackpad-X scratch --- // While space is held, the pointer's horizontal velocity becomes the // deck's playback speed: slide right = forward, left = reverse, hold // still = silence. Released in act(). if (spaceHeld && d0?.loaded && pen) { const px = pen.x ?? 0; if (scratchPrevX < 0) scratchPrevX = px; // seed on first frame const dx = px - scratchPrevX; scratchPrevX = px; spaceMoved += Math.abs(dx); if (spaceMoved > 3) spaceScratchActive = true; // past the tap threshold if (spaceScratchActive) { // Map px/frame to speed. ~6px/frame ≈ 1x; clamp to a hard scratch range. let sp = dx / 6; if (sp > 3) sp = 3; if (sp < -3) sp = -3; scratchSpeed = sp; dk0.setSpeed(0, sp); if (!d0.playing) dk0.play(0); // engine must run to render the scrub } }
oldSim({ system, sound });}
function oldSim({ system, sound }) { // USB hot-plug check every 3 seconds while active, or 15 seconds while idle. const mountPending = !!system?.mountMusicPending; const nowMounted = !!system?.mountMusicMounted; const usbCheckFrames = usbConnected ? 180 : 900; if (!mountPending && frame - lastUsbCheck > usbCheckFrames) { lastUsbCheck = frame; system?.mountMusic?.(); } if (nowMounted && !usbConnected) { usbConnected = true; mounted = true; say(sound, "USB DJ on"); scan(system, null); if (files.length > 0) { trackIdx = 0; loadTrack(sound); } } else if (!nowMounted && usbConnected && !mountPending) { usbConnected = false; const dk = sound?.deck; const d = dk?.decks?.[0]; if (d?.playing) { dk.pause(0); spinSpeed = 0; } files = []; say(sound, "USB DJ off"); msg("USB removed"); } // Auto-advance when the track reaches the end. Fires once per track // (advancedForIdx guard) regardless of whether the deck flips `playing` // to false exactly at EOF, and never mid-scratch/seek. Skipped while a // scratch is in progress so scrubbing past the tail doesn't skip tracks. const d = sound?.deck?.decks?.[0]; const scratching = dragging || spaceScratchActive; if ( d?.loaded && d.duration > 0 && !scratching && d.position >= d.duration - 0.25 && advancedForIdx !== trackIdx && files.length > 0 ) { advancedForIdx = trackIdx; // claim this track so we advance only once trackIdx = (trackIdx + 1) % files.length; // wrap at the end of the crate loadTrack(sound); }}
function leave() {}
export { boot, act, paint, sim, leave };