Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

update: rewrite play.mjs as a simple audio player

Replaces the old unused messaging piece with a working audio player
using the stream API. Defaults to reversed Sopra il Silenzio track,
supports arbitrary URLs and asset paths via params.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+182 -61
+182 -61
system/public/aesthetic.computer/disks/play.mjs
··· 1 - // Play, 22.12.10.15.18 2 - // A dramaturgical messaging game for N players. 3 - 4 - /* #region 🏁 todo 5 - - [-] Send / echo message to server using sockets 6 - and leave it printed on the screen near the user's identity. 7 - - [] Add a cooler palette and increase the font size. 8 - - [] Record all messages in a thread so they can be played downloaded as... 9 - (Pick one for now...) 10 - - [] A gif / webp? A movie file with music? 11 - - [] A formatted multi-swipe post? 12 - - [] Rename to something more appropriate before moving on? 13 - - [] Add tiny typing sounds to keyboard? 14 - + Done 15 - - [x] Make sure text input is working nicely! 16 - #endregion */ 17 - 18 - import { TextInput } from "../lib/type.mjs"; 19 - let input; 20 - 21 - async function boot($) { 22 - const { net, store, debug } = $; 23 - //const sesh = await net.session(); // Make a session backend. 24 - //if (debug) console.log("Session:", sesh); 25 - 26 - const id = (await store.retrieve("identity")) || "anon"; // Get user identity. 27 - if (debug) console.log("Identity:", id); 28 - 29 - // TODO: How is it possible to send the identity to the server 30 - // and guaranteee the user is actually who they say they are? 22.12.12.15.31 31 - const server = net.socket((id, type, content) => { 32 - console.log(id, type, content); 33 - }); 34 - 35 - // server.send("msg", "u r in a scary stupid field"); // TODO: Why doesn't this work? 36 - 37 - input = new TextInput($, "What are you up to?"); // Instantiate a text prompt. 38 - } 39 - 40 - // 🧮 Sim(ulate) (Runs once per logic frame (120fps locked)). 41 - function sim($) { 42 - input?.sim($); 43 - } 44 - 45 - // 🎨 Paint (Executes every display frame) 46 - function paint($) { 47 - const { wipe } = $; 48 - wipe(0); 49 - return input?.paint($); 50 - } 51 - 52 - // ✒ Act (Runs once per user interaction) 53 - function act($) { 54 - input?.act($); 55 - } 56 - 57 - export const nohud = true; 58 - export { boot, sim, paint, act }; 59 - 60 - // 📚 Library (Useful functions used throughout the piece) 61 - // ... 1 + // Play, 2025.4.05 2 + // Simple audio player. Usage: play [track-name|url] 3 + 4 + import { createScrubber } from "./common/scrub.mjs"; 5 + 6 + const STREAM_ID = "play"; 7 + const CDN = "https://assets.aesthetic.computer"; 8 + 9 + const KNOWN_TRACKS = { 10 + "sopra-reversed": { 11 + audio: `${CDN}/audio/Sopra%20il%20Silenzio%20-%2010336%20-%202026-02-10%20(reversed-trimmed).mp3`, 12 + title: "Sopra il Silenzio (reversed)", 13 + }, 14 + }; 15 + 16 + let trackTitle, audioUrl; 17 + let isPlaying = false, isLoading = false, hasStarted = false; 18 + let currentTime = 0, duration = 0, bufferedTime = 0; 19 + let scrubber = createScrubber(); 20 + let bars = new Array(32).fill(0); 21 + let frequencyData = []; 22 + let playBtn, scrubBox; 23 + 24 + function boot({ params, send, ui, screen, hud }) { 25 + hud.label("play"); 26 + const param = params[0] || "sopra-reversed"; 27 + 28 + const track = KNOWN_TRACKS[param]; 29 + if (track) { 30 + audioUrl = track.audio; 31 + trackTitle = track.title; 32 + } else if (param.startsWith("http")) { 33 + audioUrl = param; 34 + trackTitle = param.split("/").pop().replace(/\.[^.]+$/, ""); 35 + } else { 36 + audioUrl = `${CDN}/audio/${param}.mp3`; 37 + trackTitle = param; 38 + } 39 + 40 + playBtn = new ui.TextButton("PLAY", { center: "x", screen, y: 0 }); 41 + } 42 + 43 + function paint({ wipe, ink, screen, line }) { 44 + const floor = Math.floor; 45 + wipe(20, 15, 30); 46 + 47 + const m = 6; 48 + let y = 20; 49 + 50 + // Title 51 + ink(200, 180, 220).write(trackTitle, { x: m, y }); 52 + y += 14; 53 + 54 + // Frequency bars 55 + const barAreaH = 48, barCount = bars.length, gap = 1; 56 + const barW = floor((screen.width - m * 2 - (barCount - 1) * gap) / barCount); 57 + for (let i = 0; i < barCount; i++) { 58 + const h = floor(bars[i] * barAreaH); 59 + if (h > 0) { 60 + ink(0, 200, 180, 180).box( 61 + m + i * (barW + gap), y + barAreaH - h, barW, h, "fill", 62 + ); 63 + } 64 + } 65 + y += barAreaH + 6; 66 + 67 + // Scrub bar 68 + const scrubH = 12, scrubW = screen.width - m * 2; 69 + scrubBox = { x: m, y: y - 8, w: scrubW, h: scrubH + 16 }; 70 + 71 + ink(40, 30, 55).box(m, y, scrubW, scrubH, "fill"); 72 + if (duration > 0) { 73 + ink(60, 50, 80).box(m, y, floor((bufferedTime / duration) * scrubW), scrubH, "fill"); 74 + const progress = scrubber.isScrubbing || scrubber.inertiaActive 75 + ? scrubber.needleProgress 76 + : currentTime / duration; 77 + ink(0, 255, 200).line(m + floor(progress * scrubW), y, m + floor(progress * scrubW), y + scrubH); 78 + } 79 + ink(80, 70, 100).box(m, y, scrubW, scrubH, "outline"); 80 + y += scrubH + 10; 81 + 82 + // Play/pause button 83 + const label = isLoading ? "..." : isPlaying ? "PAUSE" : "PLAY"; 84 + if (playBtn.txt !== label) playBtn.txt = label; 85 + playBtn.reposition({ center: "x", screen, y }); 86 + playBtn.paint({ ink }, isPlaying 87 + ? [[0, 80, 70], [0, 200, 180], 255] 88 + : [[50, 40, 80], [120, 100, 160], 255]); 89 + y += playBtn.height + 4; 90 + 91 + // Time 92 + const timeStr = duration > 0 93 + ? `${fmtTime(currentTime)} / ${fmtTime(duration)}` 94 + : "--:-- / --:--"; 95 + ink(150, 130, 170).write(timeStr, { center: "x", screen, y }); 96 + } 97 + 98 + function togglePlay(send) { 99 + if (isLoading) return; 100 + if (isPlaying) { 101 + send({ type: "stream:pause", content: { id: STREAM_ID } }); 102 + } else if (hasStarted) { 103 + send({ type: "stream:resume", content: { id: STREAM_ID } }); 104 + } else { 105 + isLoading = true; 106 + send({ type: "stream:play", content: { id: STREAM_ID, url: audioUrl, volume: 0.8 } }); 107 + } 108 + } 109 + 110 + function act({ event: e, screen, send }) { 111 + playBtn?.act(e, { push: () => togglePlay(send) }); 112 + 113 + if (scrubBox && e.is("touch") && duration > 0) { 114 + const { x, y } = e; 115 + if (x >= scrubBox.x && x <= scrubBox.x + scrubBox.w && 116 + y >= scrubBox.y && y <= scrubBox.y + scrubBox.h) { 117 + scrubber.start(e, (x - scrubBox.x) / scrubBox.w, isPlaying); 118 + if (isPlaying) send({ type: "stream:pause", content: { id: STREAM_ID } }); 119 + } 120 + } 121 + if (scrubber.isScrubbing && e.is("draw")) scrubber.drag(e, screen.width); 122 + if (scrubber.isScrubbing && e.is("lift")) { 123 + const result = scrubber.end(); 124 + if (result.wasScrubbing) { 125 + send({ type: "stream:seek", content: { id: STREAM_ID, time: result.finalProgress * duration } }); 126 + if (result.wasPlayingBefore) send({ type: "stream:resume", content: { id: STREAM_ID } }); 127 + } 128 + } 129 + 130 + if (e.is("keyboard:down:space")) togglePlay(send); 131 + if (e.is("keyboard:down:arrowleft") && duration > 0) 132 + send({ type: "stream:seek", content: { id: STREAM_ID, time: Math.max(0, currentTime - 10) } }); 133 + if (e.is("keyboard:down:arrowright") && duration > 0) 134 + send({ type: "stream:seek", content: { id: STREAM_ID, time: Math.min(duration, currentTime + 10) } }); 135 + } 136 + 137 + function sim({ send }) { 138 + send({ type: "stream:time", content: { id: STREAM_ID } }); 139 + if (isPlaying) send({ type: "stream:frequencies", content: { id: STREAM_ID } }); 140 + 141 + const simResult = scrubber.simulate(); 142 + if (simResult && duration > 0) 143 + send({ type: "stream:seek", content: { id: STREAM_ID, time: simResult.needleProgress * duration } }); 144 + 145 + if (frequencyData.length > 0) { 146 + const step = Math.max(1, Math.floor(frequencyData.length / bars.length)); 147 + for (let i = 0; i < bars.length; i++) { 148 + bars[i] = bars[i] * 0.7 + ((frequencyData[i * step] || 0) / 255) * 0.3; 149 + } 150 + } else if (!isPlaying) { 151 + for (let i = 0; i < bars.length; i++) bars[i] *= 0.92; 152 + } 153 + } 154 + 155 + function receive({ type, content }) { 156 + if (content?.id !== STREAM_ID) return; 157 + if (type === "stream:playing") { isPlaying = true; isLoading = false; hasStarted = true; } 158 + if (type === "stream:paused") isPlaying = false; 159 + if (type === "stream:stopped") { isPlaying = false; hasStarted = false; } 160 + if (type === "stream:error") { isPlaying = false; isLoading = false; } 161 + if (type === "stream:time-data") { 162 + if (!scrubber.isScrubbing && !scrubber.inertiaActive) currentTime = content.currentTime; 163 + duration = content.duration; 164 + bufferedTime = content.buffered; 165 + if (content.ended && hasStarted) isPlaying = false; 166 + } 167 + if (type === "stream:seeked") currentTime = content.time; 168 + if (type === "stream:frequencies-data") frequencyData = content.data || []; 169 + } 170 + 171 + function leave({ send }) { 172 + send({ type: "stream:stop", content: { id: STREAM_ID } }); 173 + scrubber.reset(); 174 + isPlaying = false; hasStarted = false; currentTime = 0; duration = 0; 175 + } 176 + 177 + export { boot, paint, act, sim, receive, leave }; 178 + 179 + function fmtTime(sec) { 180 + if (!Number.isFinite(sec) || sec < 0) return "--:--"; 181 + return `${Math.floor(sec / 60)}:${String(Math.floor(sec % 60)).padStart(2, "0")}`; 182 + }