From d8595056d276d767ed631fe59f79d1e2b6bf49e1 Mon Sep 17 00:00:00 2001 From: prompt.ac/@jeffrey Date: Sat, 01 Aug 2026 16:04:28 +0000 Subject: [PATCH] Polish cap playback controls and audio --- system/public/aesthetic.computer/bios.mjs | 7 ++++--- system/public/aesthetic.computer/disks/cap.mjs | 31 ++++++++++++++++++++++++++----- system/public/aesthetic.computer/disks/video.mjs | 46 +++++++++++++--------------------------------- 3 file(s) changed, 43 insertion(s)(+), 41 deletion(s)(-) diff --git a/system/public/aesthetic.computer/bios.mjs b/system/public/aesthetic.computer/bios.mjs --- a/system/public/aesthetic.computer/bios.mjs +++ b/system/public/aesthetic.computer/bios.mjs @@ -3187,9 +3187,10 @@ try { // Allow pieces to configure audio constraints via data parameter const audioConstraints = { echoCancellation: data?.echoCancellation ?? false, - noiseSuppression: data?.noiseSuppression ?? false, - autoGainControl: data?.autoGainControl ?? false, - latency: data?.latency ?? 0, + noiseSuppression: data?.noiseSuppression ?? false, + autoGainControl: data?.autoGainControl ?? false, + volume: data?.volume ?? 1, + latency: data?.latency ?? 0, }; // Request higher sample rate if on Chrome desktop for lower latency diff --git a/system/public/aesthetic.computer/disks/cap.mjs b/system/public/aesthetic.computer/disks/cap.mjs --- a/system/public/aesthetic.computer/disks/cap.mjs +++ b/system/public/aesthetic.computer/disks/cap.mjs @@ -50,13 +50,17 @@ // 🔍 Slide-up-to-zoom (while holding record). // `zoom` scales the camera frame around the screen center. 1 = no zoom. // `zoomStartY` is the Y position of the touch that began recording. // `zoomMax` caps how far the user can zoom in via slide. -// `zoomSensitivity` is the screen-pixels-per-zoom-unit: at 240 px/unit -// a comfy thumb slide of half a phone height (~400px) gives ~1.7x zoom. +// A short upward slide zooms quickly; recent finger velocity projects the +// gesture forward so fast throws feel more extreme than slow positioning. let zoom = 1; let zoomStartY = null; -const zoomMax = 4; +let zoomLastY = null; +let zoomLastAt = 0; +let zoomVelocity = 0; +const zoomMax = 6; const zoomMin = 1; -const zoomSensitivity = 240; +const zoomSensitivity = 120; +const zoomInertiaMs = 180; // 🥾 Boot function boot({ ui, params, colon, system, rec, notice }) { @@ -79,6 +83,9 @@ torchEnabled = false; torchPending = false; zoom = 1; zoomStartY = null; + zoomLastY = null; + zoomLastAt = 0; + zoomVelocity = 0; // Parse parameters if (params[0] === "me" || params[0] === "selfie") facing = "user"; @@ -446,6 +453,9 @@ if (!onSwapBtn && !onFlashBtn && !onHud && !isRecording && !pendingRecordStart) { // Anchor zoom slider at the touch-down Y so the very first drag // pixel is treated as zoom = 1, no jumps. zoomStartY = e.y; + zoomLastY = e.y; + zoomLastAt = performance.now(); + zoomVelocity = 0; startRecording(rec, notice); } } @@ -455,9 +465,16 @@ // We anchor zoomStartY at the original touch and remap the absolute // Y delta each draw event — this means the user can swing freely // up and down without accumulating drift across pauses. if (e.is("draw") && isRecording && zoomStartY !== null) { + const now = performance.now(); + const dt = Math.max(1, now - zoomLastAt); + const instantVelocity = (zoomLastY - e.y) / dt; + zoomVelocity += (instantVelocity - zoomVelocity) * 0.55; const dy = zoomStartY - e.y; // up is positive - const target = 1 + dy / zoomSensitivity; + const projectedDy = dy + zoomVelocity * zoomInertiaMs; + const target = 1 + projectedDy / zoomSensitivity; zoom = Math.max(zoomMin, Math.min(zoomMax, target)); + zoomLastY = e.y; + zoomLastAt = now; } if (e.is("lift") && !leaving()) { @@ -469,6 +486,9 @@ pendingRecordStart = false; notice("CANCELLED", ["yellow", "black"]); } zoomStartY = null; + zoomLastY = null; + zoomLastAt = 0; + zoomVelocity = 0; zoom = 1; } @@ -536,6 +556,7 @@ microphone.connect({ echoCancellation: false, noiseSuppression: false, autoGainControl: true, + volume: 1, }); } } diff --git a/system/public/aesthetic.computer/disks/video.mjs b/system/public/aesthetic.computer/disks/video.mjs --- a/system/public/aesthetic.computer/disks/video.mjs +++ b/system/public/aesthetic.computer/disks/video.mjs @@ -679,15 +679,9 @@ if (!mp4Playing) { ink(255, 200).write("||", { center: "xy" }); ink(255, 75).box(0, 0, screen.width, screen.height, "inline"); } - ink(0, 0, 0, 105).box( - 0, - screen.height - CONTROL_RAIL_HEIGHT, - screen.width, - CONTROL_RAIL_HEIGHT, - ); - // Thin progress bar above the control rail. + // Thin progress bar at the bottom edge, behind the controls. const barH = 2; - const barY = screen.height - CONTROL_RAIL_HEIGHT - barH; + const barY = screen.height - barH; ink(255, 40).box(0, barY, screen.width, barH); ink(255, 200, 0).box(0, barY, Math.floor(mp4Progress * screen.width), barH); @@ -705,15 +699,6 @@ if (presenting && !playing && !isPrinting) { // Paused: subtle overlay without a black background ink(255, 200).write("||", { center: "xy" }); ink(255, 75).box(0, 0, screen.width, screen.height, "inline"); - } - - if (exportAvailable && !isPrinting) { - ink(0, 0, 0, 105).box( - 0, - screen.height - CONTROL_RAIL_HEIGHT, - screen.width, - CONTROL_RAIL_HEIGHT, - ); } // Draw export buttons - reposition every frame (simple!) @@ -1030,7 +1015,8 @@ false, "MatrixChunky8", ); - // 🔴 One playback timeline sits above the Back/Done control rail. + // 🔴 One playback timeline sits on the bottom edge, behind the + // Back/Done controls. // position — drawn from live state so it never lies. It blinks white // on every UTC bar boundary, a silent visual click for timing up. const livePos = scrubDriven @@ -1044,7 +1030,7 @@ const latComp = (audioDiag.latencyMs || 0) / 1000 / (tapeInfo?.totalDuration || 8); let needlePos = livePos - latComp; needlePos = ((needlePos % 1) + 1) % 1; - const timelineY = screen.height - CONTROL_RAIL_HEIGHT - 2; + const timelineY = screen.height - 2; ink(255, 255, 255, 45).box(0, timelineY, screen.width, 2); ink(255, 51, 68, 170).box( 0, @@ -1294,12 +1280,12 @@ // 1/4 chop orange, 1/8 chop pink, reset green. Rollover activation is // ON — glide across pads notepat-style to trigger them. const DECK = [ [ - { key: "jumpL", label: "<", color: [90, 130, 200] }, - { key: "jumpR", label: ">", color: [90, 130, 200] }, - ], - [ { key: "pitchD", label: "p-", color: [150, 110, 200] }, { key: "pitchU", label: "p+", color: [150, 110, 200] }, + ], + [ + { key: "jumpL", label: "<", color: [90, 130, 200] }, + { key: "jumpR", label: ">", color: [90, 130, 200] }, ], [{ key: "chop4", label: "1/4 chop", color: [220, 150, 40] }], [{ key: "chop8", label: "1/8 chop", color: [255, 120, 180] }], @@ -1310,14 +1296,7 @@ const bw = 64; // Cluster width const bh = 14; // Button height const bgap = 2; const bx0 = screen.width - SAFE_R - bw; - const deckHeight = DECK.length * bh + (DECK.length - 1) * bgap; - const deckTop = Math.max( - SAFE_T + 48, - Math.min( - screen.height - SAFE_B - deckHeight - 28, - Math.floor((screen.height - deckHeight) / 2), - ), - ); + const deckTop = SAFE_T + 48; let by = deckTop; for (const row of DECK) { const cw = Math.floor((bw - (row.length - 1) * bgap) / row.length); @@ -1361,7 +1340,7 @@ // Pitch offset readout beside the pitch row when engaged. if (pitchSemis !== 0) { ink(200, 160, 255).write( `${pitchSemis > 0 ? "+" : ""}${pitchSemis}st`, - { y: deckTop + bh + bgap + 4, right: screen.width - bx0 + 4 }, + { y: deckTop + 4, right: screen.width - bx0 + 4 }, undefined, undefined, false, @@ -2907,7 +2886,8 @@ // of speed; the worklet's two-tap shifter re-pitches the audio // while the tempo, needle, and grid stay exactly where they are. const nudgePitch = (d) => { pitchSemis = Math.max(-12, Math.min(12, pitchSemis + d)); - send({ type: "tape:audio-pitch", content: Math.pow(2, pitchSemis / 12) }); + // The worklet's delay ratio runs opposite to musical semitones. + send({ type: "tape:audio-pitch", content: Math.pow(2, -pitchSemis / 12) }); triggerRender(); }; legendBtns?.pitchD?.act(e, { down: () => nudgePitch(-2) }); -- tangled.sh