import { useState, useCallback, useRef, useEffect } from "react"; const T = { bg: "#0A0A0C", panel: "#141418", surface: "#1C1C22", border: "#2A2A32", amber: "#E8A030", amberDim: "#7A5218", blue: "#3A7BA8", blueDim: "#1E4060", green: "#3AB87A", red: "#C84040", textPrimary: "#F0EDE6", textSec: "#8888A0", textDim: "#444458", }; async function api(method, path, body) { const opts = { method, headers: { "Content-Type": "application/json" } }; if (body) opts.body = JSON.stringify(body); const res = await fetch(`${path}`, opts); if (!res.ok) throw new Error(`API ${method} ${path}: ${res.status}`); return res.json(); } const SETTINGS = { "Audio": [ { key: "master_volume", label: "Master Volume", type: "slider", min: 0, max: 100, def: 72, unit: "%", icon: "πŸ”Š", via: "volume" }, { key: "sample_rate", label: "Sample Rate", type: "select", def: "48000", options: [ { value: "22050", label: "22.05 kHz" }, { value: "44100", label: "44.1 kHz" }, { value: "48000", label: "48 kHz" }, { value: "88200", label: "88.2 kHz" }, { value: "96000", label: "96 kHz" }, ], icon: "πŸ“Š", via: "audio_profile" }, { key: "buffer_size", label: "Buffer Size", type: "select", def: "512", options: [ { value: "64", label: "64 (1.3ms)" }, { value: "128", label: "128 (2.7ms)" }, { value: "256", label: "256 (5.3ms)" }, { value: "512", label: "512 (10.6ms)" }, { value: "1024",label: "1024 (21.3ms)" }, ], icon: "πŸ“¦", via: "audio_profile" }, { key: "input_pad", label: "Input Pad", type: "select", def: "off", options: [ { value: "off", label: "Off (0 dB)" }, { value: "-6", label: "-6 dB" }, { value: "-12", label: "-12 dB" }, { value: "-18", label: "-18 dB" }, ], icon: "πŸ“₯", via: "settings" }, { key: "input_impedance", label: "Input Impedance", type: "select", def: "1m", options: [ { value: "1m", label: "1 MΞ©" }, { value: "500k", label: "500 kΞ©" }, { value: "250k", label: "250 kΞ©" }, { value: "150k", label: "150 kΞ©" }, { value: "68k", label: "68 kΞ©" }, ], icon: "⚑", via: "settings" }, ], "Routing": [ { key: "routing_mode", label: "Routing Mode", type: "select", def: "mono", options: [ { value: "mono", label: "Mono" }, { value: "4cm", label: "4CM (Stereo FX Loop)" }, ], icon: "πŸ”€", via: "routing" }, { key: "routing_breakpoint", label: "FX Loop Breakpoint", type: "select", def: "7", options: [ { value: "1", label: "After block 1" }, { value: "2", label: "After block 2" }, { value: "3", label: "After block 3" }, { value: "4", label: "After block 4" }, { value: "5", label: "After block 5" }, { value: "6", label: "After block 6" }, { value: "7", label: "After block 7" }, { value: "8", label: "After block 8" }, ], icon: "↔", via: "routing" }, ], "Audio Interface": [ { key: "input_device", label: "Input Device", type: "select", def: "hw:0,0", options: [ { value: "hw:0,0", label: "Default (hw:0,0)" }, { value: "hw:1,0", label: "USB Audio (hw:1,0)" }, { value: "hw:2,0", label: "HDMI Audio (hw:2,0)" }, ], icon: "🎀", via: "settings" }, { key: "output_device", label: "Output Device", type: "select", def: "hw:0,0", options: [ { value: "hw:0,0", label: "Default (hw:0,0)" }, { value: "hw:1,0", label: "USB Audio (hw:1,0)" }, { value: "hw:2,0", label: "HDMI Audio (hw:2,0)" }, ], icon: "πŸ”ˆ", via: "settings" }, { key: "channel_mode", label: "Channel Mode", type: "select", def: "mono", options: [ { value: "mono", label: "Mono" }, { value: "dual-mono", label: "Dual Mono" }, { value: "stereo_4cm", label: "Stereo (4CM)" }, ], icon: "πŸ”Š", via: "settings" }, ], "MIDI": [ { key: "midi_channel", label: "MIDI Channel", type: "select", def: "omni", options: [ { value: "omni", label: "Omni" }, ...Array.from({ length: 16 }, (_, i) => ({ value: String(i + 1), label: `Channel ${i + 1}` })), ], icon: "🎹", via: "settings" }, { key: "midi_clock", label: "MIDI Clock", type: "toggle", def: false, via: "settings", icon: "⏱" }, { key: "midi_thru", label: "MIDI Thru", type: "toggle", def: true, via: "settings", icon: "↔" }, ], "Display": [ { key: "brightness", label: "Display Brightness", type: "slider", min: 1, max: 10, def: 8, unit: "", icon: "β˜€οΈ", via: "settings" }, { key: "auto_dim", label: "Auto Dim", type: "toggle", def: true, via: "settings", icon: "πŸŒ™" }, { key: "screen_saver", label: "Screen Saver", type: "select", def: "5m", options: [ { value: "off", label: "Off" }, { value: "1m", label: "1 min" }, { value: "5m", label: "5 min" }, { value: "15m", label: "15 min" }, { value: "30m", label: "30 min" }, ], icon: "πŸ’€", via: "settings" }, ], "Tempo": [ { key: "tap_tempo", label: "Tap Tempo", type: "bpm", def: 120, via: "settings", icon: "πŸ”„" }, { key: "tempo_division", label: "Tempo Division", type: "select", def: "quarter", options: [ { value: "half", label: "Β½ Note" }, { value: "quarter", label: "ΒΌ Note" }, { value: "eighth", label: "β…› Note" }, { value: "triplet", label: "β…› Triplet" }, { value: "sixteenth", label: "¹⁄₁₆ Note" }, { value: "dotted_eighth", label: "Dotted β…›" }, ], icon: "🎢", via: "settings" }, { key: "tap_tempo_mute", label: "Mute on Tap", type: "toggle", def: false, via: "settings", icon: "πŸ”‡" }, ], }; function buildDefaults(overrides = {}) { const init = {}; for (const group of Object.values(SETTINGS)) { for (const s of group) { init[s.key] = s.def; } } return { ...init, ...overrides }; } // ── Widgets ──────────────────────────────────────────────────────────────── function SettingsSlider({ label, value, min, max, unit, onChange }) { const trackRef = useRef(null); const dragRef = useRef(false); const pct = ((value - min) / (max - min)) * 100; const valFromEvent = useCallback((e) => { const t = trackRef.current; if (!t) return value; const r = t.getBoundingClientRect(); const x = (e.touches ? e.touches[0].clientX : e.clientX) - r.left; return Math.round(min + Math.max(0, Math.min(1, x / r.width)) * (max - min)); }, [min, max, value]); useEffect(() => { const move = (e) => { if (!dragRef.current) return; e.preventDefault(); onChange?.(valFromEvent(e)); }; const up = () => { dragRef.current = false; }; window.addEventListener("mousemove", move); window.addEventListener("mouseup", up); window.addEventListener("touchmove", move, { passive: false }); window.addEventListener("touchend", up); return () => { window.removeEventListener("mousemove", move); window.removeEventListener("mouseup", up); window.removeEventListener("touchmove", move); window.removeEventListener("touchend", up); }; }, [valFromEvent, onChange]); const handleStart = (e) => { e.preventDefault(); dragRef.current = true; onChange?.(valFromEvent(e)); }; const quick = (delta) => (e) => { e.preventDefault(); e.stopPropagation(); onChange?.(Math.max(min, Math.min(max, value + delta))); }; return (
{label}
{value}{unit}
); } function SettingsSelect({ label, value, options, onChange }) { return (
{label}
{options.map(opt => ( ))}
); } function SettingsToggle({ label, value, onChange }) { return (
{label}
); } function BpmWidget({ label, value, onChange }) { const tapTimes = useRef([]); const handleTap = useCallback(() => { const now = Date.now(); tapTimes.current.push(now); if (tapTimes.current.length > 4) tapTimes.current.shift(); if (tapTimes.current.length < 2) return; const intervals = []; for (let i = 1; i < tapTimes.current.length; i++) { intervals.push(tapTimes.current[i] - tapTimes.current[i - 1]); } const avg = intervals.reduce((a, b) => a + b, 0) / intervals.length; const bpm = Math.max(20, Math.min(300, Math.round(60000 / avg))); onChange?.(bpm); }, [onChange]); return (
{label}
{value} BPM
); } // ── Global Settings ───────────────────────────────────────────────────────── export default function GlobalSettings({ onClose, onMasterVolume, onAudioProfile, onRoutingChange, onSaveSettings }) { const [values, setValues] = useState(buildDefaults()); const [loaded, setLoaded] = useState(false); useEffect(() => { let cancelled = false; Promise.all([ api("GET", "/api/state").catch(() => null), api("GET", "/api/audio/profile").catch(() => null), api("GET", "/api/routing").catch(() => null), ]).then(([state, audioProfile, routing]) => { if (cancelled) return; const overrides = {}; if (state) { if (state.master_volume != null) overrides.master_volume = Math.round(state.master_volume * 100); if (state.sample_rate != null) overrides.sample_rate = String(state.sample_rate); if (state.channel_mode) overrides.channel_mode = state.channel_mode; } if (audioProfile) { if (audioProfile.rate != null) overrides.sample_rate = String(audioProfile.rate); if (audioProfile.period != null) overrides.buffer_size = String(audioProfile.period); } if (routing) { if (routing.routing_mode) overrides.routing_mode = routing.routing_mode; if (routing.routing_breakpoint != null) overrides.routing_breakpoint = String(routing.routing_breakpoint); } setValues(prev => ({ ...prev, ...overrides })); setLoaded(true); }); return () => { cancelled = true; }; }, []); const groups = Object.entries(SETTINGS); const update = useCallback((key, val) => { setValues(prev => { const next = { ...prev, [key]: val }; // Route via dedicated endpoints if (key === "master_volume") onMasterVolume?.(val); else if (key === "sample_rate" || key === "buffer_size") { const rate = key === "sample_rate" ? val : (next.sample_rate || "48000"); const period = key === "buffer_size" ? val : (next.buffer_size || "512"); onAudioProfile?.(parseInt(rate), parseInt(period)); } else if (key === "routing_mode" || key === "routing_breakpoint") { const mode = key === "routing_mode" ? val : (next.routing_mode || "mono"); const bp = key === "routing_breakpoint" ? val : (next.routing_breakpoint || "7"); onRoutingChange?.(mode, parseInt(bp)); } else { // All other settings β†’ POST /api/settings (generic persistence) onSaveSettings?.({ [key]: val }); } return next; }); }, [onMasterVolume, onAudioProfile, onRoutingChange, onSaveSettings]); return (
Settings
{!loaded && Loading…}
{groups.map(([groupName, settings]) => (
{groupName}
{settings.map(s => (
{s.icon}
{s.type === "slider" && update(s.key, v)} />} {s.type === "select" && update(s.key, v)} />} {s.type === "toggle" && update(s.key, v)} />} {s.type === "bpm" && update(s.key, v)} />}
))}
))}
); }