import { invoke } from '@tauri-apps/api/core'; import { DSP } from '../../types'; import { Channel, InputChannel, OutputChannel, Ratio, DelayUnit, DiscreteFrequency, CrossoverFilter, PEQFilter, PEQBand, InputSource, } from './types'; import { SetDsps, NotifyFn, updateDSP, setPollingPaused } from '../../dsp/dspShared'; import { registerPollStopper } from '../../dsp/pollRegistry'; import { defaultDSPState, defaultMeters } from './types'; import { DELAY_RANGES } from './types'; type DSP408 = Extract; export type DSP408Api = ReturnType; // --- selection + polling state, private to this module ------------------- const selectedIds = new Set(); const pollIntervals = new Map>(); const ACTIVE_POLL_INTERVAL_MS = 1000; const BACKGROUND_POLL_INTERVAL_MS = 5000; function pollIntervalFor(id: number): number { return selectedIds.has(id) ? ACTIVE_POLL_INTERVAL_MS : BACKGROUND_POLL_INTERVAL_MS; } async function poll(dsp: DSP408, setDsps: SetDsps) { try { const meters = await invoke('get_meter_levels', { id: dsp.id }); updateDSP(setDsps, dsp.id, (d) => d.pollingPaused ? d : ({ ...d, meters, } as DSP408) ); } catch { // transient poll failures — don't notify } } function startPolling(dsp: DSP408, setDsps: SetDsps) { stopPolling(dsp.id); void poll(dsp, setDsps); const interval = setInterval(() => poll(dsp, setDsps), pollIntervalFor(dsp.id)); pollIntervals.set(dsp.id, interval); } function restartPollingIfActive(dsp: DSP408, setDsps: SetDsps) { // only reschedule if we're actually polling this dsp right now if (pollIntervals.has(dsp.id)) startPolling(dsp, setDsps); } export function stopPolling(id: number) { const interval = pollIntervals.get(id); if (interval) { clearInterval(interval); pollIntervals.delete(id); } } registerPollStopper('DSP408', stopPolling); // --- pause helper ---------------------------------------------------------- async function withPollingPaused( dsp: DSP408, setDsps: SetDsps, operation: () => Promise ): Promise { setPollingPaused(setDsps, dsp.id, true); try { return await operation(); } finally { setPollingPaused(setDsps, dsp.id, false); } } // --- factory ----------------------------------------------------------- export async function create(params: { name: string; ip: string; port: number; deviceId: number; demo: boolean; }): Promise { const id = await invoke('create_dsp408', { ip: params.ip, port: params.port, deviceId: params.deviceId, }); return { id, type: 'DSP408', name: params.name, ip: params.ip, port: params.port, deviceId: params.deviceId, demo: params.demo, status: 'disconnected', pollingPaused: true, state: defaultDSPState, meters: defaultMeters, }; } export function forDSP408(dsp: DSP408, setDsps: SetDsps, notify: NotifyFn) { async function updateDSPState(): Promise { try { const state = await invoke('get_dsp_state', { id: dsp.id }); updateDSP(setDsps, dsp.id, (d) => ({ ...d, state }) as DSP408); } catch (err) { notify(`Failed to fetch state: ${String(err)}`, 'error', dsp.name); throw err; } } return { type: 'DSP408' as const, setDSPState(updater: (dsp: DSP) => DSP) { updateDSP(setDsps, dsp.id, updater); }, setActive(active: boolean) { if (active) { selectedIds.add(dsp.id); } else { selectedIds.delete(dsp.id); } restartPollingIfActive(dsp, setDsps); }, updateDSPState, async connect() { if (dsp.demo) { updateDSP(setDsps, dsp.id, (d) => ({ ...d, status: 'connected', pollingPaused: false })); return; } updateDSP(setDsps, dsp.id, (d) => ({ ...d, status: 'connecting' })); try { await invoke('connect_dsp408', { id: dsp.id }); await updateDSPState(); updateDSP(setDsps, dsp.id, (d) => ({ ...d, status: 'connected', pollingPaused: false })); notify('Connected', 'success', dsp.name); startPolling(dsp, setDsps); } catch (err) { updateDSP(setDsps, dsp.id, (d) => ({ ...d, status: 'disconnected' })); notify(`Connection failed: ${String(err)}`, 'error', dsp.name); } }, async disconnect() { if (dsp.demo) { updateDSP(setDsps, dsp.id, (d) => ({ ...d, status: 'disconnected', pollingPaused: true, })); return; } stopPolling(dsp.id); try { await withPollingPaused(dsp, setDsps, async () => { await invoke('disconnect_dsp408', { id: dsp.id }); }); updateDSP(setDsps, dsp.id, (d) => ({ ...d, status: 'disconnected', pollingPaused: true, })); notify('Disconnected', 'success', dsp.name); } catch (err) { notify(`Disconnection failed: ${String(err)}`, 'error', dsp.name); } }, async recallPreset(presetIndex: number): Promise { if (dsp.demo) { updateDSP(setDsps, dsp.id, (d) => { if (!d.state) return d; return { ...d, state: { ...d.state, presets: { ...d.state.presets, current_index: presetIndex - 1, }, }, }; }); return true; } return withPollingPaused(dsp, setDsps, async () => { try { const success = await invoke('recall_preset', { id: dsp.id, presetIndex, }); if (!success) { notify('Failed to recall preset', 'error', dsp.name); return false; } await updateDSPState(); notify(`Preset ${presetIndex} recalled`, 'success', dsp.name); return true; } catch (err) { notify(`Preset recall failed: ${String(err)}`, 'error', dsp.name); return false; } }); }, async setCurrentPresetName(name: string): Promise { const paddedName = name.slice(0, 14).padEnd(14, ' '); const updatePresetName = () => { updateDSP(setDsps, dsp.id, (d) => { if (!d.state) return d; const presets = d.state.presets; const index = presets.current_index; return { ...d, state: { ...d.state, presets: { ...presets, names: presets.names.map((presetName, i) => i === index ? paddedName : presetName ), modified: presets.modified.map((modified, i) => (i === index ? true : modified)), }, }, }; }); }; if (dsp.demo) { updatePresetName(); return true; } return withPollingPaused(dsp, setDsps, async () => { try { const success = await invoke('set_current_preset_name', { id: dsp.id, name: paddedName, }); if (!success) { notify('Failed to rename preset', 'error', dsp.name); return false; } await updateDSPState(); notify('Preset renamed', 'success', dsp.name); return true; } catch (err) { notify(`Preset rename failed: ${String(err)}`, 'error', dsp.name); return false; } }); }, async setGain(channel: Channel, gain: number): Promise { const updateGain = () => { updateDSP(setDsps, dsp.id, (d) => { if (!d.state) return d; const cfg = d.state.current_config; if ('Input' in channel) { return { ...d, state: { ...d.state, current_config: { ...cfg, input_states: { ...cfg.input_states, [channel.Input]: { ...cfg.input_states[channel.Input], gain, }, }, }, }, }; } return { ...d, state: { ...d.state, current_config: { ...cfg, output_states: { ...cfg.output_states, [channel.Output]: { ...cfg.output_states[channel.Output], gain, }, }, }, }, }; }); }; if (dsp.demo) { updateGain(); return true; } return withPollingPaused(dsp, setDsps, async () => { try { const success = await invoke('set_channel_gain', { id: dsp.id, channel, db: gain, }); if (!success) { notify('Failed to set gain', 'error', dsp.name); return false; } await updateDSPState(); notify('Gain updated', 'success', dsp.name); return true; } catch (err) { notify(`Gain update failed: ${String(err)}`, 'error', dsp.name); return false; } }); }, async setMute(channel: Channel, muted: boolean): Promise { const updateMute = () => { updateDSP(setDsps, dsp.id, (d) => { if (!d.state) return d; const cfg = d.state.current_config; if ('Input' in channel) { const inputChannel = channel.Input; return { ...d, state: { ...d.state, current_config: { ...cfg, input_states: { ...cfg.input_states, [inputChannel]: { ...cfg.input_states[inputChannel], mute: muted, }, }, }, }, }; } const outputChannel = channel.Output; return { ...d, state: { ...d.state, current_config: { ...cfg, output_states: { ...cfg.output_states, [outputChannel]: { ...cfg.output_states[outputChannel], mute: muted, }, }, }, }, }; }); }; if (dsp.demo) { updateMute(); return true; } return withPollingPaused(dsp, setDsps, async () => { try { const success = await invoke('set_mute', { id: dsp.id, channel, muted, }); if (!success) { notify('Failed to change mute state', 'error', dsp.name); return false; } await updateDSPState(); notify(muted ? 'Muted' : 'Unmuted', 'success', dsp.name); return true; } catch (err) { notify(`Mute update failed: ${String(err)}`, 'error', dsp.name); return false; } }); }, async setInverse(channel: Channel, inverted: boolean): Promise { const updatePhase = () => { updateDSP(setDsps, dsp.id, (d) => { if (!d.state) return d; const cfg = d.state.current_config; if ('Input' in channel) { const inputChannel = channel.Input; return { ...d, state: { ...d.state, current_config: { ...cfg, input_states: { ...cfg.input_states, [inputChannel]: { ...cfg.input_states[inputChannel], phase_inverted: inverted, }, }, }, }, }; } const outputChannel = channel.Output; return { ...d, state: { ...d.state, current_config: { ...cfg, output_states: { ...cfg.output_states, [outputChannel]: { ...cfg.output_states[outputChannel], phase_inverted: inverted, }, }, }, }, }; }); }; if (dsp.demo) { updatePhase(); return true; } return withPollingPaused(dsp, setDsps, async () => { try { const success = await invoke('set_channel_inverse_gain', { id: dsp.id, channel, inverted, }); if (!success) { notify('Failed to change phase', 'error', dsp.name); return false; } await updateDSPState(); notify(inverted ? 'Phase inverted' : 'Phase normal', 'success', dsp.name); return true; } catch (err) { notify(`Phase update failed: ${String(err)}`, 'error', dsp.name); return false; } }); }, async setGate( inputChannel: InputChannel, threshold: number, attack: number, hold: number, release: number ): Promise { const updateGate = () => { updateDSP(setDsps, dsp.id, (d) => { if (!d.state) return d; const cfg = d.state.current_config; return { ...d, state: { ...d.state, current_config: { ...cfg, input_states: { ...cfg.input_states, [inputChannel]: { ...cfg.input_states[inputChannel], gate: { threshold, attack, hold, release, }, }, }, }, }, }; }); }; if (dsp.demo) { updateGate(); return true; } return withPollingPaused(dsp, setDsps, async () => { try { const success = await invoke('set_gate', { id: dsp.id, channel: inputChannel, threshold, attack, hold, release, }); if (!success) { notify('Failed to change gate settings', 'error', dsp.name); return false; } await updateDSPState(); notify('Gate updated', 'success', dsp.name); return true; } catch (err) { notify(`Gate update failed: ${String(err)}`, 'error', dsp.name); return false; } }); }, async setCompressor( outputChannel: OutputChannel, ratio: Ratio, attack: number, release: number, knee: number, threshold: number ): Promise { const updateCompressor = () => { updateDSP(setDsps, dsp.id, (d) => { if (!d.state) return d; const cfg = d.state.current_config; return { ...d, state: { ...d.state, current_config: { ...cfg, output_states: { ...cfg.output_states, [outputChannel]: { ...cfg.output_states[outputChannel], compressor: { ratio, attack, release, knee, threshold, }, }, }, }, }, }; }); }; if (dsp.demo) { updateCompressor(); return true; } return withPollingPaused(dsp, setDsps, async () => { try { const success = await invoke('set_compressor', { id: dsp.id, channel: outputChannel, ratio, attack, release, knee, threshold, }); if (!success) { notify('Failed to change compressor settings', 'error', dsp.name); return false; } await updateDSPState(); notify('Compressor updated', 'success', dsp.name); return true; } catch (err) { notify(`Compressor update failed: ${String(err)}`, 'error', dsp.name); return false; } }); }, async setLimiter( outputChannel: OutputChannel, threshold: number, attack: number, release: number ): Promise { const updateLimiter = () => { updateDSP(setDsps, dsp.id, (d) => { if (!d.state) return d; const cfg = d.state.current_config; return { ...d, state: { ...d.state, current_config: { ...cfg, output_states: { ...cfg.output_states, [outputChannel]: { ...cfg.output_states[outputChannel], limiter: { threshold, attack, release, }, }, }, }, }, }; }); }; if (dsp.demo) { updateLimiter(); return true; } return withPollingPaused(dsp, setDsps, async () => { try { const success = await invoke('set_limiter', { id: dsp.id, channel: outputChannel, threshold, attack, release, }); if (!success) { notify('Failed to change limiter settings', 'error', dsp.name); return false; } await updateDSPState(); notify('Limiter updated', 'success', dsp.name); return true; } catch (err) { notify(`Limiter update failed: ${String(err)}`, 'error', dsp.name); return false; } }); }, async setDelayUnit(unit: DelayUnit): Promise { const updateDelayUnit = () => { function convertDelayUnit(value: number, from: DelayUnit, to: DelayUnit): number { if (from === to) return value; const [, fromMax] = DELAY_RANGES[from]; const [, toMax] = DELAY_RANGES[to]; if (fromMax === 0) return 0; const ratio = value / fromMax; return ratio * toMax; } updateDSP(setDsps, dsp.id, (d) => { if (!d.state) return d; const currentConfig = d.state.current_config; const oldUnit = currentConfig.delay_unit; const convertAll = >(states: T): T => Object.fromEntries( Object.entries(states).map(([channel, s]) => [ channel, { ...s, delay: convertDelayUnit(s.delay, oldUnit, unit) }, ]) ) as T; return { ...d, state: { ...d.state, current_config: { ...currentConfig, delay_unit: unit, input_states: convertAll(currentConfig.input_states), output_states: convertAll(currentConfig.output_states), }, }, }; }); }; if (dsp.demo) { updateDelayUnit(); return true; } return withPollingPaused(dsp, setDsps, async () => { try { const success = await invoke('set_delay_unit', { id: dsp.id, unit, }); if (!success) { notify('Failed to change delay unit', 'error', dsp.name); return false; } await updateDSPState(); notify('Delay unit updated', 'success', dsp.name); return true; } catch (err) { notify(`Delay unit update failed: ${String(err)}`, 'error', dsp.name); return false; } }); }, async setDelay(channel: Channel, delay: number): Promise { const updateDelay = () => { updateDSP(setDsps, dsp.id, (d) => { if (!d.state) return d; const cfg = d.state.current_config; if ('Input' in channel) { return { ...d, state: { ...d.state, current_config: { ...cfg, input_states: { ...cfg.input_states, [channel.Input]: { ...cfg.input_states[channel.Input], delay, }, }, }, }, }; } return { ...d, state: { ...d.state, current_config: { ...cfg, output_states: { ...cfg.output_states, [channel.Output]: { ...cfg.output_states[channel.Output], delay, }, }, }, }, }; }); }; if (dsp.demo) { updateDelay(); return true; } return withPollingPaused(dsp, setDsps, async () => { try { const success = await invoke('set_delay', { id: dsp.id, channel, delay, unit: dsp.state.current_config.delay_unit, }); if (!success) { notify('Failed to change channel delay', 'error', dsp.name); return false; } await updateDSPState(); notify('Channel delay updated', 'success', dsp.name); return true; } catch (err) { notify(`Channel delay update failed: ${String(err)}`, 'error', dsp.name); return false; } }); }, async setMatrixRouting(output: OutputChannel, inputs: InputChannel[]): Promise { const updateRouting = () => { updateDSP(setDsps, dsp.id, (d) => { if (!d.state) return d; const cfg = d.state.current_config; return { ...d, state: { ...d.state, current_config: { ...cfg, output_states: { ...cfg.output_states, [output]: { ...cfg.output_states[output], matrix_routes: { ...cfg.output_states[output].matrix_routes, connected: inputs, }, }, }, }, }, }; }); }; if (dsp.demo) { updateRouting(); return true; } return withPollingPaused(dsp, setDsps, async () => { try { const success = await invoke('set_matrix_routing', { id: dsp.id, output, inputs, }); if (!success) { notify('Failed to change matrix routing', 'error', dsp.name); return false; } await updateDSPState(); notify('Matrix routing updated', 'success', dsp.name); return true; } catch (err) { notify(`Matrix routing update failed: ${String(err)}`, 'error', dsp.name); return false; } }); }, async setMatrixGain( inputChannel: InputChannel, outputChannel: OutputChannel, gain: number ): Promise { const updateMatrixGain = () => { updateDSP(setDsps, dsp.id, (d) => { if (!d.state) return d; const cfg = d.state.current_config; const outputState = cfg.output_states[outputChannel]; const inputIndex = Object.values(InputChannel).indexOf(inputChannel); return { ...d, state: { ...d.state, current_config: { ...cfg, output_states: { ...cfg.output_states, [outputChannel]: { ...outputState, matrix_routes: { ...outputState.matrix_routes, gains: outputState.matrix_routes.gains.map((g, i) => i === inputIndex ? gain : g ), }, }, }, }, }, }; }); }; if (dsp.demo) { updateMatrixGain(); return true; } return withPollingPaused(dsp, setDsps, async () => { try { const success = await invoke('set_matrix_gain', { id: dsp.id, inputChannel, outputChannel, gain, }); if (!success) { notify('Failed to change matrix gain', 'error', dsp.name); return false; } await updateDSPState(); notify('Matrix gain updated', 'success', dsp.name); return true; } catch (err) { notify(`Matrix gain update failed: ${String(err)}`, 'error', dsp.name); return false; } }); }, async setGeqBand( inputChannel: InputChannel, frequencyBand: DiscreteFrequency, gain: number ): Promise { const updateGeqBand = () => { updateDSP(setDsps, dsp.id, (d) => { if (!d.state) return d; const cfg = d.state.current_config; const inputState = cfg.input_states[inputChannel]; return { ...d, state: { ...d.state, current_config: { ...cfg, input_states: { ...cfg.input_states, [inputChannel]: { ...inputState, geq: { ...inputState.geq, gains: { ...inputState.geq.gains, [frequencyBand]: gain, }, }, }, }, }, }, }; }); }; if (dsp.demo) { updateGeqBand(); return true; } return withPollingPaused(dsp, setDsps, async () => { try { const success = await invoke('set_geq_band', { id: dsp.id, inputChannel, frequencyBand, gain, }); if (!success) { notify('Failed to change GEQ band', 'error', dsp.name); return false; } await updateDSPState(); notify('GEQ band updated', 'success', dsp.name); return true; } catch (err) { notify(`GEQ band update failed: ${String(err)}`, 'error', dsp.name); return false; } }); }, async setGeqBypass(inputChannel: InputChannel, bypass: boolean): Promise { const updateGeqBypass = () => { updateDSP(setDsps, dsp.id, (d) => { if (!d.state) return d; const cfg = d.state.current_config; const inputState = cfg.input_states[inputChannel]; return { ...d, state: { ...d.state, current_config: { ...cfg, input_states: { ...cfg.input_states, [inputChannel]: { ...inputState, geq: { ...inputState.geq, bypass, }, }, }, }, }, }; }); }; if (dsp.demo) { updateGeqBypass(); return true; } return withPollingPaused(dsp, setDsps, async () => { try { const success = await invoke('set_geq_bypass', { id: dsp.id, inputChannel, bypass, }); if (!success) { notify('Failed to change GEQ bypass', 'error', dsp.name); return false; } await updateDSPState(); notify(bypass ? 'GEQ bypass enabled' : 'GEQ bypass disabled', 'success', dsp.name); return true; } catch (err) { notify(`GEQ bypass update failed: ${String(err)}`, 'error', dsp.name); return false; } }); }, async setLowPass( channel: Channel, freq: number, filterType: CrossoverFilter ): Promise { const updateLowPass = () => { updateDSP(setDsps, dsp.id, (d) => { if (!d.state) return d; const cfg = d.state.current_config; const lowPass = { frequency: freq, filter_type: filterType, }; if ('Input' in channel) { return { ...d, state: { ...d.state, current_config: { ...cfg, input_states: { ...cfg.input_states, [channel.Input]: { ...cfg.input_states[channel.Input], low_pass: lowPass, }, }, }, }, }; } return { ...d, state: { ...d.state, current_config: { ...cfg, output_states: { ...cfg.output_states, [channel.Output]: { ...cfg.output_states[channel.Output], low_pass: lowPass, }, }, }, }, }; }); }; if (dsp.demo) { updateLowPass(); return true; } return withPollingPaused(dsp, setDsps, async () => { try { const success = await invoke('set_low_pass', { id: dsp.id, channel, freq, filterType, }); if (!success) { notify('Failed to set low-pass filter', 'error', dsp.name); return false; } await updateDSPState(); notify('Low-pass filter updated', 'success', dsp.name); return true; } catch (err) { notify(`Low-pass update failed: ${String(err)}`, 'error', dsp.name); return false; } }); }, async setHighPass( channel: Channel, freq: number, filterType: CrossoverFilter ): Promise { const updateHighPass = () => { updateDSP(setDsps, dsp.id, (d) => { if (!d.state) return d; const cfg = d.state.current_config; const highPass = { frequency: freq, filter_type: filterType, }; if ('Input' in channel) { return { ...d, state: { ...d.state, current_config: { ...cfg, input_states: { ...cfg.input_states, [channel.Input]: { ...cfg.input_states[channel.Input], high_pass: highPass, }, }, }, }, }; } return { ...d, state: { ...d.state, current_config: { ...cfg, output_states: { ...cfg.output_states, [channel.Output]: { ...cfg.output_states[channel.Output], high_pass: highPass, }, }, }, }, }; }); }; if (dsp.demo) { updateHighPass(); return true; } return withPollingPaused(dsp, setDsps, async () => { try { const success = await invoke('set_high_pass', { id: dsp.id, channel, freq, filterType, }); if (!success) { notify('Failed to set high-pass filter', 'error', dsp.name); return false; } await updateDSPState(); notify('High-pass filter updated', 'success', dsp.name); return true; } catch (err) { notify(`High-pass update failed: ${String(err)}`, 'error', dsp.name); return false; } }); }, async setPeqBand( channel: Channel, band: PEQBand, gainDb: number, freqHz: number, q: number, filterType: PEQFilter, bypass: boolean ): Promise { const updatePeqBand = () => { updateDSP(setDsps, dsp.id, (d) => { if (!d.state) return d; const cfg = d.state.current_config; const peqBand = { gain: gainDb, frequency: freqHz, q, filter_type: filterType, bypass, }; if ('Input' in channel) { const inputState = cfg.input_states[channel.Input]; return { ...d, state: { ...d.state, current_config: { ...cfg, input_states: { ...cfg.input_states, [channel.Input]: { ...inputState, peq_chain: { ...inputState.peq_chain, bands: { ...inputState.peq_chain.bands, [band]: peqBand, }, }, }, }, }, }, }; } const outputState = cfg.output_states[channel.Output]; return { ...d, state: { ...d.state, current_config: { ...cfg, output_states: { ...cfg.output_states, [channel.Output]: { ...outputState, peq_chain: { ...outputState.peq_chain, bands: { ...outputState.peq_chain.bands, [band]: peqBand, }, }, }, }, }, }, }; }); }; if (dsp.demo) { updatePeqBand(); return true; } return withPollingPaused(dsp, setDsps, async () => { try { const success = await invoke('set_peq_band', { id: dsp.id, channel, band, gainDb, freqHz, q, filterType, bypass, }); if (!success) { notify('Failed to set PEQ band', 'error', dsp.name); return false; } await updateDSPState(); notify('PEQ band updated', 'success', dsp.name); return true; } catch (err) { notify(`PEQ update failed: ${String(err)}`, 'error', dsp.name); return false; } }); }, async setPEQBypass(channel: Channel, bypass: boolean): Promise { const updateBypass = () => { updateDSP(setDsps, dsp.id, (d) => { if (!d.state) return d; const cfg = d.state.current_config; if ('Input' in channel) { return { ...d, state: { ...d.state, current_config: { ...cfg, input_states: { ...cfg.input_states, [channel.Input]: { ...cfg.input_states[channel.Input], peq_chain: { ...cfg.input_states[channel.Input].peq_chain, bypass, }, }, }, }, }, }; } return { ...d, state: { ...d.state, current_config: { ...cfg, output_states: { ...cfg.output_states, [channel.Output]: { ...cfg.output_states[channel.Output], peq_chain: { ...cfg.output_states[channel.Output].peq_chain, bypass, }, }, }, }, }, }; }); }; if (dsp.demo) { updateBypass(); return true; } return withPollingPaused(dsp, setDsps, async () => { try { const success = await invoke('set_bypass_peq', { id: dsp.id, channel, bypass, }); if (!success) { notify('Failed to set PEQ bypass', 'error', dsp.name); return false; } await updateDSPState(); notify(bypass ? 'PEQ bypass enabled' : 'PEQ bypass disabled', 'success', dsp.name); return true; } catch (err) { notify(`PEQ bypass failed: ${String(err)}`, 'error', dsp.name); return false; } }); }, async storeCurrentConfiguration(presetIndex: number, force: boolean): Promise { if (dsp.demo) { if (presetIndex === 0 && !force) { return false; } updateDSP(setDsps, dsp.id, (d) => { if (!d.state) return d; const currentIndex = d.state.presets.current_index; return { ...d, state: { ...d.state, presets: { ...d.state.presets, modified: d.state.presets.modified.map((modified, index) => index === currentIndex ? true : modified ), }, }, }; }); return true; } return withPollingPaused(dsp, setDsps, async () => { try { const success = await invoke('store_current_configuration', { id: dsp.id, presetIndex, force, }); if (!success) { return false; } await updateDSPState(); return true; } catch (err) { notify(`Store configuration failed: ${String(err)}`, 'error', dsp.name); return false; } }); }, async factoryReset(force: boolean): Promise { if (!force) { return false; } if (dsp.demo) { return true; } return withPollingPaused(dsp, setDsps, async () => { try { const success = await invoke('factory_reset', { id: dsp.id, force, }); if (!success) { return false; } await updateDSPState(); return true; } catch (err) { notify(`Factory reset failed: ${String(err)}`, 'error', dsp.name); return false; } }); }, async copyChannel(channel1: Channel, channel2: Channel): Promise { if (dsp.demo) { return true; } return withPollingPaused(dsp, setDsps, async () => { try { const success = await invoke('copy_channel', { id: dsp.id, channel1, channel2, }); if (!success) { return false; } await updateDSPState(); return true; } catch (err) { notify(`Channel copy failed: ${String(err)}`, 'error', dsp.name); return false; } }); }, async setInputSource(inputSource: InputSource): Promise { const updateInputSource = () => { updateDSP(setDsps, dsp.id, (d) => { if (!d.state) return d; return { ...d, state: { ...d.state, current_config: { ...d.state.current_config, input_source: inputSource, }, }, }; }); }; if (dsp.demo) { updateInputSource(); return true; } return withPollingPaused(dsp, setDsps, async () => { try { const success = await invoke('set_input_source', { id: dsp.id, inputSource, }); if (!success) { return false; } await updateDSPState(); return true; } catch (err) { notify(`Input source update failed: ${String(err)}`, 'error', dsp.name); return false; } }); }, async linkChannels(source: Channel, destinations: Channel[]): Promise { if (dsp.demo) { // TODO: update local channel link state return true; } return withPollingPaused(dsp, setDsps, async () => { try { const success = await invoke('link_channels', { id: dsp.id, source, destinations, }); if (!success) { return false; } await updateDSPState(); return true; } catch (err) { notify(`Channel linking failed: ${String(err)}`, 'error', dsp.name); return false; } }); }, async setChannelName(channel: Channel, name: string): Promise { const updateChannelName = () => { updateDSP(setDsps, dsp.id, (d) => { if (!d.state) return d; const cfg = d.state.current_config; if ('Input' in channel) { return { ...d, state: { ...d.state, current_config: { ...cfg, input_states: { ...cfg.input_states, [channel.Input]: { ...cfg.input_states[channel.Input], name, }, }, }, }, }; } return { ...d, state: { ...d.state, current_config: { ...cfg, output_states: { ...cfg.output_states, [channel.Output]: { ...cfg.output_states[channel.Output], name, }, }, }, }, }; }); }; if (dsp.demo) { updateChannelName(); return true; } return withPollingPaused(dsp, setDsps, async () => { try { const success = await invoke('set_channel_name', { id: dsp.id, channel, name, }); if (!success) { return false; } await updateDSPState(); return true; } catch (err) { notify(`Channel name update failed: ${String(err)}`, 'error', dsp.name); return false; } }); }, }; }