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:
+233
-134
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user