fix: GlobalSettings loads current state from API on mount, add Routing & Audio Interface sections, wire onRoutingChange
This commit is contained in:
@@ -33,6 +33,9 @@ const API = {
|
|||||||
activatePreset:(bank, program) => api("POST", `/api/presets/${bank}/${program}/activate`),
|
activatePreset:(bank, program) => api("POST", `/api/presets/${bank}/${program}/activate`),
|
||||||
savePreset: (bank, program, name, tags) => api("PUT", "/api/presets", { bank, program, name, tags }),
|
savePreset: (bank, program, name, tags) => api("PUT", "/api/presets", { bank, program, name, tags }),
|
||||||
setAudioProfile:(rate, period) => api("POST", "/api/audio/profile", { profile: "stable", rate: parseInt(rate), period: parseInt(period) }),
|
setAudioProfile:(rate, period) => api("POST", "/api/audio/profile", { profile: "stable", rate: parseInt(rate), period: parseInt(period) }),
|
||||||
|
getAudioProfile:() => api("GET", "/api/audio/profile"),
|
||||||
|
getRouting: () => api("GET", "/api/routing"),
|
||||||
|
setRouting: (mode, breakpoint) => api("POST", "/api/routing", { routing_mode: mode, routing_breakpoint: breakpoint }),
|
||||||
searchModels: (q) => api("GET", `/api/tonehub/search?q=${encodeURIComponent(q)}`),
|
searchModels: (q) => api("GET", `/api/tonehub/search?q=${encodeURIComponent(q)}`),
|
||||||
searchIrs: (q) => api("GET", `/api/tonehub/search/irs?q=${encodeURIComponent(q)}`),
|
searchIrs: (q) => api("GET", `/api/tonehub/search/irs?q=${encodeURIComponent(q)}`),
|
||||||
installModel: (url, name) => api("POST", "/api/models/tonedownload/install", { download_url: url, name }),
|
installModel: (url, name) => api("POST", "/api/models/tonedownload/install", { download_url: url, name }),
|
||||||
@@ -1011,6 +1014,7 @@ export default function App(){
|
|||||||
onClose={()=>setSettingsView(null)}
|
onClose={()=>setSettingsView(null)}
|
||||||
onMasterVolume={(v)=>API.masterVolume(v).catch(()=>{})}
|
onMasterVolume={(v)=>API.masterVolume(v).catch(()=>{})}
|
||||||
onAudioProfile={(rate, period)=>API.setAudioProfile(rate, period).catch(()=>{})}
|
onAudioProfile={(rate, period)=>API.setAudioProfile(rate, period).catch(()=>{})}
|
||||||
|
onRoutingChange={(mode, bp)=>API.setRouting(mode, bp).catch(()=>{})}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
+113
-24
@@ -6,6 +6,15 @@ const T = {
|
|||||||
green: "#3AB87A", red: "#C84040", textPrimary: "#F0EDE6", textSec: "#8888A0", textDim: "#444458",
|
green: "#3AB87A", red: "#C84040", textPrimary: "#F0EDE6", textSec: "#8888A0", textDim: "#444458",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── API calls (injected via props or fetched internally) ──
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Settings definitions ──
|
// ── Settings definitions ──
|
||||||
const SETTINGS = {
|
const SETTINGS = {
|
||||||
"Audio": [
|
"Audio": [
|
||||||
@@ -42,6 +51,44 @@ const SETTINGS = {
|
|||||||
{ value: "68k", label: "68 kΩ" },
|
{ value: "68k", label: "68 kΩ" },
|
||||||
], icon: "⚡" },
|
], icon: "⚡" },
|
||||||
],
|
],
|
||||||
|
"Routing": [
|
||||||
|
{ key: "routing_mode", label: "Routing Mode", type: "select", def: "mono",
|
||||||
|
options: [
|
||||||
|
{ value: "mono", label: "Mono" },
|
||||||
|
{ value: "4cm", label: "4CM (Stereo FX Loop)" },
|
||||||
|
], icon: "🔀" },
|
||||||
|
{ 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: "↔" },
|
||||||
|
],
|
||||||
|
"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: "🎤" },
|
||||||
|
{ 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: "🔈" },
|
||||||
|
{ 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: "🔊" },
|
||||||
|
],
|
||||||
"MIDI": [
|
"MIDI": [
|
||||||
{ key: "midi_channel", label: "MIDI Channel", type: "select", def: "omni",
|
{ key: "midi_channel", label: "MIDI Channel", type: "select", def: "omni",
|
||||||
options: [
|
options: [
|
||||||
@@ -80,6 +127,17 @@ const SETTINGS = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Build defaults from SETTINGS definitions ──
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
// ── Slider Widget ──
|
// ── Slider Widget ──
|
||||||
function SettingsSlider({ label, value, min, max, unit, onChange }) {
|
function SettingsSlider({ label, value, min, max, unit, onChange }) {
|
||||||
const trackRef = useRef(null);
|
const trackRef = useRef(null);
|
||||||
@@ -291,36 +349,66 @@ function BpmWidget({ label, value, onChange }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Global Settings ──
|
// ── Global Settings ──
|
||||||
export default function GlobalSettings({ onClose, onMasterVolume, onAudioProfile }) {
|
export default function GlobalSettings({ onClose, onMasterVolume, onAudioProfile, onRoutingChange }) {
|
||||||
const [values, setValues] = useState(() => {
|
const [values, setValues] = useState(buildDefaults());
|
||||||
const init = {};
|
const [loaded, setLoaded] = useState(false);
|
||||||
for (const group of Object.values(SETTINGS)) {
|
|
||||||
for (const s of group) {
|
// ── Load current settings from API on mount ──
|
||||||
init[s.key] = s.def;
|
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 = {};
|
||||||
|
// From /api/state
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
// From /api/audio/profile (more accurate for rate/period)
|
||||||
return init;
|
if (audioProfile) {
|
||||||
});
|
if (audioProfile.rate != null) overrides.sample_rate = String(audioProfile.rate);
|
||||||
|
if (audioProfile.period != null) overrides.buffer_size = String(audioProfile.period);
|
||||||
|
}
|
||||||
|
// From /api/routing
|
||||||
|
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 groups = Object.entries(SETTINGS);
|
||||||
|
|
||||||
const update = useCallback((key, val) => {
|
const update = useCallback((key, val) => {
|
||||||
setValues(prev => ({ ...prev, [key]: val }));
|
setValues(prev => {
|
||||||
// Fire external callback for master volume
|
const next = { ...prev, [key]: val };
|
||||||
if (key === "master_volume") {
|
|
||||||
onMasterVolume?.(val);
|
// Fire external callbacks
|
||||||
}
|
if (key === "master_volume") {
|
||||||
// Fire external callback for audio profile changes
|
onMasterVolume?.(val);
|
||||||
if (key === "sample_rate" || key === "buffer_size") {
|
}
|
||||||
// Get the latest values — use the new val for the changed key
|
if (key === "sample_rate" || key === "buffer_size") {
|
||||||
setValues(prev => {
|
const rate = key === "sample_rate" ? val : (next.sample_rate || "48000");
|
||||||
const rate = key === "sample_rate" ? val : (prev.sample_rate || "48000");
|
const period = key === "buffer_size" ? val : (next.buffer_size || "512");
|
||||||
const period = key === "buffer_size" ? val : (prev.buffer_size || "512");
|
|
||||||
onAudioProfile?.(parseInt(rate), parseInt(period));
|
onAudioProfile?.(parseInt(rate), parseInt(period));
|
||||||
return prev;
|
}
|
||||||
});
|
if (key === "routing_mode" || key === "routing_breakpoint") {
|
||||||
}
|
const mode = key === "routing_mode" ? val : (next.routing_mode || "mono");
|
||||||
}, [onMasterVolume, onAudioProfile]);
|
const bp = key === "routing_breakpoint" ? val : (next.routing_breakpoint || "7");
|
||||||
|
onRoutingChange?.(mode, parseInt(bp));
|
||||||
|
}
|
||||||
|
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, [onMasterVolume, onAudioProfile, onRoutingChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -348,6 +436,7 @@ export default function GlobalSettings({ onClose, onMasterVolume, onAudioProfile
|
|||||||
</button>
|
</button>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: 15, fontWeight: 600 }}>Global Settings</div>
|
<div style={{ fontSize: 15, fontWeight: 600 }}>Global Settings</div>
|
||||||
|
{!loaded && <span style={{ fontSize: 10, color: T.textDim }}>Loading current values…</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user