diff --git a/system/public/aesthetic.computer/bios.mjs b/system/public/aesthetic.computer/bios.mjs index c1ae02fbb3..6ff0c5a5ce 100644 --- a/system/public/aesthetic.computer/bios.mjs +++ b/system/public/aesthetic.computer/bios.mjs @@ -3405,7 +3405,7 @@ async function boot(parsed, bpm = 60, resolution, debug) { speakerProcessor.port.postMessage({ type: "new-bpm", data: newBPM }); }; - triggerSound = function (sound) { + triggerSound = function (sound) { // 🔬 Telemetry: Record this sound trigger for latency testing const triggerTime = performance.now(); soundTelemetry.triggerCount++; @@ -3419,7 +3419,20 @@ async function boot(parsed, bpm = 60, resolution, debug) { soundTelemetry.recentTriggers.shift(); } - speakerProcessor.port.postMessage({ type: "sound", data: sound }); + if (sound.probe) { + sound.probe = { + ...sound.probe, + biosAt: triggerTime, + biosEpoch: Date.now(), + sentAt: performance.now(), + payloadBytes: JSON.stringify({ + type: sound.type, tone: sound.tone, beats: sound.beats, + attack: sound.attack, decay: sound.decay, volume: sound.volume, + pan: sound.pan, probe: sound.probe, + }).length, + }; + } + speakerProcessor.port.postMessage({ type: "sound", data: sound }); return { progress: () => { @@ -3559,7 +3572,7 @@ async function boot(parsed, bpm = 60, resolution, debug) { return; } - if (msg.type === "telemetry") { + if (msg.type === "telemetry") { if (typeof window !== 'undefined') { window.__speaker_telemetry = window.__speaker_telemetry || {}; window.__speaker_telemetry.queueLength = msg.content.queueLength || 0; @@ -3567,8 +3580,24 @@ async function boot(parsed, bpm = 60, resolution, debug) { window.__speaker_telemetry.performanceMode = msg.content.performanceMode || 'auto'; window.__speaker_telemetry.lastUpdate = Date.now(); } - return; - } + return; + } + + if (msg.type === "audio:probe") { + const receivedAt = performance.now(); + const probe = msg.content || {}; + send({ + type: "audio:probe", + content: { + ...probe, + workletRoundTrip: receivedAt - (probe.sentAt || receivedAt), + pieceToBios: probe.pieceEpoch + ? (probe.biosEpoch || Date.now()) - probe.pieceEpoch + : (probe.biosAt || receivedAt) - (probe.pieceAt || probe.biosAt || receivedAt), + }, + }); + return; + } // 🎛️ VST Bridge Mode - forward audio samples to native plugin if (msg.type === "vst:samples") { @@ -18226,7 +18255,40 @@ async function boot(parsed, bpm = 60, resolution, debug) { send({ type: "update:auto-state", content: autoReload }); } if (autoReload && !window.acUpdateReloadTimer) { - window.acUpdateReloadTimer = setTimeout(() => window.location.reload(), 2500); + // Make a hot update feel intentional: a tiny ascending chord and warm + // visual pulse, then enough grace for Xbox/TV output buffers to play it. + try { + const now = audioContext?.currentTime; + if (audioContext && Number.isFinite(now)) { + [523.25, 659.25, 783.99].forEach((frequency, index) => { + const oscillator = audioContext.createOscillator(); + const gain = audioContext.createGain(); + oscillator.type = index === 2 ? "triangle" : "sine"; + oscillator.frequency.value = frequency; + gain.gain.setValueAtTime(0.0001, now + index * 0.055); + gain.gain.exponentialRampToValueAtTime(0.085, now + index * 0.055 + 0.012); + gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.55 + index * 0.055); + oscillator.connect(gain).connect(speakerGain || audioContext.destination); + oscillator.start(now + index * 0.055); + oscillator.stop(now + 0.65 + index * 0.055); + }); + } + const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches; + const flash = document.createElement("div"); + Object.assign(flash.style, { + position: "fixed", inset: "0", zIndex: "2147483647", + pointerEvents: "none", background: "rgba(255,210,40,.48)", + transition: reduced ? "opacity .35s ease" : "background .16s ease, opacity .5s ease", + }); + document.body.appendChild(flash); + requestAnimationFrame(() => { + if (!reduced) flash.style.background = "rgba(255,70,45,.42)"; + }); + setTimeout(() => { flash.style.opacity = "0"; }, reduced ? 180 : 360); + } catch (error) { + console.warn("📦 Update awareness cue unavailable:", error); + } + window.acUpdateReloadTimer = setTimeout(() => window.location.reload(), 1250); } } buildOverlay("updateBadge", content.updateBadge); // 📦 Update-ready ↑ badge (top-right) diff --git a/system/public/aesthetic.computer/disks/gamepad.mjs b/system/public/aesthetic.computer/disks/gamepad.mjs index 3129837abf..46da453194 100644 --- a/system/public/aesthetic.computer/disks/gamepad.mjs +++ b/system/public/aesthetic.computer/disks/gamepad.mjs @@ -37,6 +37,8 @@ const latencySamples = []; const MAX_LATENCY_SAMPLES = 60; let lastLatency = null; let audioLatency = null; +let audioProbe = null; +let probeSequence = 0; let sendToBios = null; let lightMode = false; const gamepadTheme = () => lightMode @@ -305,7 +307,10 @@ function paint({ wipe, ink, write, screen, line, circle, box, painting, paste, h const browserAudio = audioLatency ? ` BASE ${audioLatency.base.toFixed(1)}ms OUT ${audioLatency.output.toFixed(1)}ms Q ${audioLatency.quantum.toFixed(1)}ms` : " AUDIO --"; - const latencyText = `IN ${lastLatency.delivery.toFixed(0)}ms AVG ${average.toFixed(0)}ms SYN ${lastLatency.synth.toFixed(2)}ms${browserAudio}`; + const worker = audioProbe + ? ` P>B ${audioProbe.pieceToBios.toFixed(1)}ms WRT ${audioProbe.workletRoundTrip.toFixed(1)}ms ATK ${audioProbe.attackMs.toFixed(1)}ms/${audioProbe.attackFrames}smp ${audioProbe.payloadBytes}B V${audioProbe.runningCount}` + : " WORK --"; + const latencyText = `IN ${lastLatency.delivery.toFixed(0)}ms AVG ${average.toFixed(0)}ms SYN ${lastLatency.synth.toFixed(2)}ms${browserAudio}${worker}`; const width = latencyText.length * 4 + 6; const x = Math.max(2, screen.width - width - 4); const y = hudHeight; @@ -650,6 +655,12 @@ function act({ event: e, sound }) { duration: "🔁", decay: 0.85, volume: 0.22, + probe: { + id: `gamepad-${++probeSequence}`, + button: buttonIndex, + pieceAt: performance.now(), + pieceEpoch: Date.now(), + }, }); if (voice) buttonVoices.set(voiceKey, voice); const synthMs = performance.now() - synthStartedAt; @@ -720,7 +731,20 @@ function boot({ send }) { } function receive({ type, content }) { - if (type !== "audio:latency-info" || !content) return; + if (!content) return; + if (type === "audio:probe") { + audioProbe = content; + console.log( + `🔬 AUDIO_PIPELINE id=${content.id} button=${content.button}` + + ` pieceToBios=${content.pieceToBios.toFixed(2)}ms` + + ` workletRT=${content.workletRoundTrip.toFixed(2)}ms` + + ` attack=${content.attackMs.toFixed(2)}ms/${content.attackFrames}frames` + + ` payload=${content.payloadBytes}B queue=${content.queueLength}` + + ` voices=${content.runningCount} frame=${content.workletFrame}`, + ); + return; + } + if (type !== "audio:latency-info") return; audioLatency = { base: Number(content.base) || 0, output: Number(content.output) || 0, diff --git a/system/public/aesthetic.computer/lib/disk.mjs b/system/public/aesthetic.computer/lib/disk.mjs index 715e2691f8..c0d167280b 100644 --- a/system/public/aesthetic.computer/lib/disk.mjs +++ b/system/public/aesthetic.computer/lib/disk.mjs @@ -11196,6 +11196,7 @@ async function makeFrame({ data: { type, content } }) { type === "tape:frames" || type === "tape:audio-context-state" || type === "audio:latency-info" || + type === "audio:probe" || type === "tape:playback-progress" || // Video-backed (kind:"mp4") tape playback callbacks → video.mjs. type === "tape:mp4-ready" || @@ -12856,6 +12857,7 @@ async function makeFrame({ data: { type, content } }) { decay = 0.9, // A multiplier for when the sound fades. volume, pan = 0, + probe = null, // Opt-in main-thread → AudioWorklet profiling metadata. generator = null, // Custom waveform generator function for type "custom" } = {}) { const id = soundId; @@ -12867,7 +12869,7 @@ async function makeFrame({ data: { type, content } }) { tone = $sound.freq(tone); // console.log("⛈️ Tone:", tone); // Add generator to sound data for custom type - const soundData = { id, type, tone, beats, attack, decay, volume, pan }; + const soundData = { id, type, tone, beats, attack, decay, volume, pan, probe }; if (type === "custom" && generator) { soundData.generator = generator.toString(); // Convert function to string for postMessage } diff --git a/system/public/aesthetic.computer/lib/speaker-bundled.mjs b/system/public/aesthetic.computer/lib/speaker-bundled.mjs index 99e8c8ce6b..8c36f56e1a 100644 --- a/system/public/aesthetic.computer/lib/speaker-bundled.mjs +++ b/system/public/aesthetic.computer/lib/speaker-bundled.mjs @@ -1362,6 +1362,18 @@ var SpeakerProcessor = class extends AudioWorkletProcessor { }); this.#running[msg.data.id] = sound; this.#queue.push(sound); + if (msg.data.probe) { + this.#report("audio:probe", { + ...msg.data.probe, + workletFrame: currentFrame, + workletTime: currentTime, + attackFrames: attack2, + attackMs: attack2 / sampleRate * 1e3, + queueLength: this.#queue.length, + runningCount: Object.keys(this.#running).length, + sampleRate + }); + } return; } if (msg.type === "bubble") { diff --git a/system/public/aesthetic.computer/lib/speaker.mjs b/system/public/aesthetic.computer/lib/speaker.mjs index bef5c2013c..69729c1149 100644 --- a/system/public/aesthetic.computer/lib/speaker.mjs +++ b/system/public/aesthetic.computer/lib/speaker.mjs @@ -568,7 +568,19 @@ class SpeakerProcessor extends AudioWorkletProcessor { this.#running[msg.data.id] = sound; // Index by the unique id. // } - this.#queue.push(sound); + this.#queue.push(sound); + if (msg.data.probe) { + this.#report("audio:probe", { + ...msg.data.probe, + workletFrame: currentFrame, + workletTime: currentTime, + attackFrames: attack, + attackMs: (attack / sampleRate) * 1000, + queueLength: this.#queue.length, + runningCount: Object.keys(this.#running).length, + sampleRate, + }); + } //} else { // Wait and then trigger... // console.log("😊 Waiting on sound:", msg.data.when);