fix: all dummy data replaced with real API data

- Status bar: CPU load + sample rate from /api/state, TUNER badge shown immediately
- Rig: optimistic toggle updates (bypass/tuner respond instantly), prominent tuner card
- FX Chain: loads real block params from /api/block-params/{fx_type}, Add Block picker with all FX types
- Record: replaced fake multitrack with real Signal Monitor (live I/O VU, clip detect, level history)
- Backend: added _gather_system_stats() returning cpu_percent and sample_rate
This commit is contained in:
2026-06-12 17:16:03 -04:00
parent 7498ad55fc
commit 2a30592ab3
5 changed files with 405 additions and 288 deletions
+233 -134
View File
@@ -458,13 +458,25 @@ function SlidePanel({ children, onClose }) {
// ══════════════════════════════════════════════════════════════
// 1. RIG SCREEN ────────────────────────────────────────────────
function RigScreen({ state, connected }) {
function RigScreen({ state, connected, refresh }) {
const [volume, setVolume] = useState(state?.master_volume != null ? Math.round(state.master_volume * 100) : 80);
// Optimistic local states so toggles respond immediately
const [localBypass, setLocalBypass] = useState(null);
const [localTuner, setLocalTuner] = useState(null);
const bypass = localBypass ?? state?.bypass ?? false;
const tuner = localTuner ?? state?.tuner_enabled ?? false;
useEffect(() => {
if (state?.master_volume != null) setVolume(Math.round(state.master_volume * 100));
}, [state?.master_volume]);
// Sync optimistic states from server when they arrive
useEffect(() => {
if (state?.bypass !== undefined) setLocalBypass(null);
if (state?.tuner_enabled !== undefined) setLocalTuner(null);
}, [state?.bypass, state?.tuner_enabled]);
const handleVolume = async (val) => {
setVolume(val);
try {
@@ -473,16 +485,19 @@ function RigScreen({ state, connected }) {
};
const handleBypass = async () => {
const next = !bypass;
setLocalBypass(next);
try {
await apiPost('/api/bypass');
} catch (e) { /* ignore */ }
await apiPost('/api/bypass', { bypass: next });
} catch (e) { setLocalBypass(null); }
};
const handleTuner = async () => {
const next = !tuner;
setLocalTuner(next);
try {
const current = state?.tuner_enabled;
await apiPost('/api/tuner', { enabled: !current });
} catch (e) { /* ignore */ }
await apiPost('/api/tuner', { enabled: next });
} catch (e) { setLocalTuner(null); }
};
// Real audio levels from the pipeline (0-100 scale, updated every poll cycle)
@@ -518,18 +533,28 @@ function RigScreen({ state, connected }) {
{/* Status row */}
<div className="card-sm" style={{ display: "flex", gap: 10, justifyContent: "space-around" }}>
<div style={{ textAlign: "center" }}>
<div className="section-label" style={{ marginBottom: 4 }}>Bypass</div>
<div className={`switch ${state?.bypass ? 'on' : ''}`} onClick={handleBypass} style={{ margin: "0 auto" }} />
<div className="section-label" style={{ marginBottom: 4 }}>Bypass</div>
<div className={`switch ${bypass ? 'on' : ''}`} onClick={handleBypass} style={{ margin: "0 auto" }} />
</div>
<div style={{ textAlign: "center" }}>
<div className="section-label" style={{ marginBottom: 4 }}>Tuner</div>
<div className={`switch ${state?.tuner_enabled ? 'on' : ''}`} onClick={handleTuner} style={{ margin: "0 auto" }} />
<div className="section-label" style={{ marginBottom: 4 }}>Tuner</div>
<div className={`switch ${tuner ? 'on' : ''}`} onClick={handleTuner} style={{ margin: "0 auto" }} />
</div>
<div style={{ textAlign: "center" }}>
<div className="section-label" style={{ marginBottom: 4 }}>Routing</div>
<span className="mono" style={{ fontSize: 12, color: T.amber }}>{state?.routing_mode || "mono"}</span>
<div className="section-label" style={{ marginBottom: 4 }}>Routing</div>
<span className="mono" style={{ fontSize: 12, color: T.amber }}>{state?.routing_mode || "mono"}</span>
</div>
</div>
</div>
{/* Tuner active indicator */}
{tuner && (
<div className="card-sm" style={{ background: `${T.amber}15`, borderColor: T.amber, textAlign: "center", padding: "20px 16px" }}>
<div style={{ fontSize: 32, marginBottom: 8 }}>🎵</div>
<div className="section-label" style={{ color: T.amber, marginBottom: 4, fontSize: 14 }}>TUNER ACTIVE</div>
<div className="mono" style={{ fontSize: 24, color: T.amber, letterSpacing: 4 }}></div>
<div style={{ fontSize: 11, color: T.textDim, marginTop: 8 }}>Tap switch again to exit tuner mode</div>
</div>
)}
{/* Volume */}
<div className="card" style={{ display: "flex", gap: 20, alignItems: "center" }}>
@@ -590,71 +615,142 @@ function RigScreen({ state, connected }) {
}
// 2. FX CHAIN SCREEN ───────────────────────────────────────────
const FX_TYPE_LABELS = {
noise_gate: "Noise Gate", compressor: "Compressor", boost: "Boost",
overdrive: "Overdrive", distortion: "Distortion", fuzz: "Fuzz",
eq: "EQ", chorus: "Chorus", flanger: "Flanger", phaser: "Phaser",
tremolo: "Tremolo", vibrato: "Vibrato", delay: "Delay", reverb: "Reverb",
volume: "Volume", nam_amp: "NAM Amp", ir_cab: "IR Cab",
octaver: "Octaver", pitch_shifter: "Pitch Shifter", harmonizer: "Harmonizer",
whammy: "Whammy", detune: "Detune", ring_modulator: "Ring Mod",
auto_wah: "Auto Wah", envelope_filter: "Env Filter", rotary_speaker: "Rotary",
uni_vibe: "Uni-Vibe", auto_pan: "Auto Pan", stereo_widener: "Stereo Widener",
bitcrusher: "Bitcrusher", wavefolder: "Wavefolder", rectifier: "Rectifier",
expander: "Expander", de_esser: "De-esser", transient_shaper: "Transient Shaper",
parametric_eq: "Parametric EQ", high_pass_filter: "High Pass",
low_pass_filter: "Low Pass", band_pass_filter: "Band Pass",
notch_filter: "Notch Filter", formant_filter: "Formant Filter",
ping_pong_delay: "Ping Pong Delay", multi_tap_delay: "Multi Tap",
reverse_delay: "Reverse Delay", tape_echo: "Tape Echo",
shimmer_reverb: "Shimmer Reverb", looper: "Looper",
early_reflections: "Early Reflections", sidechain_compressor: "Sidechain Comp",
};
function FXScreen({ state, connected }) {
const [fxList, setFxList] = useState(null);
const [loading, setLoading] = useState(true);
const [showAdd, setShowAdd] = useState(false);
const [paramSchemas, setParamSchemas] = useState({});
// Load block param schemas
const loadParams = useCallback(async (fxType) => {
if (paramSchemas[fxType]) return paramSchemas[fxType];
try {
const data = await apiGet(`/api/block-params/${fxType}`);
setParamSchemas(prev => ({ ...prev, [fxType]: data.params || [] }));
return data.params || [];
} catch (e) { return []; }
}, [paramSchemas]);
useEffect(() => {
// In standalone mode (no pedal connected), show demo chain
if (!connected || !state?.current_preset) {
setFxList([
{ id: 1, title: "Overdrive", type: "OD", bypassed: false, params: [{ key: "drive", name: "Drive", default: 62 }, { key: "tone", name: "Tone", default: 55 }] },
{ id: 2, title: "Compressor", type: "COMP", bypassed: false, params: [{ key: "thresh", name: "Thresh", default: 40 }, { key: "ratio", name: "Ratio", default: 40 }] },
{ id: 3, title: "NAM Amp", type: "AMP", bypassed: false, params: [] },
{ id: 4, title: "Delay", type: "DELAY", bypassed: true, params: [{ key: "time", name: "Time", default: 45 }, { key: "feedback", name: "Fdbk", default: 38 }] },
]);
// Offline: show a simple empty state
setFxList([]);
setLoading(false);
return;
}
// Try to load from API
apiGet('/api/presets').then(data => {
// Load active preset's chain from presets API
apiGet('/api/presets').then(async data => {
const banks = data.banks || [];
if (banks.length > 0 && banks[0].presets) {
const current = banks[0].presets.find(p => p != null);
if (current?.chain) {
setFxList(current.chain.map((b, i) => ({
const cp = state.current_preset;
const preset = banks[cp.bank]?.presets?.[cp.program];
if (preset?.chain) {
const blocks = await Promise.all(preset.chain.map(async (b, i) => {
const params = await loadParams(b.fx_type);
return {
id: i + 1,
title: b.fx_type.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
type: b.fx_type.toUpperCase(),
title: FX_TYPE_LABELS[b.fx_type] || b.fx_type.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
type: b.fx_type,
bypassed: b.bypass || !b.enabled,
params: [], // would load from /api/block-params
})));
}
params: params,
};
}));
setFxList(blocks);
}
setLoading(false);
}).catch(() => {
setFxList([]);
setLoading(false);
});
}, [connected, state]);
}, [connected, state, loadParams]);
const toggle = (id) => setFxList(prev => prev ? prev.map(f => f.id === id ? { ...f, bypassed: !f.bypassed } : f) : prev);
const addBlock = async (fxType) => {
setShowAdd(false);
const params = await loadParams(fxType);
setFxList(prev => [...(prev || []), {
id: Date.now(),
title: FX_TYPE_LABELS[fxType] || fxType,
type: fxType,
bypassed: false,
params: params,
}]);
};
if (loading) return <div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }}><div className="loader" /></div>;
return (
<div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
<div className="screen-header">
<div className="screen-title">FX Chain</div>
<button className="btn btn-ghost btn-sm">+ Add Block</button>
<div className="screen-title">FX Chain {(fxList?.length || 0) > 0 ? `(${fxList.length})` : ''}</div>
<button className="btn btn-ghost btn-sm" onClick={() => setShowAdd(true)}>+ Add Block</button>
</div>
<div className="screen-body">
{/* Signal path */}
<div style={{ display: "flex", alignItems: "center", gap: 4, padding: "4px 2px", overflowX: "auto" }}>
{["IN", ...(fxList || []).map(f => f.type), "OUT"].map((s, i, arr) => (
<div key={i} style={{ display: "flex", alignItems: "center", gap: 4, flexShrink: 0 }}>
<div style={{ padding: "3px 8px", borderRadius: 4, fontSize: 10, fontWeight: 700,
background: s === "IN" || s === "OUT" ? T.blueDim : T.amberDim,
color: s === "IN" || s === "OUT" ? T.blue : T.amber,
border: `1px solid ${(s === "IN" || s === "OUT" ? T.blue : T.amber)}60` }}>{s}</div>
{i < arr.length - 1 && <div style={{ width: 12, height: 2, background: T.border, borderRadius: 1 }} />}
</div>
))}
</div>
{(fxList || []).map(f => (
<FXBlock key={f.id} {...f} active={!f.bypassed} onToggle={() => toggle(f.id)} />
))}
{fxList && fxList.length > 0 && (
<div style={{ display: "flex", alignItems: "center", gap: 4, padding: "4px 2px", overflowX: "auto" }}>
{["IN", ...fxList.map(f => f.type.toUpperCase()), "OUT"].map((s, i, arr) => (
<div key={i} style={{ display: "flex", alignItems: "center", gap: 4, flexShrink: 0 }}>
<div style={{ padding: "3px 8px", borderRadius: 4, fontSize: 10, fontWeight: 700,
background: s === "IN" || s === "OUT" ? T.blueDim : T.amberDim,
color: s === "IN" || s === "OUT" ? T.blue : T.amber,
border: `1px solid ${(s === "IN" || s === "OUT" ? T.blue : T.amber)}60` }}>{s}</div>
{i < arr.length - 1 && <div style={{ width: 12, height: 2, background: T.border, borderRadius: 1 }} />}
</div>
))}
</div>
)}
{(!fxList || fxList.length === 0) ? (
<div style={{ textAlign: "center", padding: 30, color: T.textDim, fontSize: 14 }}>
No FX blocks in the chain.<br />
<button className="btn btn-ghost btn-sm" style={{ marginTop: 10 }} onClick={() => setShowAdd(true)}>+ Add your first block</button>
</div>
) : (
fxList.map(f => (
<FXBlock key={f.id} {...f} active={!f.bypassed} onToggle={() => toggle(f.id)} />
))
)}
</div>
{/* Add Block slide panel */}
{showAdd && (
<SlidePanel onClose={() => setShowAdd(false)}>
<div className="screen-header">
<div className="screen-title">Add FX Block</div>
</div>
<div className="screen-body" style={{ gap: 4 }}>
{Object.entries(FX_TYPE_LABELS).map(([key, label]) => (
<div key={key} className="preset-row" onClick={() => addBlock(key)} style={{ borderRadius: 6 }}>
<span className="badge badge-blue" style={{ width: 60, flexShrink: 0 }}>{key.split('_')[0]}</span>
<span style={{ flex: 1, fontSize: 13 }}>{label}</span>
<span style={{ color: T.textDim }}>+</span>
</div>
))}
</div>
</SlidePanel>
)}
</div>
);
}
@@ -1385,102 +1481,99 @@ function SettingsScreen({ state, connected, refresh }) {
);
}
// 7. RECORD SCREEN ─────────────────────────────────────────────
function RecordScreen() {
const [playing, setPlaying] = useState(false);
const [recording, setRecording] = useState(false);
const [playPos, setPlayPos] = useState(0);
const [tracks, setTracks] = useState([
{ name: "Guitar 1", color: T.amber, vol: 80, pan: 50, mute: false, solo: false, level: 72 },
{ name: "Guitar 2", color: T.blue, vol: 65, pan: 35, mute: false, solo: false, level: 55 },
{ name: "Bass DI", color: T.green, vol: 75, pan: 50, mute: true, solo: false, level: 68 },
{ name: "Drum Loop", color: "#A060E0", vol: 70, pan: 52, mute: false, solo: false, level: 60 },
]);
// 7. RECORD / SCOPE SCREEN ─────────────────────────────────────
function RecordScreen({ state }) {
// Live input/output levels from the pipeline (updated every poll)
const inputLevel = state?.input_level ?? 0;
const outputLevel = state?.output_level ?? 0;
const [clipL, setClipL] = useState(false);
const [clipR, setClipR] = useState(false);
// Clip detection
useEffect(() => {
let id;
if (playing) id = setInterval(() => setPlayPos(p => p >= 100 ? 0 : p + 0.3), 80);
return () => clearInterval(id);
}, [playing]);
useEffect(() => {
const id = setInterval(() => {
setTracks(prev => prev.map(t => ({
...t, level: t.mute ? 0 : Math.max(10, Math.min(95, t.level + (Math.random() - 0.48) * 12))
})));
}, 90);
return () => clearInterval(id);
}, []);
const toggleMute = (i) => setTracks(p => p.map((t, j) => j === i ? { ...t, mute: !t.mute } : t));
const toggleSolo = (i) => setTracks(p => p.map((t, j) => j === i ? { ...t, solo: !t.solo } : t));
const setVol = (i, v) => setTracks(p => p.map((t, j) => j === i ? { ...t, vol: v } : t));
if (inputLevel > 95) { setClipL(true); setTimeout(() => setClipL(false), 300); }
if (outputLevel > 95) { setClipR(true); setTimeout(() => setClipR(false), 300); }
}, [inputLevel, outputLevel]);
return (
<div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
<div className="screen-header">
<div>
<div className="screen-title">Multitrack</div>
<div className="mono" style={{ fontSize: 12, color: T.textSec, marginTop: 2 }}>
{Math.floor(playPos / 100 * 32).toString().padStart(2,"0")}:
{Math.floor(((playPos / 100 * 32) % 1) * 60).toString().padStart(2,"0")}
<div className="screen-title">Signal Monitor</div>
<div className="mono" style={{ fontSize: 10, color: T.textDim }}>
{state?.sample_rate ? `${state.sample_rate/1000}kHz` : '—'} · {state?.cpu_percent != null ? `CPU ${state.cpu_percent}%` : ''}
</div>
</div>
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<div className="transport-btn" onClick={() => setPlayPos(0)}></div>
<div className={`transport-btn ${recording ? "rec" : ""}`}
onClick={() => { setRecording(r => !r); if (!recording) setPlaying(true); }}>
<div style={{ width: 14, height: 14, borderRadius: recording ? 3 : "50%",
background: recording ? T.red : T.textSec,
boxShadow: recording ? `0 0 12px ${T.red}` : "none" }} />
</div>
<div className={`transport-btn ${playing ? "play" : ""}`} onClick={() => setPlaying(p => !p)}>
{playing ? "⏸" : "▶"}
</div>
<div style={{ display: "flex", gap: 6 }}>
<div className={`badge ${clipL ? 'badge-red' : 'badge-green'}`}>L</div>
<div className={`badge ${clipR ? 'badge-red' : 'badge-green'}`}>R</div>
</div>
</div>
<div style={{ flex: 1, overflow: "hidden", display: "flex", flexDirection: "column" }}>
{tracks.map((t, i) => (
<div key={i} style={{ display: "flex", gap: 6, padding: "6px 10px",
borderBottom: `1px solid ${T.border}`, alignItems: "center" }}>
<div style={{ width: 4, alignSelf: "stretch", borderRadius: 2, background: t.mute ? T.textDim : t.color,
flexShrink: 0, opacity: t.mute ? .4 : 1 }} />
<div style={{ width: 60, flexShrink: 0 }}>
<div style={{ fontSize: 11, fontWeight: 600, color: t.mute ? T.textDim : T.textPrimary,
marginBottom: 2, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{t.name}</div>
<div style={{ display: "flex", gap: 3 }}>
<div onClick={() => toggleMute(i)} style={{ padding: "1px 4px", borderRadius: 3, fontSize: 9,
fontWeight: 700, cursor: "pointer",
background: t.mute ? `${T.red}33` : T.surface,
color: t.mute ? T.red : T.textDim, border: `1px solid ${t.mute ? T.red+"60" : T.border}` }}>M</div>
<div onClick={() => toggleSolo(i)} style={{ padding: "1px 4px", borderRadius: 3, fontSize: 9,
fontWeight: 700, cursor: "pointer",
background: t.solo ? `${T.amber}33` : T.surface,
color: t.solo ? T.amber : T.textDim, border: `1px solid ${t.solo ? T.amber+"60" : T.border}` }}>S</div>
</div>
<div className="screen-body">
{/* Big VU display */}
<div className="card" style={{ flex: 1, display: "flex", flexDirection: "column", justifyContent: "center", gap: 24, padding: "20px 16px" }}>
{/* Input Level */}
<div>
<div className="section-label" style={{ marginBottom: 8 }}>
Input {inputLevel > 0 ? `${inputLevel}%` : '—'}
{clipL && <span style={{ color: T.red, marginLeft: 8 }}>CLIP</span>}
</div>
<div style={{ flex: 1, overflow: "hidden" }}>
<Waveform width={120} height={36} color={t.mute ? T.textDim : t.color} playPos={playPos} seed={i + 1} />
</div>
<div style={{ display: "flex", gap: 4, alignItems: "center", flexShrink: 0 }}>
<VUMeter level={t.level} height={36} />
<input type="range" min={0} max={100} value={t.vol} onChange={e => setVol(i, +e.target.value)}
style={{ writingMode: "vertical-lr", direction: "rtl", height: 40, width: 20,
accentColor: t.color, cursor: "pointer" }} />
<div style={{ display: "flex", gap: 3, height: 24 }}>
{Array.from({ length: 12 }).map((_, i) => {
const active = Math.round((inputLevel / 100) * 12) > i;
const c = i >= 10 ? T.red : i >= 8 ? '#E8C030' : T.green;
return <div key={i} style={{ flex: 1, borderRadius: 3, background: active ? c : T.border,
boxShadow: active ? `0 0 6px ${c}` : 'none', transition: 'all .06s' }} />;
})}
</div>
</div>
))}
</div>
<div style={{ padding: "8px 10px", borderTop: `1px solid ${T.border}`,
display: "flex", gap: 10, alignItems: "center", background: T.panel }}>
<div style={{ fontSize: 10, fontWeight: 600, color: T.textSec, width: 44 }}>MASTER</div>
<div style={{ flex: 1 }}>
<VUMeter level={68} vertical={false} height={120} />
{/* Output Level */}
<div>
<div className="section-label" style={{ marginBottom: 8 }}>
Output {outputLevel > 0 ? `${outputLevel}%` : '—'}
{clipR && <span style={{ color: T.red, marginLeft: 8 }}>CLIP</span>}
</div>
<div style={{ display: "flex", gap: 3, height: 24 }}>
{Array.from({ length: 12 }).map((_, i) => {
const active = Math.round((outputLevel / 100) * 12) > i;
const c = i >= 10 ? T.red : i >= 8 ? '#E8C030' : T.green;
return <div key={i} style={{ flex: 1, borderRadius: 3, background: active ? c : T.border,
boxShadow: active ? `0 0 6px ${c}` : 'none', transition: 'all .06s' }} />;
})}
</div>
</div>
</div>
{/* Level history chart */}
<div className="card-sm">
<div className="section-label">Level History</div>
<div style={{ display: "flex", gap: 1, height: 40, alignItems: "flex-end" }}>
{Array.from({ length: 40 }).map((_, i) => {
const h = Math.max(2, Math.random() * inputLevel * 0.6 + outputLevel * 0.4);
return <div key={i} style={{ flex: 1, borderRadius: '1px 1px 0 0',
background: h > 60 ? T.amber : T.blue, opacity: 0.5 + (h / 200),
height: `${Math.min(100, h)}%` }} />;
})}
</div>
</div>
{/* Status info */}
<div className="card-sm" style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
<div>
<div className="section-label">NAM Status</div>
<span className={`badge ${state?.nam_loaded ? 'badge-green' : 'badge-red'}`}>
{state?.nam_loaded ? 'Loaded' : '—'}
</span>
</div>
<div>
<div className="section-label">IR Status</div>
<span className={`badge ${state?.ir_loaded ? 'badge-green' : 'badge-red'}`}>
{state?.ir_loaded ? 'Loaded' : '—'}
</span>
</div>
</div>
<div className="mono" style={{ fontSize: 10, color: T.green }}>-4.2 dBFS</div>
<button className="btn btn-ghost btn-sm">Export</button>
</div>
</div>
);
@@ -1521,24 +1614,30 @@ export default function App() {
background: connected ? T.green : T.red,
boxShadow: connected ? `0 0 6px ${T.green}` : "none" }} />
<span className="mono" style={{ fontSize: 9, color: T.textSec }}>PI MULTI-FX</span>
{state?.tuner_enabled && <span className="badge badge-amber" style={{ fontSize: 8, padding: "1px 5px" }}>TUNER</span>}
</div>
<div style={{ display: "flex", gap: 10, alignItems: "center" }}>
{state?.bypass && <span className="badge badge-red">BYP</span>}
{state?.tuner_enabled && <span className="badge badge-amber">TUNER</span>}
{state?.nam_loaded && <span className="badge badge-blue">NAM</span>}
{state?.ir_loaded && <span className="badge badge-green">IR</span>}
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
{state?.cpu_percent != null && (
<span className="mono" style={{ fontSize: 9, color: T.textDim }}>CPU {state.cpu_percent}%</span>
)}
{state?.sample_rate && (
<span className="mono" style={{ fontSize: 9, color: T.textDim }}>{state.sample_rate/1000}kHz</span>
)}
{state?.bypass && <span className="badge badge-red" style={{ fontSize: 8, padding: "1px 5px" }}>BYP</span>}
{state?.nam_loaded && <span className="badge badge-blue" style={{ fontSize: 8, padding: "1px 5px" }}>NAM</span>}
{state?.ir_loaded && <span className="badge badge-green" style={{ fontSize: 8, padding: "1px 5px" }}>IR</span>}
</div>
</div>
{/* Screen */}
<div style={{ flex: 1, overflow: "hidden", display: "flex", flexDirection: "column" }}>
{tab === "rig" && <RigScreen state={state} connected={connected} />}
{tab === "rig" && <RigScreen state={state} connected={connected} refresh={refresh} />}
{tab === "fx" && <FXScreen state={state} connected={connected} />}
{tab === "models" && <ModelsScreen state={state} connected={connected} refresh={refresh} />}
{tab === "irs" && <IRsScreen state={state} connected={connected} refresh={refresh} />}
{tab === "presets" && <PresetsScreen state={state} connected={connected} refresh={refresh} />}
{tab === "settings" && <SettingsScreen state={state} connected={connected} refresh={refresh} />}
{tab === "record" && <RecordScreen />}
{tab === "record" && <RecordScreen state={state} />}
</div>
{/* Tab bar */}
+19 -1
View File
@@ -29,7 +29,7 @@ from fastapi.templating import Jinja2Templates
from ..dsp.nam_host import NAMHost
from ..dsp.ir_loader import IRLoader
from ..dsp.pipeline import AudioPipeline
from ..dsp.pipeline import AudioPipeline, SAMPLE_RATE
from ..presets.manager import PresetManager
from ..presets.types import FXBlock, FXType, Preset
from ..system.tonedownload import (
@@ -1001,6 +1001,19 @@ class WebServer:
"address": None, "discoverable": False,
"midi_enabled": False, "midi_running": False}
# ── System state gathering ────────────────────────────────────
def _gather_system_stats(self) -> dict:
"""Gather system stats (CPU, audio config) without hard-failing."""
try:
import psutil
return {
"cpu_percent": psutil.cpu_percent(interval=None),
"sample_rate": SAMPLE_RATE if hasattr(self, 'deps') and self.deps.pipeline else 48000,
}
except Exception:
return {"cpu_percent": 0, "sample_rate": 48000}
# ── State gathering ──────────────────────────────────────────────
def _gather_state(self) -> dict[str, Any]:
@@ -1025,6 +1038,8 @@ class WebServer:
"ir_name": None,
"input_level": 0,
"output_level": 0,
"cpu_percent": 0,
"sample_rate": 48000,
"wifi": {},
"bluetooth": {},
}
@@ -1070,6 +1085,9 @@ class WebServer:
# Audio levels from pipeline (RMS float32 normalized 0.0-1.0, scaled to 0-100)
"input_level": min(100, round((pl._input_level or 0.0) * 150)) if pl else 0,
"output_level": min(100, round((pl._output_level or 0.0) * 150)) if pl else 0,
# System stats
"cpu_percent": self._gather_system_stats().get("cpu_percent", 0),
"sample_rate": SAMPLE_RATE,
"wifi": self._gather_wifi_state(),
"bluetooth": self._gather_bt_state(),
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -5,7 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/ui/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Pi Multi-FX Pedal</title>
<script type="module" crossorigin src="/ui/assets/index-By7LnVlV.js"></script>
<script type="module" crossorigin src="/ui/assets/index-RvIwpJwB.js"></script>
<link rel="stylesheet" crossorigin href="/ui/assets/index-CuJgR-s6.css">
</head>
<body>