diff --git a/system/public/aesthetic.computer/disks/gamepad.mjs b/system/public/aesthetic.computer/disks/gamepad.mjs index e6b76b6e64..387d7de827 100644 --- a/system/public/aesthetic.computer/disks/gamepad.mjs +++ b/system/public/aesthetic.computer/disks/gamepad.mjs @@ -17,28 +17,28 @@ import { getAxisMapping } from "../lib/gamepad-mappings.mjs"; -import { cssColors } from "../lib/num.mjs"; - -const { keys } = Object; +import { cssColors } from "../lib/num.mjs"; + +const { keys } = Object; const connectedGamepads = {}; const buttonHistory = []; // Store button press history const MAX_HISTORY = 256; // Enough frames to fill full screen height const recentEvents = []; const MAX_EVENTS = 10; const FONT = "MatrixChunky8"; // Pixel-perfect font -const soundQueue = []; // Stable pitches make the test useful as a tiny instrument. The M30 face // layout (A B C / X Y Z) lands on two related three-note rows. const BUTTON_TONES = [261.63, 329.63, 523.25, 659.25, 783.99, 392.0, 174.61, 196.0, 220.0, 246.94, 293.66, 349.23, 880.0, 987.77, 1174.66, 1318.51, 1567.98]; +const buttonVoices = new Map(); const timelinePaintings = [null, null, null, null]; // One timeline per gamepad -function drawXboxControllerDiagram({ ink, line, circle, box }, gp, x, y) { - const baseX = x; - const baseY = y; +function drawXboxControllerDiagram({ ink, line, circle, box }, gp, x, y) { + const baseX = x; + const baseY = y; const gamepadId = gp.id; // Get button colors from mapping @@ -128,9 +128,9 @@ function drawXboxControllerDiagram({ ink, line, circle, box }, gp, x, y) { // TOP: Triggers (outer) and Bumpers (inner) - SWAPPED const topY = baseY; ink(getColors(6)).box(baseX + 2, topY, 12, 2); // LT (outer/left) - ink(getColors(4)).box(baseX + 14, topY, 8, 2); // LB (inner) - - ink(getColors(7)).box(baseX + width - 14, topY, 12, 2); // RT (outer/right) + ink(getColors(4)).box(baseX + 14, topY, 8, 2); // LB (inner) + + ink(getColors(7)).box(baseX + width - 14, topY, 12, 2); // RT (outer/right) ink(getColors(5)).box(baseX + width - 22, topY, 8, 2); // RB (inner) } @@ -138,9 +138,9 @@ function draw8BitDoMicroDiagram({ ink, line, circle, box }, gp, x, y) { const baseX = x; const baseY = y; const gamepadId = gp.id || "standard"; - - // Get button colors from mapping - const getColors = (btnIndex) => { + + // Get button colors from mapping + const getColors = (btnIndex) => { const colors = getButtonColors(gamepadId, btnIndex); const isPressed = gp.pressedButtons.includes(btnIndex); // Return active color when pressed, inactive when not @@ -220,9 +220,9 @@ function draw8BitDoMicroDiagram({ ink, line, circle, box }, gp, x, y) { ink(getColors(6)).box(baseX, shoulderY, 14, 2); // L (wider) ink(getColors(7)).box(baseX + 34, shoulderY, 14, 2); // R (wider) - // L2/R2 triggers - with gap in center (original mapping) - ink(getColors(8)).box(baseX + 14, shoulderY, 8, 2); // L2 (narrower, gap on right) - ink(getColors(9)).box(baseX + 26, shoulderY, 8, 2); // R2 (narrower, gap on left) + // L2/R2 triggers - with gap in center (original mapping) + ink(getColors(8)).box(baseX + 14, shoulderY, 8, 2); // L2 (narrower, gap on right) + ink(getColors(9)).box(baseX + 26, shoulderY, 8, 2); // R2 (narrower, gap on left) // Home/Heart button at bottom center ink(getColors(12)).box(centerX - 2, baseY + 19, 4, 3); @@ -264,9 +264,9 @@ function drawGamepadDiagram({ ink, line, circle, box }, gp, x, y) { // Xbox Controller drawXboxControllerDiagram({ ink, line, circle, box }, gp, x, y); } else { - // 8BitDo Micro or generic - draw8BitDoMicroDiagram({ ink, line, circle, box }, gp, x, y); - } + // 8BitDo Micro or generic + draw8BitDoMicroDiagram({ ink, line, circle, box }, gp, x, y); + } } function paint({ wipe, ink, write, screen, line, circle, box, painting, paste, help, text }) { @@ -447,9 +447,9 @@ function paint({ wipe, ink, write, screen, line, circle, box, painting, paste, h } // Show controller name and diagram at bottom - if (gp.id) { - const nameY = slotY + slotHeight - diagramHeight - metadataHeight - 8; - const maxWidth = slotWidth - 8; + if (gp.id) { + const nameY = slotY + slotHeight - diagramHeight - metadataHeight - 8; + const maxWidth = slotWidth - 8; // Detect controller type and show friendly name let displayName = gp.id; @@ -460,9 +460,9 @@ function paint({ wipe, ink, write, screen, line, circle, box, painting, paste, h } else if (displayName.includes("2dc8") && displayName.includes("9020")) { displayName = "8BitDo Micro"; } else { - // Clean up generic names - remove vendor/product IDs - displayName = displayName.replace(/\s*\(STANDARD GAMEPAD[^)]*\)/i, ""); - displayName = displayName.replace(/\s*\(Vendor:[^)]*\)/i, ""); + // Clean up generic names - remove vendor/product IDs + displayName = displayName.replace(/\s*\(STANDARD GAMEPAD[^)]*\)/i, ""); + displayName = displayName.replace(/\s*\(Vendor:[^)]*\)/i, ""); } // Controller name - use MatrixChunky8 font @@ -543,33 +543,19 @@ function paint({ wipe, ink, write, screen, line, circle, box, painting, paste, h const axesText = parts.join(" "); ink(playerColor, 180).write(axesText, { x: slotX + 4, y: currentY }, undefined, maxWidth, true, FONT); - } - } - } + } + } + } }); } -function sim({ sound }) { - while (soundQueue.length) { - const tone = soundQueue.shift(); - sound?.synth?.({ - type: "triangle", - tone, - attack: 0.003, - duration: 0.12, - decay: 0.55, - volume: 0.18, - }); - } -} - -function act({ event: e }) { +function act({ event: e, sound }) { if (e.is("gamepad")) { const gpIndex = e.gamepad; - // Initialize gamepad entry if needed - if (!connectedGamepads[gpIndex]) { - connectedGamepads[gpIndex] = { + // Initialize gamepad entry if needed + if (!connectedGamepads[gpIndex]) { + connectedGamepads[gpIndex] = { id: e.gamepadId || null, // Capture gamepad name/type from event pressedButtons: [], axes: {}, @@ -609,24 +595,43 @@ function act({ event: e }) { gp.id = e.gamepadId; } - // Update last event - gp.lastEvent = e.name; - + // Update last event + gp.lastEvent = e.name; + // Track button state if (e.is(`gamepad:${gpIndex}:button`)) { const buttonIndex = e.button; + const voiceKey = `${gpIndex}:${buttonIndex}`; if (e.action === "push") { + const synthStartedAt = performance.now(); if (!gp.pressedButtons.includes(buttonIndex)) { gp.pressedButtons.push(buttonIndex); } - soundQueue.push(BUTTON_TONES[buttonIndex] || 220 + buttonIndex * 30); + buttonVoices.get(voiceKey)?.kill?.(0.005); + const voice = sound?.synth?.({ + type: "triangle", + tone: BUTTON_TONES[buttonIndex] || 220 + buttonIndex * 30, + attack: 0.001, + duration: "🔁", + decay: 0.85, + volume: 0.22, + }); + if (voice) buttonVoices.set(voiceKey, voice); + const synthMs = performance.now() - synthStartedAt; + const deliveryMs = e.createdAt ? Date.now() - e.createdAt : null; + console.log( + `🎮 AUDIO_LATENCY button=${buttonIndex}` + + ` delivery=${deliveryMs ?? "?"}ms synth=${synthMs.toFixed(2)}ms`, + ); console.log(`🔴 Button ${buttonIndex} PRESSED -`, getButtonName(gp.id, buttonIndex)); } else if (e.action === "release") { gp.pressedButtons = gp.pressedButtons.filter(b => b !== buttonIndex); - console.log(`âšĒ Button ${buttonIndex} RELEASED -`, getButtonName(gp.id, buttonIndex)); - } - - // Add to recent events + buttonVoices.get(voiceKey)?.kill?.(0.025); + buttonVoices.delete(voiceKey); + console.log(`âšĒ Button ${buttonIndex} RELEASED -`, getButtonName(gp.id, buttonIndex)); + } + + // Add to recent events recentEvents.push(`P${gpIndex + 1} Button ${buttonIndex}: ${e.action}`); if (recentEvents.length > MAX_EVENTS) recentEvents.shift(); } @@ -653,13 +658,18 @@ function act({ event: e }) { console.log(`đŸ•šī¸ Axis ${axisIndex} -`, getAxisName(gp.id, axisIndex), "centered"); gp.lastLoggedAxes[axisIndex] = 0; } - } - } - } -} - -// 📚 Library -// function boot() { + } + } + } +} + +function leave() { + for (const voice of buttonVoices.values()) voice?.kill?.(0.01); + buttonVoices.clear(); +} + +// 📚 Library +// function boot() { // Runs once at the start. // } @@ -681,10 +691,10 @@ function act({ event: e }) { // function preview({ ink, wipe }) { // Render a custom thumbnail image. -// } - -// function icon() { +// } + +// function icon() { // Render an application icon, aka favicon. // } -export { paint, act, sim }; +export { paint, act, leave }; diff --git a/system/public/aesthetic.computer/lib/gamepad.mjs b/system/public/aesthetic.computer/lib/gamepad.mjs index 15559ae2c5..1f4bc28273 100644 --- a/system/public/aesthetic.computer/lib/gamepad.mjs +++ b/system/public/aesthetic.computer/lib/gamepad.mjs @@ -30,6 +30,7 @@ export class Gamepad { const name = `gamepad:${gi}:button:${bi}:${action}`; const event = { name, + createdAt: Date.now(), gamepad: gi, button: bi, action, @@ -64,6 +65,7 @@ export class Gamepad { const name = `gamepad:${gi}:axis:${ai}:move`; const event = { name, + createdAt: Date.now(), gamepad: gi, axis: ai, value, @@ -78,6 +80,7 @@ export class Gamepad { const name = `gamepad:${gi}:axis:${ai}:move`; const event = { name, + createdAt: Date.now(), gamepad: gi, axis: ai, value: 0,