Skip to main content

secs_types/
ipc.rs

1//! IPC between the screen and the service.
2//!
3//! The screen sends a [`Request`] as `{"method": "...", "params": {...}}` and
4//! always receives an envelope: `{"ok": true, "data": Response}` or
5//! `{"ok": false, "error": IpcError}`. The service never throws across this
6//! seam. Long jobs report progress through the event sink supplied with the call.
7//!
8//! Errors carry a code and parameters, not prose: the screen turns them into
9//! a sentence in the current language and keeps `detail` (the raw text) for
10//! the details view and the log.
11
12use std::collections::BTreeMap;
13
14use serde::{Deserialize, Serialize};
15use ts_rs::TS;
16
17use crate::calibration::{CalibrationSettings, Mode};
18use crate::compute::{ComputePreference, ComputeReport, ComputeStatus};
19pub use crate::settings_edit::CalibrationSettingsEditParams;
20
21/// A call from the screen.
22///
23/// The method names come from the variant names in one place: serde and
24/// strum both rename them to snake_case, and `method_names_match_the_wire`
25/// checks that the two agree.
26#[derive(
27    Debug, Clone, PartialEq, Serialize, Deserialize, TS, strum::VariantNames, strum::IntoStaticStr,
28)]
29#[serde(tag = "method", content = "params", rename_all = "snake_case")]
30#[strum(serialize_all = "snake_case")]
31#[ts(export)]
32pub enum Request {
33    /// Version, the upstream release this build is ported from, and the
34    /// platform. The first call the screen makes.
35    Bootstrap,
36    /// The `ProjectNew` Request variant.
37    ProjectNew(ProjectNewParams),
38    /// The `ProjectOpen` Request variant.
39    ProjectOpen(ProjectPathParams),
40    /// The `ProjectSave` Request variant.
41    ProjectSave(ProjectSaveParams),
42    /// The `ProjectInfo` Request variant.
43    ProjectInfo,
44    /// The `SampleLoad` Request variant.
45    SampleLoad(SampleLoadParams),
46    /// The `Calibrate` Request variant.
47    Calibrate(CalibrateParams),
48    /// The `RecommendXo` Request variant.
49    RecommendXo,
50    /// The `Mimo` Request variant.
51    Mimo(MimoParams),
52    /// The `Results` Request variant.
53    Results,
54    /// The `Export` Request variant.
55    Export(ExportParams),
56    /// The `JobCancel` Request variant.
57    JobCancel(JobCancelParams),
58    /// The `Jobs` Request variant.
59    Jobs,
60    /// The `MeasureMethods` Request variant.
61    MeasureMethods,
62    /// The `MeasureConfigure` Request variant.
63    MeasureConfigure(MeasureConfigureParams),
64    /// Returns the measurement settings stored in the open project.
65    MeasureConfigGet,
66    /// Returns one recording-status entry per arranged position.
67    MeasurementRecords,
68    /// Returns the project's microphone calibration and attachment counts.
69    MicCalibrationGet,
70    /// Reads, validates and stores a microphone calibration file in the project.
71    MicCalibrationSet(MicCalibrationSetParams),
72    /// Removes the project's selected microphone calibration file.
73    MicCalibrationClear,
74    /// Attaches the selected file to sweep measurements that have no mic curve.
75    MicCalibrationAttach,
76    /// Returns stored per-role level and SPL values.
77    LevelsGet,
78    /// Fills only missing per-role level values with upstream defaults.
79    LevelsFillDefaults,
80    /// Stops the current 2.5-second level burst without stopping its monitor.
81    LevelStop,
82    /// The `MeasurePosition` Request variant.
83    MeasurePosition(MeasurePositionParams),
84    /// The `PositionClear` Request variant.
85    PositionClear(PositionClearParams),
86    /// The `Undo` Request variant.
87    Undo(UndoParams),
88    /// The `RewLaunch` Request variant.
89    RewLaunch,
90    /// The `MdatScan` Request variant.
91    MdatScan(MdatScanParams),
92    /// The `MdatImport` Request variant.
93    MdatImport(MdatImportParams),
94    /// Returns the editable speaker configuration.
95    ConfigGet,
96    /// Replaces the speaker configuration with a built-in preset.
97    ConfigApplyPreset(ConfigPresetParams),
98    /// Changes the number of configured subwoofers.
99    ConfigSetSubCount(ConfigSubCountParams),
100    /// Changes one speaker or one of its bands.
101    ConfigSetSpeaker(ConfigSetSpeakerParams),
102    /// Adds the first unused upstream speaker role.
103    ConfigAddSpeaker,
104    /// Removes one configured speaker.
105    ConfigRemoveSpeaker(ConfigIndexParams),
106    /// Renumbers outputs in configuration order.
107    ConfigRenumber,
108    /// Returns structured configuration validation notices.
109    ConfigValidate,
110    /// Writes the upstream patch-sheet CSV.
111    PatchSheetCsv(PathParams),
112    /// Returns the editable DSP profile.
113    DspGet,
114    /// Applies a DSP preset or manual fields.
115    DspSet(DspSetParams),
116    /// Returns measurement arrangement and timing options.
117    LayoutGet,
118    /// Changes measurement arrangement or timing options.
119    LayoutSet(LayoutSetParams),
120    /// Returns the current per-position and total estimate.
121    PositionEstimate,
122    /// Returns the target curve and presets.
123    TargetGet,
124    /// Replaces target control points.
125    TargetSetPoints(TargetPointsParams),
126    /// Applies a built-in target preset.
127    TargetApplyPreset(TargetPresetParams),
128    /// Saves the target curve as upstream JSON.
129    TargetSave(PathParams),
130    /// Loads the target curve from upstream JSON.
131    TargetLoad(PathParams),
132    /// Returns calibration settings stored in the project.
133    CalibrationSettingsGet,
134    /// Stores calibration settings in the project.
135    CalibrationSettingsSet(CalibrationSettingsParams),
136    /// Applies one upstream calibration-screen edit and stores the settings.
137    CalibrationSettingsEdit(CalibrationSettingsEditParams),
138    /// Filter warnings from current settings (gain warnings require a run).
139    SubWarnings,
140    /// Optional REW facts read through the injected transport.
141    RewInfo,
142    /// Changes REW's audio driver and returns refreshed REW facts.
143    RewDriverSet(RewDriverSetParams),
144    /// Returns numeric capabilities for one calibration mode.
145    ModeSpec(ModeSpecParams),
146    /// Returns application-local settings.
147    AppSettingsGet,
148    /// Stores application-local settings.
149    AppSettingsSet(AppSettingsParams),
150    /// Reads CamillaDSP and Equalizer APO application state.
151    ApplyStatus,
152    /// Exports and applies the current design to CamillaDSP.
153    ApplyCamilla(ApplyCamillaParams),
154    /// Exports and applies the current design to Equalizer APO.
155    ApplyEqapo,
156    /// Removes SECS 2R's managed Equalizer APO include.
157    RevertEqapo,
158    /// Validates and stores application settings for both apply targets.
159    ApplySettingsSet(ApplySettings),
160    /// Installs the Windows managed host service with an elevation prompt.
161    HostInstall,
162    /// Stops and removes the Windows managed host service with an elevation prompt.
163    HostUninstall,
164    /// Clears every target persisted by the managed host service.
165    HostStopManaging,
166    /// Reports the application update state and the one-time "updated" notice.
167    UpdateStatus,
168    /// Checks for a newer release now, whatever the stored policy.
169    UpdateCheck,
170    /// Downloads the newer release found by the last check.
171    UpdateDownload,
172    /// Installs the downloaded release and starts the new version.
173    UpdateRestart,
174    /// Marks the one-time "updated" notice as shown.
175    UpdateNoticeDismiss,
176    /// Opens one release's page in the system browser.
177    UpdateOpenRelease(UpdateOpenReleaseParams),
178    /// Reports the opt-in local API server and token.
179    LocalApiStatus,
180    /// Replaces the local API bearer token and restarts its server when needed.
181    LocalApiTokenRegenerate,
182    /// Available compute devices and the effective application preference.
183    ComputeStatus,
184    /// Opens the official GPU runtime installation guide for this platform.
185    OpenGpuHelp,
186    /// Smoothed, normalized measurement curves and their shared frequency grid.
187    MeasurementCurves(MeasurementCurvesParams),
188    /// Ready-to-draw calibration curves and axis limits for one result tab.
189    ResultCurves(ResultCurvesParams),
190    /// Numeric metrics, filter counts, BA rows and coded result notices.
191    ResultLines,
192    /// Copies the last BA output alignment into the manual sub settings.
193    SubTakeValues,
194    /// Opens a project-file picker through the application host.
195    DialogOpenProject,
196    /// Opens a project save-file picker through the application host.
197    DialogSaveProject(DialogSuggestedParams),
198    /// Opens an mdat-file picker through the application host.
199    DialogPickMdat,
200    /// Opens a folder picker through the application host.
201    DialogPickFolder(DialogPickFolderParams),
202    /// Opens a typed save-file picker through the application host.
203    DialogSaveFile(DialogSaveFileParams),
204    /// Opens a typed file picker through the application host.
205    DialogOpenFile(DialogOpenFileParams),
206    /// Opens a folder in the operating-system file manager.
207    OpenFolder(OpenFolderParams),
208    /// Starts the live input level monitor.
209    LevelMonitorStart(LevelMonitorStartParams),
210    /// Stops the live input level monitor.
211    LevelMonitorStop,
212    /// Plays a role's level-check signal for 2.5 seconds.
213    LevelPlay(LevelPlayParams),
214    /// Starts watching measurement-method status.
215    StatusWatchStart,
216    /// Stops watching measurement-method status.
217    StatusWatchStop,
218    /// Returns the stored or derived export base folder.
219    ExportFolderGet,
220    /// Stores the export base folder in the open project.
221    ExportFolderSet(ExportFolderSetParams),
222}
223
224impl Request {
225    /// Every method name, as it appears on the wire, derived from the
226    /// variants. `secs-policy` checks that each one is registered in
227    /// `features.toml` as `ipc.<method>`.
228    pub const METHODS: &'static [&'static str] = <Self as strum::VariantNames>::VARIANTS;
229
230    /// The wire name of this request's method.
231    pub fn method(&self) -> &'static str {
232        self.into()
233    }
234}
235
236/// The successful result of a [`Request`], tagged with the same method name.
237#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
238#[serde(tag = "method", content = "data", rename_all = "snake_case")]
239#[ts(export)]
240pub enum Response {
241    /// The `Bootstrap` Response variant.
242    Bootstrap(BootstrapInfo),
243    /// The `ProjectNew` Response variant.
244    ProjectNew(ProjectInfo),
245    /// The `ProjectOpen` Response variant.
246    ProjectOpen(ProjectInfo),
247    /// The `ProjectSave` Response variant.
248    ProjectSave(ProjectInfo),
249    /// The `ProjectInfo` Response variant.
250    ProjectInfo(ProjectInfo),
251    /// The `SampleLoad` Response variant.
252    SampleLoad(JobStarted),
253    /// The `Calibrate` Response variant.
254    Calibrate(JobStarted),
255    /// The `RecommendXo` Response variant.
256    RecommendXo(JobStarted),
257    /// The `Mimo` Response variant.
258    Mimo(JobStarted),
259    /// The `Results` Response variant.
260    Results(ResultsSummary),
261    /// The `Export` Response variant.
262    Export(JobStarted),
263    /// The `JobCancel` Response variant.
264    JobCancel(JobSnapshot),
265    /// The `Jobs` Response variant.
266    Jobs(Vec<JobSnapshot>),
267    /// The `MeasureMethods` Response variant.
268    MeasureMethods(MeasureMethodsResult),
269    /// The `MeasureConfigure` Response variant.
270    MeasureConfigure(MeasureConfigResult),
271    /// Measurement settings stored in the open project.
272    MeasureConfigGet(MeasureConfigResult),
273    /// Recording status for every arranged position.
274    MeasurementRecords(MeasurementRecordsResult),
275    /// Selected microphone calibration and attachment counts.
276    MicCalibrationGet(MicCalibrationStatus),
277    /// Selected microphone calibration after storing a file.
278    MicCalibrationSet(MicCalibrationStatus),
279    /// Empty microphone calibration status after clearing the selection.
280    MicCalibrationClear(MicCalibrationStatus),
281    /// Microphone calibration status after attaching it to eligible sweeps.
282    MicCalibrationAttach(MicCalibrationStatus),
283    /// Stored per-role level and SPL values.
284    LevelsGet(LevelsResult),
285    /// Stored values after filling missing level defaults.
286    LevelsFillDefaults(LevelsResult),
287    /// Confirms that the current level burst was stopped.
288    LevelStop(()),
289    /// The `MeasurePosition` Response variant.
290    MeasurePosition(JobStarted),
291    /// The `PositionClear` Response variant.
292    PositionClear(UndoResult),
293    /// The `Undo` Response variant.
294    Undo(ProjectInfo),
295    /// The `RewLaunch` Response variant.
296    RewLaunch(RewStatusResult),
297    /// The `MdatScan` Response variant.
298    MdatScan(MdatScanResult),
299    /// The `MdatImport` Response variant.
300    MdatImport(JobStarted),
301    /// The `ConfigGet` Response variant.
302    ConfigGet(ConfigResult),
303    /// The `ConfigApplyPreset` Response variant.
304    ConfigApplyPreset(ConfigMutationResult),
305    /// The `ConfigSetSubCount` Response variant.
306    ConfigSetSubCount(ConfigMutationResult),
307    /// The `ConfigSetSpeaker` Response variant.
308    ConfigSetSpeaker(ConfigResult),
309    /// The `ConfigAddSpeaker` Response variant.
310    ConfigAddSpeaker(ConfigResult),
311    /// The `ConfigRemoveSpeaker` Response variant.
312    ConfigRemoveSpeaker(ConfigResult),
313    /// The `ConfigRenumber` Response variant.
314    ConfigRenumber(ConfigResult),
315    /// The `ConfigValidate` Response variant.
316    ConfigValidate(Vec<Notice>),
317    /// The `PatchSheetCsv` Response variant.
318    PatchSheetCsv(PathResult),
319    /// The `DspGet` Response variant.
320    DspGet(DspResult),
321    /// The `DspSet` Response variant.
322    DspSet(DspResult),
323    /// The `LayoutGet` Response variant.
324    LayoutGet(LayoutResult),
325    /// The `LayoutSet` Response variant.
326    LayoutSet(LayoutMutationResult),
327    /// The `PositionEstimate` Response variant.
328    PositionEstimate(PositionEstimateResult),
329    /// The `TargetGet` Response variant.
330    TargetGet(TargetResult),
331    /// The `TargetSetPoints` Response variant.
332    TargetSetPoints(TargetResult),
333    /// The `TargetApplyPreset` Response variant.
334    TargetApplyPreset(TargetResult),
335    /// The `TargetSave` Response variant.
336    TargetSave(PathResult),
337    /// The `TargetLoad` Response variant.
338    TargetLoad(TargetResult),
339    /// The `CalibrationSettingsGet` Response variant.
340    CalibrationSettingsGet(CalibrationSettings),
341    /// The `CalibrationSettingsSet` Response variant.
342    CalibrationSettingsSet(CalibrationSettings),
343    /// Settings after one edit.
344    CalibrationSettingsEdit(CalibrationSettings),
345    /// Current settings-only filter warnings.
346    SubWarnings(Vec<Notice>),
347    /// Current REW facts; unavailable fields are null.
348    RewInfo(RewInfoResult),
349    /// Current REW facts after changing its driver.
350    RewDriverSet(RewInfoResult),
351    /// The `ModeSpec` Response variant.
352    ModeSpec(ModeSpecResult),
353    /// The `AppSettingsGet` Response variant.
354    AppSettingsGet(AppSettings),
355    /// The `AppSettingsSet` Response variant.
356    AppSettingsSet(AppSettings),
357    /// Current state of both application targets.
358    ApplyStatus(ApplyStatus),
359    /// The CamillaDSP application job.
360    ApplyCamilla(JobStarted),
361    /// The Equalizer APO application job.
362    ApplyEqapo(JobStarted),
363    /// Confirms removal of the managed Equalizer APO include.
364    RevertEqapo(()),
365    /// Stored application settings for both targets.
366    ApplySettingsSet(ApplySettings),
367    /// Confirms that the Windows managed host service installer was started.
368    HostInstall(()),
369    /// Confirms that the Windows managed host service uninstaller was started.
370    HostUninstall(()),
371    /// Confirms that every managed target was cleared.
372    HostStopManaging(()),
373    /// Application update state.
374    UpdateStatus(UpdateStatus),
375    /// Update state after the check finished.
376    UpdateCheck(UpdateStatus),
377    /// Update state once the download started.
378    UpdateDownload(UpdateStatus),
379    /// Confirms that installing and restarting started.
380    UpdateRestart(()),
381    /// Update state without the notice.
382    UpdateNoticeDismiss(UpdateStatus),
383    /// Confirms that the release page was handed to the system browser.
384    UpdateOpenRelease(()),
385    /// Current local API server and token state.
386    LocalApiStatus(LocalApiStatus),
387    /// Local API state after replacing its bearer token.
388    LocalApiTokenRegenerate(LocalApiStatus),
389    /// Device capability and automatic-selection result.
390    ComputeStatus(ComputeStatus),
391    /// GPU installation documentation opened by the application host.
392    OpenGpuHelp(()),
393    /// Measurement curves on the common display grid.
394    MeasurementCurves(MeasurementCurvesResult),
395    /// Calibration tab curves, ranges and staleness.
396    ResultCurves(ResultCurvesResult),
397    /// Numeric summary lines and BA table.
398    ResultLines(ResultLinesResult),
399    /// Stored settings after taking the last sub alignment.
400    SubTakeValues(CalibrationSettings),
401    /// The chosen project path, or null when cancelled.
402    DialogOpenProject(Option<String>),
403    /// The chosen project destination, or null when cancelled.
404    DialogSaveProject(Option<String>),
405    /// The chosen mdat path, or null when cancelled.
406    DialogPickMdat(Option<String>),
407    /// The chosen folder, or null when cancelled.
408    DialogPickFolder(Option<String>),
409    /// The chosen save destination, or null when cancelled.
410    DialogSaveFile(Option<String>),
411    /// The chosen source path, or null when cancelled.
412    DialogOpenFile(Option<String>),
413    /// Confirms that the folder-open request reached the host.
414    OpenFolder(()),
415    /// The live level-monitor job.
416    LevelMonitorStart(JobStarted),
417    /// The stopped live level-monitor job, when one was running.
418    LevelMonitorStop(Option<JobSnapshot>),
419    /// Confirms that role playback was accepted.
420    LevelPlay(()),
421    /// The measurement-status watch job.
422    StatusWatchStart(JobStarted),
423    /// The stopped status-watch job, when one was running.
424    StatusWatchStop(Option<JobSnapshot>),
425    /// The current export base folder.
426    ExportFolderGet(ExportFolderResult),
427    /// The stored export base folder.
428    ExportFolderSet(ExportFolderResult),
429}
430
431/// Optional measurement selection; an absent selection includes every recorded pair.
432#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS, Default)]
433#[ts(export)]
434pub struct MeasurementCurvesParams {
435    /// Position numbers to include.
436    #[ts(type = "number[] | null")]
437    pub positions: Option<Vec<i64>>,
438    /// Speaker roles to include.
439    pub roles: Option<Vec<String>>,
440}
441/// A smoothed measurement curve shifted by the result's shared reference.
442#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
443#[ts(export)]
444pub struct MeasurementCurve {
445    /// Speaker role.
446    pub role: String,
447    /// Measurement position.
448    #[ts(type = "number")]
449    pub position: i64,
450    /// Configuration kind: main, sub or top.
451    pub kind: String,
452    /// Levels in dB on the shared grid.
453    pub values: Vec<f64>,
454}
455/// Display limits; both endpoints are data values, not logarithms.
456#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
457#[ts(export)]
458pub struct CurveAxes {
459    /// Frequency bounds in Hz.
460    pub frequency_hz: [f64; 2],
461    /// Level or delay bounds.
462    pub value: [f64; 2],
463    /// Vertical unit: db or ms.
464    pub unit: String,
465}
466/// Frequency band used to set zero dB on the measurement graph.
467#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
468#[serde(rename_all = "snake_case")]
469#[ts(export)]
470pub enum CurveReferenceBand {
471    /// 200–2000 Hz mean of a main speaker.
472    Mid,
473    /// 40–120 Hz mean of a subwoofer.
474    Sub,
475}
476/// The stored measurement that sets zero dB for every displayed curve.
477#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
478#[ts(export)]
479pub struct CurveReference {
480    /// Configured speaker role.
481    pub role: String,
482    /// Measurement position.
483    #[ts(type = "number")]
484    pub position: i64,
485    /// Frequency band used for the mean.
486    pub band: CurveReferenceBand,
487}
488/// Measurement graph data.
489#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
490#[ts(export)]
491pub struct MeasurementCurvesResult {
492    /// Shared 280-point logarithmic frequency grid in Hz.
493    pub grid: Vec<f64>,
494    /// One curve per recorded role and position.
495    pub curves: Vec<MeasurementCurve>,
496    /// Stored curve and band that set zero dB, or absent when nothing is measured.
497    pub reference: Option<CurveReference>,
498    /// Upstream default axis bounds.
499    pub axes: CurveAxes,
500}
501/// The calibration graph to display.
502#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
503#[serde(rename_all = "snake_case")]
504#[ts(export)]
505pub enum ResultTab {
506    /// Sound pressure level relative to the target reference.
507    Spl,
508    /// Group delay in milliseconds.
509    Gd,
510    /// Crossover preview.
511    Xo,
512    /// Bass alignment.
513    Ba,
514}
515/// Selection for one ready-to-draw result graph.
516#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
517#[ts(export)]
518pub struct ResultCurvesParams {
519    /// Graph tab.
520    pub tab: ResultTab,
521    /// Selected main role; absent selects the first available channel.
522    pub channel: Option<String>,
523    /// Crossover group; absent selects the selected channel's group.
524    pub group: Option<String>,
525    /// Dragged crossover in Hz; absent uses the configured value.
526    pub xo_hz: Option<f64>,
527    /// Apply input correction to the XO preview; defaults to true.
528    pub corrected: Option<bool>,
529}
530/// One named series on a result graph.
531#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
532#[ts(export)]
533pub struct ResultCurve {
534    /// Series identity: before, after, target, mimo, main, sub, sum_before or sum_after.
535    pub kind: String,
536    /// Speaker role for per-sub curves, otherwise absent.
537    pub role: Option<String>,
538    /// Values on the response's shared grid.
539    pub values: Vec<f64>,
540}
541/// A result graph whose calculations are entirely owned by the service.
542#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
543#[ts(export)]
544pub struct ResultCurvesResult {
545    /// Requested tab.
546    pub tab: ResultTab,
547    /// Shared frequency grid in Hz.
548    pub grid: Vec<f64>,
549    /// Ordered drawable series.
550    pub curves: Vec<ResultCurve>,
551    /// Upstream default axis bounds.
552    pub axes: CurveAxes,
553    /// Whether the retained result no longer matches current inputs.
554    pub stale: bool,
555    /// Whether the graph comes from a dedicated subs-only result.
556    pub subs_only: bool,
557    /// RC lower and upper bounds, with an unknown auto upper bound represented as null.
558    pub rc_band_hz: Option<(f64, Option<f64>)>,
559    /// Selected crossover frequency for the draggable line.
560    pub xo_hz: Option<f64>,
561}
562/// Values behind one channel's metrics and filters lines.
563#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
564#[ts(export)]
565pub struct ChannelResultLine {
566    /// Main role.
567    pub role: String,
568    /// Before and after percentage within one dB, 20–200 Hz.
569    pub within_1db_pct: [f64; 2],
570    /// Before and after group-delay spread, 20–100 Hz, in ms.
571    pub gd_spread_ms: [f64; 2],
572    /// Number of input PEQs.
573    pub peq_count: usize,
574    /// FIR taps, absent when no FIR is used.
575    pub fir_taps: Option<usize>,
576    /// Crossover frequency, absent without bass management.
577    pub xo_hz: Option<f64>,
578    /// Fresh, enabled MIMO support path count, absent when not shown.
579    pub mimo_support_count: Option<usize>,
580}
581/// One sub's output alignment and fixed filters.
582#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
583#[ts(export)]
584pub struct SubResultRow {
585    /// Sub role.
586    pub role: String,
587    /// Alignment mode code.
588    pub mode: String,
589    /// Output gain in dB.
590    pub gain_db: f64,
591    /// Output delay in ms.
592    pub delay_ms: f64,
593    /// Output polarity, plus or minus one.
594    pub polarity: i8,
595    /// Ordered all-pass frequency and Q pairs.
596    pub allpass: Vec<(f64, f64)>,
597    /// High-pass frequency and filter type.
598    pub high_pass: (f64, String),
599    /// Low-pass frequency and filter type.
600    pub low_pass: (f64, String),
601    /// Fixed and generated peaking filters as frequency, Q and gain.
602    pub peq: Vec<(f64, f64, f64)>,
603}
604/// A typed BA layout summary.
605#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
606#[ts(export)]
607pub struct SubLayoutLine {
608    /// Bundle name.
609    pub bundle: String,
610    /// Layout code, dba or multi.
611    pub kind: String,
612    /// All members.
613    pub members: Vec<String>,
614    /// Front row roles.
615    pub front: Vec<String>,
616    /// Rear row roles.
617    pub rear: Vec<String>,
618    /// Rear delay in ms.
619    pub rear_ms: f64,
620    /// Automatic rear delay in ms, when available.
621    pub auto_ms: Option<f64>,
622    /// Manual rear trim in ms.
623    pub trim_ms: f64,
624    /// Shared peaking filters.
625    pub peq: Vec<(f64, f64, f64)>,
626}
627/// BA metrics and table, independent of the selected main channel.
628#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
629#[ts(export)]
630pub struct SubResultLines {
631    /// Whether this BA result is stale.
632    pub stale: bool,
633    /// Whether a dedicated subs-only run supplies these values.
634    pub subs_only: bool,
635    /// Metric band in Hz.
636    pub band_hz: [f64; 2],
637    /// Before and after flatness in dB.
638    pub flatness_db: [f64; 2],
639    /// Before and after across-seat spread, absent for a single seat.
640    pub seat_spread_db: Option<[f64; 2]>,
641    /// Number of complete sub positions.
642    pub positions: usize,
643    /// Output rows in configuration order.
644    pub rows: Vec<SubResultRow>,
645    /// Bundle layouts.
646    pub layouts: Vec<SubLayoutLine>,
647    /// Filter and gain warning codes and their arguments.
648    pub warnings: Vec<Notice>,
649}
650/// Numeric calibration summary lines; no localized sentences cross IPC.
651#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
652#[ts(export)]
653pub struct ResultLinesResult {
654    /// Whether the full calibration result is stale.
655    pub stale: bool,
656    /// Per-main metrics and filter counts.
657    pub channels: Vec<ChannelResultLine>,
658    /// Total delay in milliseconds, absent before calibration.
659    pub total_delay_ms: Option<f64>,
660    /// Engine notes as codes and arguments.
661    pub notes: Vec<Notice>,
662    /// RC ceiling provenance and room-volume notices.
663    pub rc: Vec<Notice>,
664    /// BA metrics, table, layouts and warnings when at least two subs are available.
665    pub ba: Option<SubResultLines>,
666}
667
668/// What the screen needs before it draws anything.
669#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
670#[ts(export)]
671pub struct BootstrapInfo {
672    /// Product name shown in the title bar and the about view.
673    pub app_name: String,
674    /// The SECS 2R version (the workspace version).
675    pub version: String,
676    /// True while the major version is 0: every 0.x build is a prerelease.
677    pub prerelease: bool,
678    /// The SECS 2 release this build follows.
679    pub upstream: UpstreamBase,
680    /// The operating-system platform running this build.
681    pub platform: Platform,
682}
683/// The upstream SECS 2 release a build is ported from (`UPSTREAM.toml`).
684#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
685#[ts(export)]
686pub struct UpstreamBase {
687    /// Upstream product name from `UPSTREAM.toml`.
688    pub name: String,
689    /// Upstream repository URL from `UPSTREAM.toml`.
690    pub repository: String,
691    /// Upstream release version from `UPSTREAM.toml`.
692    pub version: String,
693    /// Forty-character upstream Git commit from `UPSTREAM.toml`.
694    pub commit: String,
695    /// Upstream snapshot date from `UPSTREAM.toml`.
696    pub date: String,
697}
698/// The operating-system platform running the service.
699#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
700#[ts(export)]
701pub struct Platform {
702    /// `windows`, `macos` or `linux` (`std::env::consts::OS`).
703    pub os: String,
704    /// `x86_64` or `aarch64` (`std::env::consts::ARCH`).
705    pub arch: String,
706}
707
708/// Inputs for creating a project.
709#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
710#[ts(export)]
711pub struct ProjectNewParams {
712    /// Destination `.secs` file path.
713    pub path: String,
714    /// Speaker-layout preset name.
715    pub preset: String,
716    /// Number of subwoofers.
717    pub sub_count: u8,
718    /// Whether to create the fixed 7.1.4 sample-room project.
719    pub sample: bool,
720    /// Whether this is a tutorial session using the sample room. The session
721    /// cannot be saved or exported; its measurements and results live in a
722    /// temporary folder that is removed when the session ends, and `path` is
723    /// only reported back.
724    #[serde(default)]
725    pub tutorial: bool,
726}
727/// Inputs for loading embedded or synthetic sample measurements.
728#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
729#[ts(export)]
730pub struct SampleLoadParams {
731    /// Synthetic position count, 2 to 17 (the largest layout); absent loads
732    /// the one-position embedded room. Other counts are refused with
733    /// `measurement_input` (`field = positions`).
734    pub positions: Option<u32>,
735}
736/// A project file path.
737#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
738#[ts(export)]
739pub struct ProjectPathParams {
740    /// Source `.secs` file path.
741    pub path: String,
742}
743/// Inputs for saving the open project.
744#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
745#[ts(export)]
746pub struct ProjectSaveParams {
747    /// Optional save-as `.secs` path; absent saves in place.
748    pub path: Option<String>,
749}
750/// Screen-facing information about the open project.
751#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
752#[ts(export)]
753pub struct ProjectInfo {
754    /// Open `.secs` file path.
755    pub path: String,
756    /// Project display name.
757    pub name: String,
758    /// Speaker-layout preset name.
759    pub preset: String,
760    /// Number of configured subwoofers.
761    pub sub_count: usize,
762    /// Measured position numbers.
763    pub positions: Vec<i64>,
764    /// Whether the in-memory project has unsaved changes.
765    pub dirty: bool,
766    /// Whether this open project is a non-writable tutorial session.
767    pub tutorial: bool,
768    /// Warnings produced while loading legacy project entries.
769    pub migration_warnings: Vec<MigrationWarning>,
770}
771/// A project migration warning code and affected entry.
772#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
773#[ts(export)]
774pub struct MigrationWarning {
775    /// Stable warning code translated by the screen.
776    pub code: String,
777    /// Project entry affected by the warning.
778    pub entry: String,
779}
780
781/// Optional settings for a calibration run.
782#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS, Default)]
783#[ts(export)]
784pub struct CalibrateParams {
785    /// Settings override; absent uses the open project.
786    pub settings: Option<CalibrationSettings>,
787    /// Designs the subwoofers only (upstream [서브만 계산]); the result is
788    /// kept apart and shown by the BA tab until the next full run.
789    #[serde(default)]
790    pub subs_only: bool,
791    /// Computes MIMO after the correction in the same job. Absent on the
792    /// wire means false, so the TypeScript field is optional.
793    #[serde(default)]
794    #[ts(as = "Option<bool>", optional)]
795    pub mimo: bool,
796}
797/// Optional settings for a MIMO run.
798#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS, Default)]
799#[ts(export)]
800pub struct MimoParams {
801    /// Settings override; absent uses the open project.
802    pub settings: Option<CalibrationSettings>,
803}
804
805/// A service-assigned long-running job identifier.
806#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, TS)]
807#[serde(transparent)]
808#[ts(export)]
809pub struct JobId(#[ts(type = "number")] pub u64);
810/// The operation performed by a long-running job.
811#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, TS)]
812#[serde(rename_all = "snake_case")]
813#[ts(export)]
814pub enum JobKind {
815    /// Records all configured roles at one measurement position.
816    Measure,
817    /// Loads embedded or deterministic synthetic sample measurements.
818    SampleLoad,
819    /// Imports assigned measurements from a REW mdat file.
820    MdatImport,
821    /// Computes a calibration design from project measurements.
822    Calibrate,
823    /// Scores crossover-frequency candidates.
824    RecommendXo,
825    /// Computes MIMO support paths.
826    Mimo,
827    /// Writes one selected export bundle.
828    Export,
829    /// Exports and applies the current design to a running target.
830    Apply,
831    /// Streams microphone levels and controls level-check playback.
832    LevelMonitor,
833    /// Watches measurement-method status changes.
834    StatusWatch,
835}
836/// The current lifecycle state of a job.
837#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
838#[serde(rename_all = "snake_case")]
839#[ts(export)]
840pub enum JobStatus {
841    /// The worker is still running and may accept cancellation.
842    Running,
843    /// Cancellation was requested and the worker has not stopped yet.
844    CancelRequested,
845    /// The worker completed and produced a result.
846    Succeeded,
847    /// The worker stopped with an error.
848    Failed,
849    /// The caller cancelled the operation.
850    Cancelled,
851}
852impl JobStatus {
853    /// Whether the job can no longer change state.
854    pub fn is_terminal(self) -> bool {
855        matches!(self, Self::Succeeded | Self::Failed | Self::Cancelled)
856    }
857}
858/// The immediate response after a job starts.
859#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
860#[ts(export)]
861pub struct JobStarted {
862    /// Identifier assigned to the job.
863    pub job: JobId,
864    /// Operation the job performs.
865    pub kind: JobKind,
866}
867/// The latest observable state of a job.
868#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
869#[ts(export)]
870pub struct JobSnapshot {
871    /// Identifier assigned to the job.
872    pub job: JobId,
873    /// Operation the job performs.
874    pub kind: JobKind,
875    /// Current lifecycle state.
876    pub status: JobStatus,
877    /// Whether a cancellation request can still prevent commit.
878    pub cancellable: bool,
879    /// Most recently reported stage progress.
880    pub progress: Option<JobProgress>,
881    /// Failure once finished unsuccessfully.
882    pub error: Option<IpcError>,
883}
884/// Progress within one named job stage.
885#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
886#[ts(export)]
887pub struct JobProgress {
888    /// Stable stage code translated by the screen.
889    pub stage: String,
890    #[ts(type = "number")]
891    /// Completed work units in this stage.
892    pub done: u64,
893    #[ts(type = "number")]
894    /// Total work units in this stage.
895    pub total: u64,
896}
897/// A pushed lifecycle event for a long-running job.
898#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
899#[serde(tag = "event", rename_all = "snake_case")]
900#[ts(export)]
901pub enum JobEvent {
902    /// Announces a newly accepted job and its operation.
903    Started { job: JobId, kind: JobKind },
904    /// Reports completed and total work units for a stable stage code.
905    Progress {
906        job: JobId,
907        stage: String,
908        #[ts(type = "number")]
909        done: u64,
910        #[ts(type = "number")]
911        total: u64,
912    },
913    /// Reports a presentation-neutral log code and template parameters.
914    Log {
915        job: JobId,
916        code: String,
917        params: BTreeMap<String, String>,
918    },
919    /// Reports a terminal status and an ephemeral operation result or error.
920    /// Calibration and MIMO carry only a marker; read `results` for their summary.
921    Finished {
922        job: JobId,
923        status: JobStatus,
924        result: Option<Box<JobResult>>,
925        error: Option<IpcError>,
926    },
927    /// Reports a live input level at the monitor cadence.
928    Meter {
929        job: JobId,
930        rms_dbfs: f64,
931        peak_dbfs: f64,
932        spl_db: Option<f64>,
933        clipping: bool,
934    },
935    /// Reports a changed measurement-method status snapshot.
936    MeasureStatus {
937        job: JobId,
938        status: MeasureMethodsResult,
939    },
940}
941/// The successful result produced by a job.
942#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
943#[serde(tag = "kind", content = "data", rename_all = "snake_case")]
944#[ts(export)]
945pub enum JobResult {
946    /// Calibration committed; fetch the current screen summary with `results`.
947    Calibration,
948    /// Crossover recommendation computed for the current project.
949    Xo(XoRecommendation),
950    /// MIMO committed; fetch the current screen summary with `results`.
951    Mimo,
952    /// Paths written by an exporter.
953    Export(ExportResult),
954    /// A design applied to CamillaDSP or Equalizer APO.
955    Apply(ApplyResult),
956    /// Project state after a completed measurement.
957    Measurement(ProjectInfo),
958    /// Project state after an mdat import.
959    MdatImport(ProjectInfo),
960    /// A long-lived monitor or watcher stopped without a payload.
961    Stopped,
962}
963/// The job to cancel.
964#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
965#[ts(export)]
966pub struct JobCancelParams {
967    /// Identifier of the job to cancel.
968    pub job: JobId,
969}
970
971/// The latest calibration and MIMO results.
972#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
973#[ts(export)]
974pub struct ResultsSummary {
975    /// Latest calibration result, including stale state.
976    pub calibration: Option<CalibrationSummary>,
977    /// Latest MIMO result, including stale state.
978    pub mimo: Option<MimoSummary>,
979    /// Whether the MIMO result goes into an export, and if not, why.
980    pub mimo_export: MimoExportDecision,
981}
982/// Whether the MIMO result goes into an export, and if not, why.
983#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
984#[serde(rename_all = "snake_case")]
985#[ts(export)]
986pub enum MimoExportDecision {
987    /// The current MIMO support paths go into the export.
988    Included,
989    /// MIMO is switched off in the current settings.
990    Off,
991    /// No MIMO result has been computed or restored.
992    None,
993    /// The MIMO result does not match the current inputs or compute environment.
994    Stale,
995    /// MIMO needs at least three measured positions.
996    TooFewPositions,
997    /// The current MIMO result contains no support paths.
998    NoSupports,
999}
1000/// A presentation-neutral note or warning code with template parameters.
1001#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1002#[ts(export)]
1003pub struct Notice {
1004    /// Stable snake_case code derived from the engine note variant.
1005    pub code: String,
1006    /// String values for the screen message template.
1007    pub params: BTreeMap<String, String>,
1008}
1009
1010/// The screen-facing summary of one calibration design.
1011#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1012#[ts(export)]
1013pub struct CalibrationSummary {
1014    /// Whether this result was restored from the project cache instead of computed in this
1015    /// session; staged and before/after mdat need a live design.
1016    #[serde(default)]
1017    pub cached: bool,
1018    /// Whether current project inputs differ from this result.
1019    pub stale: bool,
1020    /// Which input first differs: measurements, settings or target.
1021    pub stale_reason: Option<String>,
1022    /// Calibration mode used to compute this result.
1023    pub mode: Mode,
1024    /// Ultra-mode latency limit used to compute this result, in milliseconds.
1025    pub ultra_ms: f64,
1026    /// Values for the upstream verdict card.
1027    pub verdict: VerdictSummary,
1028    /// Per-channel before-and-after metrics.
1029    pub scorecard: Vec<ChannelScore>,
1030    /// Late 20–150 Hz energy before and after correction at every measured position.
1031    #[serde(default)]
1032    pub late_energy: Vec<LateEnergy>,
1033    /// Total filter latency in milliseconds.
1034    pub latency_ms: f64,
1035    /// Informational codes and values translated by the screen.
1036    pub notes: Vec<Notice>,
1037    /// Main-channel roles in design order; filter coefficients stay in Rust.
1038    pub main_roles: Vec<String>,
1039    /// Subwoofer roles in design order; filter coefficients stay in Rust.
1040    pub sub_roles: Vec<String>,
1041}
1042/// Values shown by the upstream `step_calibration.py` verdict card.
1043#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1044#[ts(export)]
1045pub struct VerdictSummary {
1046    /// Share within ±1 dB before correction, in percent.
1047    pub within_1db_before_pct: f64,
1048    /// Share within ±1 dB after correction, in percent.
1049    pub within_1db_after_pct: f64,
1050    /// Low-band group-delay spread before correction, in milliseconds.
1051    pub low_band_time_spread_before_ms: f64,
1052    /// Low-band group-delay spread after correction, in milliseconds.
1053    pub low_band_time_spread_after_ms: f64,
1054    /// Delay added by correction, in milliseconds.
1055    pub added_delay_ms: f64,
1056    /// Across-position level spread after correction, in dB.
1057    pub position_spread_db: f64,
1058}
1059/// Before-and-after scorecard metrics for one speaker role.
1060#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1061#[ts(export)]
1062pub struct ChannelScore {
1063    /// Internal speaker role.
1064    pub role: String,
1065    /// Metrics before correction.
1066    pub before: MetricsSummary,
1067    /// Metrics after correction.
1068    pub after: MetricsSummary,
1069}
1070/// Late 20–150 Hz energy for one main role at one measured position.
1071#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1072#[ts(export)]
1073pub struct LateEnergy {
1074    /// Internal speaker role.
1075    pub role: String,
1076    /// Measured position number.
1077    pub position: u32,
1078    /// Late-to-early energy before correction, in dB.
1079    pub before_db: f64,
1080    /// Late-to-early energy after correction, in dB.
1081    pub after_db: f64,
1082}
1083/// The upstream `Metrics` fields for one response.
1084#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, TS)]
1085#[ts(export)]
1086pub struct MetricsSummary {
1087    /// Reference level from upstream `Metrics.level`, in dB.
1088    pub level: f64,
1089    /// Share within ±1 dB from 20 Hz, in percent.
1090    pub in1_20: f64,
1091    /// Share within ±2 dB from 20 Hz, in percent.
1092    pub in2_20: f64,
1093    /// Share within ±1 dB from 200 Hz, in percent.
1094    pub in1_200: f64,
1095    /// Share within ±2 dB from 200 Hz, in percent.
1096    pub in2_200: f64,
1097    /// Share within ±1 dB from 2,000 Hz, in percent.
1098    pub in1_2000: f64,
1099    /// Share within ±2 dB from 2,000 Hz, in percent.
1100    pub in2_2000: f64,
1101    /// Phase rotation from upstream `Metrics.turns`, in turns.
1102    pub turns: f64,
1103    /// Group-delay spread near 20 Hz, in milliseconds.
1104    pub gd_20: f64,
1105    /// Group-delay spread near 100 Hz, in milliseconds.
1106    pub gd_100: f64,
1107    /// Group-delay spread near 500 Hz, in milliseconds.
1108    pub gd_500: f64,
1109    /// Largest absolute 1/24-octave group-delay peak, in milliseconds.
1110    pub gd24_abs: f64,
1111    /// Frequency of the largest absolute group-delay peak, in Hz.
1112    pub gd24_abs_hz: f64,
1113    /// Largest signed 1/24-octave group-delay peak, in milliseconds.
1114    pub gd24_max: f64,
1115    /// Span of 1/24-octave group delay, in milliseconds.
1116    pub gd24_span: f64,
1117    /// Group-delay spread near 30 Hz, in milliseconds.
1118    pub gd_30: f64,
1119    /// Pre-ringing level from upstream `Metrics.pre_db`, in dB.
1120    pub pre_db: f64,
1121}
1122/// The screen-facing summary of one MIMO run.
1123#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1124#[ts(export)]
1125pub struct MimoSummary {
1126    /// Actual computation backend, retained with the result.
1127    #[serde(default)]
1128    pub compute: ComputeReport,
1129    /// Whether current calibration inputs differ from this MIMO result.
1130    pub stale: bool,
1131    /// Realization kind, `iir` or `fir`.
1132    pub kind: String,
1133    /// Upper support frequency in Hz.
1134    pub fsiso: f64,
1135    /// Number of generated support paths.
1136    pub support_count: usize,
1137    /// Total FIR-tap budget across all generated paths.
1138    pub fir_taps_total: usize,
1139    /// Informational codes and values translated by the screen.
1140    pub notes: Vec<Notice>,
1141    /// Number of distinct input roles with at least one generated support path.
1142    pub input_count: usize,
1143    /// Per-channel metrics after MIMO at the main position.
1144    #[serde(default)]
1145    pub scorecard: Vec<MimoChannelScore>,
1146    /// Late 20–150 Hz energy after MIMO at every measured position.
1147    #[serde(default)]
1148    pub late_energy: Vec<MimoLateEnergy>,
1149}
1150/// Scorecard metrics after MIMO for one main role.
1151#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1152#[ts(export)]
1153pub struct MimoChannelScore {
1154    /// Internal speaker role.
1155    pub role: String,
1156    /// Metrics after the MIMO support contributions are added.
1157    pub after_mimo: MetricsSummary,
1158}
1159/// Late 20–150 Hz energy after MIMO for one main role and measured position.
1160#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1161#[ts(export)]
1162pub struct MimoLateEnergy {
1163    /// Internal speaker role.
1164    pub role: String,
1165    /// Measured position number.
1166    pub position: u32,
1167    /// Late-to-early energy after MIMO, in dB.
1168    pub after_mimo_db: f64,
1169}
1170/// Crossover candidates and the best value for each group.
1171#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1172#[ts(export)]
1173pub struct XoRecommendation {
1174    /// Best crossover frequency in Hz for each speaker group.
1175    pub best: Vec<(String, f64)>,
1176    /// Evaluated crossover frequencies in Hz.
1177    pub candidates: Vec<f64>,
1178}
1179
1180/// The supported export bundle.
1181#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
1182#[serde(rename_all = "snake_case")]
1183#[ts(export)]
1184pub enum ExportKind {
1185    /// CamillaDSP configuration and FIR coefficient WAV files.
1186    CamillaDsp,
1187    /// REW mdat containing the upstream staged responses.
1188    StagedMdat,
1189    /// REW mdat containing the original measurements, the correction results
1190    /// or both ([`ExportParams::mdat_scope`]; upstream's before-and-after mdat).
1191    BeforeAfterMdat,
1192    /// Per-channel filter values as text plus FIR WAV files (upstream
1193    /// `calibration.export_filter_text`). Results with RC only.
1194    FilterText,
1195    /// A complete Equalizer APO configuration: `config.txt`, the filter file
1196    /// and FIR WAV files (upstream `calibration.export_eqapo`). RC, BA and
1197    /// MIMO support paths, which upstream refuses (docs/DEVIATIONS.md F12).
1198    EqualizerApo,
1199    /// One complete 48 kHz impulse response and response CSV per reachable
1200    /// input-to-output path, including RC, BA, LFE and MIMO operations.
1201    Convolution,
1202}
1203
1204/// Why a filter export cannot carry the current result (upstream
1205/// `calibration.filter_export_block`).
1206#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
1207#[serde(rename_all = "snake_case")]
1208#[ts(export)]
1209pub enum ExportBlock {
1210    /// The result has subs (BA); filter text carries RC only.
1211    Subs,
1212    /// The result includes MIMO; filter text does not carry it.
1213    Mimo,
1214    /// The result was restored from the cache and is stale; mdat needs the measurements' spectra,
1215    /// so it must be recalibrated first.
1216    StaleCache,
1217}
1218
1219impl ExportBlock {
1220    /// The `params.reason` value of an [`ErrorCode::ExportScope`] error.
1221    pub fn name(self) -> &'static str {
1222        match self {
1223            Self::Subs => "subs",
1224            Self::Mimo => "mimo",
1225            Self::StaleCache => "stale_cache",
1226        }
1227    }
1228}
1229
1230/// Upstream `filter_export_block`: whether `kind` can carry a result with
1231/// subs (`has_subs`) and MIMO support paths (`has_mimo`). CamillaDSP, the
1232/// mdat exports, Equalizer APO and convolution carry everything; filter text
1233/// carries RC only (subs are checked first, as upstream). Upstream stops MIMO at
1234/// Equalizer APO as well; its writer already routes the support paths, and
1235/// `secs-filters/tests/eqapo_playback.rs` plays them through the engine
1236/// (DEVIATIONS F12). The screen mirrors this rule (`screens/export/types.ts`).
1237pub fn filter_export_block(
1238    kind: ExportKind,
1239    has_subs: bool,
1240    has_mimo: bool,
1241) -> Option<ExportBlock> {
1242    match kind {
1243        ExportKind::FilterText if has_subs => Some(ExportBlock::Subs),
1244        ExportKind::FilterText if has_mimo => Some(ExportBlock::Mimo),
1245        _ => None,
1246    }
1247}
1248
1249/// Whether an export format can carry the current result. Stale cached
1250/// calibration blocks mdat; filter formats retain [`filter_export_block`].
1251pub fn export_block(
1252    kind: ExportKind,
1253    has_subs: bool,
1254    has_mimo: bool,
1255    stale_cache: bool,
1256) -> Option<ExportBlock> {
1257    match kind {
1258        ExportKind::StagedMdat | ExportKind::BeforeAfterMdat if stale_cache => {
1259            Some(ExportBlock::StaleCache)
1260        }
1261        _ => filter_export_block(kind, has_subs, has_mimo),
1262    }
1263}
1264/// What the REW mdat export ([`ExportKind::BeforeAfterMdat`]) carries
1265/// (docs/DEVIATIONS.md F19). The file name says which: `원 측정` for the
1266/// measurements, `보정 결과` for the computed responses.
1267#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
1268#[serde(rename_all = "snake_case")]
1269#[ts(export)]
1270pub enum MdatScope {
1271    /// The main position's before-and-after responses (upstream's file) and
1272    /// the original measurements of the other positions.
1273    #[default]
1274    All,
1275    /// The original measurements of every position, as stored; needs no
1276    /// correction result.
1277    Measurements,
1278    /// The computed responses at the main position only.
1279    Results,
1280}
1281
1282/// Sample formats accepted by CamillaDSP's Wasapi backend.
1283#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
1284#[ts(export)]
1285pub enum CamillaWasapiFormat {
1286    /// Signed 32-bit integer.
1287    #[serde(rename = "S32")]
1288    S32,
1289    /// Signed 24-bit integer.
1290    #[serde(rename = "S24")]
1291    S24,
1292    /// Signed 16-bit integer.
1293    #[serde(rename = "S16")]
1294    S16,
1295    /// 32-bit floating point.
1296    #[serde(rename = "F32")]
1297    F32,
1298}
1299
1300impl CamillaWasapiFormat {
1301    /// CamillaDSP configuration spelling.
1302    pub fn name(self) -> &'static str {
1303        match self {
1304            Self::S32 => "S32",
1305            Self::S24 => "S24",
1306            Self::S16 => "S16",
1307            Self::F32 => "F32",
1308        }
1309    }
1310}
1311
1312/// CamillaDSP capture backend. Channel counts come from the export plan.
1313#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
1314#[serde(tag = "type", rename_all = "snake_case")]
1315#[ts(export)]
1316pub enum CamillaCaptureDevice {
1317    /// File placeholder compatible with the existing export.
1318    #[default]
1319    File,
1320    /// Windows Audio Session API capture.
1321    Wasapi {
1322        /// Endpoint name; absent selects the system default.
1323        device: Option<String>,
1324        /// Sample representation.
1325        format: CamillaWasapiFormat,
1326        /// Request exclusive access.
1327        exclusive: bool,
1328        /// Capture a playback endpoint in shared mode.
1329        loopback: bool,
1330    },
1331    /// ASIO capture. Requires CamillaDSP's ASIO build (`camilladsp-windows-asio`).
1332    Asio {
1333        /// ASIO driver name.
1334        device: String,
1335    },
1336}
1337
1338/// CamillaDSP playback backend. Channel counts come from the export plan.
1339#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
1340#[serde(tag = "type", rename_all = "snake_case")]
1341#[ts(export)]
1342pub enum CamillaPlaybackDevice {
1343    /// File placeholder compatible with the existing export.
1344    #[default]
1345    File,
1346    /// Windows Audio Session API playback.
1347    Wasapi {
1348        /// Endpoint name; absent selects the system default.
1349        device: Option<String>,
1350        /// Sample representation.
1351        format: CamillaWasapiFormat,
1352        /// Request exclusive access.
1353        exclusive: bool,
1354    },
1355    /// ASIO playback. Requires CamillaDSP's ASIO build (`camilladsp-windows-asio`).
1356    Asio {
1357        /// ASIO driver name.
1358        device: String,
1359    },
1360}
1361
1362/// Capture and playback backends for a CamillaDSP export.
1363#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
1364#[ts(export)]
1365pub struct CamillaDevices {
1366    /// Capture backend.
1367    pub capture: CamillaCaptureDevice,
1368    /// Playback backend.
1369    pub playback: CamillaPlaybackDevice,
1370}
1371
1372/// The export bundle and destination folder.
1373#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1374#[ts(export)]
1375pub struct ExportParams {
1376    /// Export bundle to write.
1377    pub kind: ExportKind,
1378    /// Destination folder path. An empty path uses the project's export folder
1379    /// and applies the upstream timestamped subfolder convention.
1380    pub folder: String,
1381    /// What the REW mdat carries; absent means [`MdatScope::All`]. Only
1382    /// [`ExportKind::BeforeAfterMdat`] takes it; other kinds refuse it.
1383    #[serde(default, skip_serializing_if = "Option::is_none")]
1384    #[ts(optional)]
1385    pub mdat_scope: Option<MdatScope>,
1386    /// Optional stable export name. Named exports replace the matching bundle.
1387    #[serde(default, skip_serializing_if = "Option::is_none")]
1388    #[ts(optional)]
1389    pub design_name: Option<String>,
1390    /// Optional CamillaDSP Windows backends. File placeholders are the default.
1391    #[serde(default, skip_serializing_if = "Option::is_none")]
1392    #[ts(optional)]
1393    pub camilla_devices: Option<CamillaDevices>,
1394    /// Additional convolution output rates. Other export kinds refuse a non-empty list.
1395    #[serde(default)]
1396    #[ts(optional = nullable)]
1397    pub extra_rates: Vec<u32>,
1398}
1399/// Files written by a completed export job.
1400#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1401#[ts(export)]
1402pub struct ExportResult {
1403    /// Paths of all files written by the exporter.
1404    pub files: Vec<String>,
1405    /// Sample rate of the files in Hz. REW mdat always reports 48000.
1406    pub output_rate: u32,
1407    /// Informational notices produced while resolving the output rate.
1408    #[serde(default)]
1409    pub notes: Vec<Notice>,
1410    /// Actual export folder written by the job.
1411    pub folder: String,
1412    /// Whether the exported calibration differed from current project inputs.
1413    pub stale: bool,
1414}
1415
1416// P28 host, live-monitor, watch and export-folder types. Keep this block
1417// contiguous so the parallel M5 IPC additions merge without interleaving.
1418/// Optional suggested name for a save dialog.
1419#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
1420#[ts(export)]
1421pub struct DialogSuggestedParams {
1422    /// Suggested file name.
1423    pub suggested: String,
1424}
1425/// Optional starting folder for a folder picker.
1426#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
1427#[ts(export)]
1428pub struct DialogPickFolderParams {
1429    /// Starting folder, when known.
1430    pub start: Option<String>,
1431}
1432/// The kind of file a host save dialog accepts.
1433#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
1434#[serde(rename_all = "snake_case")]
1435#[ts(export)]
1436pub enum SaveFileKind {
1437    /// UTF-8 patch-sheet CSV.
1438    PatchSheet,
1439    /// Target-curve JSON.
1440    Target,
1441}
1442/// Typed save-file dialog parameters.
1443#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1444#[ts(export)]
1445pub struct DialogSaveFileParams {
1446    /// File type and filter.
1447    pub kind: SaveFileKind,
1448    /// Suggested file name.
1449    pub suggested: String,
1450}
1451/// The kind of file a host open dialog accepts.
1452#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
1453#[serde(rename_all = "snake_case")]
1454#[ts(export)]
1455pub enum OpenFileKind {
1456    /// Target-curve JSON.
1457    Target,
1458    /// Microphone response calibration text (`.txt`, `.cal` or `.frd`).
1459    MicCalibration,
1460    /// CamillaDSP executable selected for managed launching.
1461    CamillaExe,
1462}
1463/// Typed open-file dialog parameters.
1464#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1465#[ts(export)]
1466pub struct DialogOpenFileParams {
1467    /// File type and filter.
1468    pub kind: OpenFileKind,
1469}
1470/// Folder to reveal in the operating-system file manager.
1471#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1472#[ts(export)]
1473pub struct OpenFolderParams {
1474    /// Folder path.
1475    pub path: String,
1476}
1477/// Optional role to associate with a level-monitor start.
1478#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
1479#[ts(export)]
1480pub struct LevelMonitorStartParams {
1481    /// Speaker role initially selected for playback.
1482    pub role: Option<String>,
1483}
1484/// Role and output level for one 2.5-second level-check burst.
1485#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1486#[ts(export)]
1487pub struct LevelPlayParams {
1488    /// Internal speaker role.
1489    pub role: String,
1490    /// Master output level in dBFS.
1491    pub master_dbfs: f64,
1492}
1493/// Export base-folder update.
1494#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1495#[ts(export)]
1496pub struct ExportFolderSetParams {
1497    /// Folder path to store in the open project.
1498    pub folder: String,
1499}
1500/// Current export base folder and whether it came from project storage.
1501#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1502#[ts(export)]
1503pub struct ExportFolderResult {
1504    /// Export base folder.
1505    pub folder: String,
1506    /// True when `extra.export.folder` supplied this value.
1507    pub stored: bool,
1508}
1509
1510// Measurement request and response types.
1511/// A validated microphone calibration file selected for the open project.
1512#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1513#[ts(export)]
1514pub struct MicCalibrationFile {
1515    /// File name as selected by the user.
1516    pub name: String,
1517    /// Full selected path; the project retains the decoded text separately.
1518    pub path: String,
1519    /// miniDSP serial number from `SERNO`, when present.
1520    pub serial: Option<String>,
1521    /// USB microphone sensitivity factor in dB.
1522    pub sens_factor_db: Option<f64>,
1523    /// Analog input gain from `AGain`, in dB.
1524    pub analog_gain_db: Option<f64>,
1525    /// Analog microphone sensitivity at 94 dB SPL, in dBFS.
1526    pub sensitivity_dbfs: Option<f64>,
1527    /// Quoted metadata lines other than the sensitivity header.
1528    pub notes: Vec<String>,
1529    /// Number of frequency-response points.
1530    pub points: u32,
1531    /// Lowest tabulated frequency in Hz.
1532    pub min_hz: f64,
1533    /// Highest tabulated frequency in Hz.
1534    pub max_hz: f64,
1535    /// Whether any response point includes phase.
1536    pub has_phase: bool,
1537}
1538/// Selected microphone calibration and recorded-measurement attachment counts.
1539#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1540#[ts(export)]
1541pub struct MicCalibrationStatus {
1542    /// The project's selected file, or absent when none is selected.
1543    pub file: Option<MicCalibrationFile>,
1544    /// Recorded position-role pairs whose sidecar attaches a `meterCal` curve.
1545    pub attached: u32,
1546    /// Sweep measurements without a `meterCal` curve that can receive the file.
1547    pub attachable: u32,
1548}
1549/// A microphone calibration file selected by full path.
1550#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1551#[ts(export)]
1552pub struct MicCalibrationSetParams {
1553    /// Full path selected by the user.
1554    pub path: String,
1555}
1556
1557/// Measurement methods available on this host.
1558#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
1559#[ts(export)]
1560pub struct MeasureMethodsResult {
1561    /// Stable identifiers for available measurement methods.
1562    pub methods: Vec<String>,
1563    /// Native endpoints with their channel counts.
1564    pub native_devices: Vec<MeasureDevice>,
1565    /// Native enumeration failure, without hiding the other methods.
1566    pub native_error: Option<IpcError>,
1567    /// REW process, API, and license status.
1568    pub rew: RewStatusResult,
1569    /// Positions present in the unmodified sample room.
1570    pub mock_positions: Vec<i64>,
1571    /// Roles present in the unmodified sample room.
1572    pub mock_roles: Vec<String>,
1573}
1574/// An enumerated native audio endpoint.
1575#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1576#[ts(export)]
1577pub struct MeasureDevice {
1578    /// Backend endpoint identifier.
1579    pub id: String,
1580    /// Backend endpoint display name.
1581    pub name: String,
1582    /// Maximum number of input channels.
1583    pub input_channels: usize,
1584    /// Maximum number of output channels.
1585    pub output_channels: usize,
1586}
1587/// Measurement method and device settings.
1588#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS, Default)]
1589#[ts(export)]
1590pub struct MeasureConfigureParams {
1591    /// Measurement method identifier.
1592    pub method: String,
1593    /// Optional input device identifier.
1594    pub input_device: Option<String>,
1595    /// Optional output device identifier.
1596    pub output_device: Option<String>,
1597    /// Optional one-based input channel number.
1598    pub input_channel: Option<u16>,
1599    /// One-based output channels in measurement-plan role order; empty preserves wiring.
1600    #[serde(default)]
1601    pub output_channels: Vec<u16>,
1602    /// Optional sweep length in samples.
1603    pub sweep_length: Option<usize>,
1604    /// Optional sweep repetition count; allowed values are 1, 2, 4 and 8.
1605    pub repetitions: Option<u8>,
1606    /// Optional sweep level in dBFS.
1607    pub sweep_level_dbfs: Option<f64>,
1608    /// Optional timing-reference mode.
1609    pub timing: Option<String>,
1610    /// Optional one-based loopback input channel.
1611    pub loopback_input_channel: Option<u16>,
1612    /// Optional acoustic timing-reference output channel.
1613    pub timing_ref_output: Option<u16>,
1614    /// Optional share preference: auto, exclusive, or shared.
1615    pub share_mode: Option<String>,
1616}
1617/// The active measurement settings.
1618#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS, Default)]
1619#[ts(export)]
1620pub struct MeasureConfigResult {
1621    /// Active measurement method identifier.
1622    pub method: String,
1623    /// Active sweep level in dBFS.
1624    pub sweep_level_dbfs: Option<f64>,
1625    /// Active input endpoint identifier; empty selects the default.
1626    pub input_device: String,
1627    /// Active output endpoint identifier; empty selects the default.
1628    pub output_device: String,
1629    /// One-based microphone input channel.
1630    pub input_channel: u16,
1631    /// Roles in the same order as output_channels.
1632    pub roles: Vec<String>,
1633    /// One-based output channels in measurement-plan role order.
1634    pub output_channels: Vec<u16>,
1635    /// Sweep length in samples.
1636    pub sweep_length: usize,
1637    /// Active repetition count after timing-reference normalization.
1638    pub repetitions: u8,
1639    /// Sweep sample rate in Hz.
1640    pub sample_rate: u32,
1641    /// Supported sweep lengths in samples.
1642    pub sweep_lengths: Vec<usize>,
1643    /// Timing-reference mode.
1644    pub timing: String,
1645    /// Optional one-based loopback input channel.
1646    pub loopback_input_channel: Option<u16>,
1647    /// Optional acoustic timing-reference output channel.
1648    pub timing_ref_output: Option<u16>,
1649    /// Share preference: auto, exclusive, or shared.
1650    pub share_mode: String,
1651}
1652/// Recording status and upstream source values for one arranged position.
1653#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1654#[ts(export)]
1655pub struct PositionRecord {
1656    /// One-based arranged position number.
1657    #[ts(type = "number")]
1658    pub position: i64,
1659    /// Whether every planned role is recorded at this position.
1660    pub done: bool,
1661    /// Planned roles that have a recording at this position.
1662    pub roles: Vec<String>,
1663    /// Source file/value by recorded role; native and REW sweeps use `measured`.
1664    pub sources: BTreeMap<String, String>,
1665}
1666/// Recording status for the complete measurement arrangement.
1667#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1668#[ts(export)]
1669pub struct MeasurementRecordsResult {
1670    /// One entry per arranged position.
1671    pub records: Vec<PositionRecord>,
1672}
1673/// Stored level-check readings, keyed by measurement role (upstream `extra['levels']`).
1674#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1675#[ts(export)]
1676pub struct LevelsResult {
1677    /// Measured input level in dBFS by role: the meter reading while that channel played.
1678    pub levels: BTreeMap<String, f64>,
1679    /// Measured sound-pressure level in dB SPL by role.
1680    pub spl: BTreeMap<String, f64>,
1681}
1682/// The measurement position to record.
1683#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1684#[ts(export)]
1685pub struct MeasurePositionParams {
1686    /// One-based measurement position number.
1687    pub position: i64,
1688}
1689/// The measurement position to clear.
1690#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1691#[ts(export)]
1692pub struct PositionClearParams {
1693    /// One-based measurement position number.
1694    pub position: i64,
1695}
1696/// A service-issued undo token.
1697#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1698#[ts(export)]
1699pub struct UndoParams {
1700    /// Opaque service-issued undo token.
1701    pub token: String,
1702}
1703/// An undo token and the project state it restores.
1704#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1705#[ts(export)]
1706pub struct UndoResult {
1707    /// Opaque token for the inverse action.
1708    pub token: String,
1709    /// Project state after the action.
1710    pub project: ProjectInfo,
1711}
1712/// Whether REW is running, reachable, and licensed as Pro.
1713#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
1714#[ts(export)]
1715pub struct RewStatusResult {
1716    /// Whether an REW process is running.
1717    pub running: bool,
1718    /// Whether the REW API responds.
1719    pub reachable: bool,
1720    /// Observed automatic-sweep license state: unknown until success or rejection.
1721    pub pro: Option<bool>,
1722}
1723/// A requested REW audio driver.
1724#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1725#[ts(export)]
1726pub struct RewDriverSetParams {
1727    /// Driver name as REW reports it (`ASIO` or `Java`).
1728    pub driver: String,
1729}
1730/// Facts from REW, distinct from project-stored measurement preferences.
1731#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS, Default)]
1732#[ts(export)]
1733pub struct RewInfoResult {
1734    pub executable_path: Option<String>,
1735    pub running: bool,
1736    pub reachable: bool,
1737    pub version: Option<String>,
1738    /// REW's current audio driver, or null when it could not be read.
1739    pub driver: Option<String>,
1740    /// Observed automatic-sweep license state: unknown until success or rejection.
1741    pub pro: Option<bool>,
1742    /// REW's current input device, read from its own audio preferences.
1743    pub input_device: Option<String>,
1744    /// REW's current output device, read from its own audio preferences.
1745    pub output_device: Option<String>,
1746    /// REW's current input channel name.
1747    pub input: Option<String>,
1748    pub blocking: Option<bool>,
1749    /// Empty means no file loaded; null means unknown or per-input calibration.
1750    pub input_calibration_file: Option<String>,
1751    pub input_devices: Option<Vec<RewDeviceInfo>>,
1752    pub output_devices: Option<Vec<RewDeviceInfo>>,
1753    pub input_channel_names: Option<Vec<String>>,
1754    pub sample_rate: Option<f64>,
1755    pub sweep: Option<RewSweepInfo>,
1756    pub level_dbfs: Option<f64>,
1757}
1758
1759#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1760#[ts(export)]
1761pub struct RewDeviceInfo {
1762    pub id: String,
1763    pub name: String,
1764    pub channels: usize,
1765}
1766
1767#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1768#[ts(export)]
1769pub struct RewSweepInfo {
1770    pub start_hz: Option<f64>,
1771    pub end_hz: Option<f64>,
1772    pub length: Option<String>,
1773}
1774
1775/// The REW mdat file to scan.
1776#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1777#[ts(export)]
1778pub struct MdatScanParams {
1779    /// Source `.mdat` file path.
1780    pub path: String,
1781}
1782/// Measurements, suggested assignments, and problem codes found in an mdat file.
1783#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
1784#[ts(export)]
1785pub struct MdatScanResult {
1786    /// Measurements in file order, including assignment guesses.
1787    pub measurements: Vec<MdatScanMeasurement>,
1788    /// Suggested measurement-to-role assignments.
1789    pub suggestions: Vec<MdatAssignment>,
1790    /// Stable problem codes and template parameters.
1791    pub problems: Vec<Notice>,
1792}
1793/// One scanned measurement with an unambiguous row identifier.
1794#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1795#[ts(export)]
1796pub struct MdatScanMeasurement {
1797    /// Zero-based row index, also usable when titles repeat.
1798    pub index: usize,
1799    /// Original measurement title.
1800    pub title: String,
1801    /// Suggested internal role, or no assignment.
1802    pub role: Option<String>,
1803    /// Guessed one-based measurement position.
1804    pub position: i64,
1805    /// Whether the title explicitly names a position, including position 1.
1806    pub position_tagged: bool,
1807    /// Low-band classifier result, absent when no IR is available.
1808    pub looks_like_sub: Option<bool>,
1809}
1810/// An mdat measurement assigned to a speaker role.
1811#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1812#[ts(export)]
1813pub struct MdatAssignment {
1814    /// Measurement title from the mdat file.
1815    pub measurement: String,
1816    /// Internal speaker role.
1817    pub role: String,
1818    /// Optional zero-based row index to disambiguate repeated titles.
1819    pub index: Option<usize>,
1820    /// Optional per-row position overriding the import destination.
1821    pub position: Option<i64>,
1822}
1823/// An mdat file, its assignments, and destination position.
1824#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1825#[ts(export)]
1826pub struct MdatImportParams {
1827    /// Source `.mdat` file path.
1828    pub path: String,
1829    /// Accepted measurement-to-role assignments.
1830    pub assignments: Vec<MdatAssignment>,
1831    /// Destination measurement position number.
1832    pub position: i64,
1833}
1834
1835// Project editing request and response types.
1836/// A filesystem path used by a project editor.
1837#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1838#[ts(export)]
1839pub struct PathParams {
1840    /// File system path chosen by the user.
1841    pub path: String,
1842}
1843/// A path written by an editor.
1844#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1845#[ts(export)]
1846pub struct PathResult {
1847    /// File system path that was written.
1848    pub path: String,
1849}
1850/// A preset change which may need measurement-clearing confirmation.
1851#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1852#[ts(export)]
1853pub struct ConfigPresetParams {
1854    /// Preset name (`2.0`, `2.1`, `2.2`, `4.0`, `4.1`, `4.2`, `5.1`, `7.1`, `7.1.4`, `9.1.6`, or `custom` for an empty layout).
1855    pub preset: String,
1856    /// Clears existing measurements; without it a project with measurements answers `confirm_required` (U5).
1857    #[serde(default)]
1858    pub confirm: bool,
1859}
1860/// A sub-count change which may need measurement-clearing confirmation.
1861#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1862#[ts(export)]
1863pub struct ConfigSubCountParams {
1864    /// Number of subwoofers, 0 to 8.
1865    pub count: i64,
1866    /// Clears existing measurements; without it a project with measurements answers `confirm_required` (U5).
1867    #[serde(default)]
1868    pub confirm: bool,
1869}
1870/// One speaker index.
1871#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1872#[ts(export)]
1873pub struct ConfigIndexParams {
1874    /// Speaker index in `ConfigResult.speakers`.
1875    pub index: usize,
1876}
1877/// Optional edits to one speaker or one of its bands.
1878#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1879#[ts(export)]
1880pub struct ConfigSetSpeakerParams {
1881    /// Speaker index in `ConfigResult.speakers`.
1882    pub index: usize,
1883    /// New role code; a sub role forces a single SUB band (`step2_config.py:988-1002`).
1884    pub role: Option<String>,
1885    /// New size: `small` (crossover applied) or `large` (full range).
1886    pub size: Option<String>,
1887    /// Band the output and polarity edits apply to; absent means the first band.
1888    pub band_index: Option<usize>,
1889    /// New measurement output channel (1-based).
1890    pub out: Option<i64>,
1891    /// New polarity: `+` normal or `-` inverted.
1892    pub pol: Option<String>,
1893}
1894/// One configured output band.
1895#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1896#[ts(export)]
1897pub struct ConfigBand {
1898    /// Band name: `FULL`, `LF`, `MF`, `HF` or `SUB`.
1899    pub band: String,
1900    /// Measurement output channel (1-based).
1901    pub out: i64,
1902    /// Polarity: `+` normal or `-` inverted.
1903    pub pol: String,
1904}
1905/// One configured speaker.
1906#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1907#[ts(export)]
1908pub struct ConfigSpeaker {
1909    /// Role code (`L`, `C`, `SW1`, `Tfl`, ...).
1910    pub role: String,
1911    /// `small` (crossover applied) or `large` (full range); subs are `sub`.
1912    pub size: String,
1913    /// Bands in order; one band unless bi-amped.
1914    pub bands: Vec<ConfigBand>,
1915}
1916/// One upstream speaker group in upstream order.
1917#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1918#[ts(export)]
1919pub struct ConfigGroup {
1920    /// Stable presentation group id: front, center, sub, surround, back, wide, top, height or other.
1921    pub id: String,
1922    /// Upstream group name used as the project and crossover-map key.
1923    pub key: String,
1924    /// Indices into `ConfigResult.speakers` that belong to this group.
1925    pub speaker_indices: Vec<usize>,
1926}
1927/// Values derived from the complete speaker configuration.
1928#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1929#[ts(export)]
1930pub struct ConfigSummary {
1931    /// Number of speakers.
1932    pub speaker_count: usize,
1933    /// Number of measurement outputs in use.
1934    pub outputs: Vec<i64>,
1935    /// Highest output channel in use.
1936    pub max_channel: i64,
1937    /// Number of output channels used twice or more.
1938    pub duplicate_outputs: Vec<i64>,
1939    /// Number of subwoofers.
1940    pub sub_count: usize,
1941}
1942/// Screen-facing editable speaker configuration.
1943#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1944#[ts(export)]
1945pub struct ConfigResult {
1946    /// Speakers in table order.
1947    pub speakers: Vec<ConfigSpeaker>,
1948    /// Preset name, or `custom` after any manual edit.
1949    pub preset: String,
1950    /// Measurement output device name (`config.device`).
1951    pub device: String,
1952    /// Table group header rows in upstream order.
1953    pub groups: Vec<ConfigGroup>,
1954    /// Counts the summary line shows.
1955    pub summary: ConfigSummary,
1956}
1957/// Configuration after a destructive edit and its optional inverse token.
1958#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1959#[ts(export)]
1960pub struct ConfigMutationResult {
1961    /// The configuration after the edit.
1962    pub config: ConfigResult,
1963    /// Token for `undo` when the edit cleared measurements (U5).
1964    pub undo_token: Option<String>,
1965}
1966/// Output sample-rate selection stored with a DSP profile.
1967#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
1968#[serde(rename_all = "snake_case")]
1969#[ts(export)]
1970pub enum OutputRateChoice {
1971    /// Resolve the rate from the default output endpoint when exporting.
1972    Auto,
1973    /// Always export at this rate in Hz.
1974    Fixed(u32),
1975}
1976
1977/// The manually editable DSP profile fields.
1978#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1979#[ts(export)]
1980pub struct DspFields {
1981    /// Output sample rate.
1982    pub output_rate: OutputRateChoice,
1983    /// Whether this profile permits changing the output rate.
1984    pub output_rate_editable: bool,
1985    /// FIR taps per channel.
1986    pub fir_taps: i64,
1987    /// Total FIR taps the DSP can run across channels.
1988    pub fir_budget: i64,
1989    /// IIR (PEQ) bands per channel.
1990    pub iir_bands: i64,
1991    /// All-pass bands per channel.
1992    pub allpass_bands: i64,
1993    /// Bass-management path delays the DSP offers.
1994    pub bm_path_delays: i64,
1995}
1996/// One DSP preset in the built-in table.
1997#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
1998#[ts(export)]
1999pub struct DspPreset {
2000    /// Preset key (`camilla`, `tesira`, `qsys`, `minidsp`, `custom`).
2001    pub key: String,
2002    /// Device name shown in the device list.
2003    pub name: String,
2004    /// The preset's filter resources.
2005    pub fields: DspFields,
2006}
2007/// A preset selection or a complete manual edit.
2008#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
2009#[ts(export)]
2010pub struct DspSetParams {
2011    /// Selects a preset; absent with `fields` set means a manual edit.
2012    pub preset: Option<String>,
2013    /// Manual values; any manual edit makes the profile `custom`.
2014    pub fields: Option<DspFields>,
2015}
2016/// Editable DSP state and derived resolution values.
2017#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
2018#[ts(export)]
2019pub struct DspResult {
2020    /// Current profile key.
2021    pub profile: String,
2022    /// Current device name.
2023    pub name: String,
2024    /// Current filter resources.
2025    pub fields: DspFields,
2026    /// Every preset, for the device list.
2027    pub presets: Vec<DspPreset>,
2028    /// FIR window length the taps give, in ms (note line).
2029    pub window_ms: f64,
2030    /// Frequency resolution the taps give, in Hz (note line).
2031    pub resolution_hz: f64,
2032}
2033/// Measurement timing-reference settings stored under `extra.devices`.
2034#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2035#[ts(export)]
2036pub struct LayoutTiming {
2037    /// Timing reference: `acoustic`, `loopback` or `none`.
2038    pub reference: String,
2039    /// Reference speaker role for the acoustic reference (default `R`).
2040    pub role: String,
2041    /// Reference output channel for the loopback reference (1-based).
2042    pub output: i64,
2043}
2044/// One measurement microphone point in centimetres.
2045#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
2046#[ts(export)]
2047pub struct LayoutPoint {
2048    /// Position number; 1 is the listening position.
2049    pub position: usize,
2050    /// Left-right offset from the centre seat, in cm (`layout_map.py`).
2051    pub x_cm: f64,
2052    /// Front-back offset from the centre seat, in cm (`layout_map.py`).
2053    pub y_cm: f64,
2054    /// Height step: 0 at ear height, +1 or -1 for ±20 cm.
2055    pub height: i8,
2056}
2057/// One supported measurement arrangement.
2058#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
2059#[ts(export)]
2060pub struct LayoutArrangement {
2061    /// Number of positions: 1, 9, 13 or 17.
2062    pub count: usize,
2063    /// Microphone points of this arrangement.
2064    pub points: Vec<LayoutPoint>,
2065}
2066/// Editable arrangement and measurement options.
2067#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
2068#[ts(export)]
2069pub struct LayoutResult {
2070    /// Chosen arrangement (`extra['array']`).
2071    pub count: usize,
2072    /// Start delay before each position as stored (`extra['start_delay']`):
2073    /// one of [`crate::rules::START_DELAYS`], `끄기` or `5 초` to `15 초`.
2074    pub start_delay: String,
2075    /// Timing reference and its reference speaker or output.
2076    pub timing: LayoutTiming,
2077    /// Every arrangement with its geometry, for the cards and maps.
2078    pub arrangements: Vec<LayoutArrangement>,
2079}
2080/// Optional arrangement and timing edits.
2081#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
2082#[ts(export)]
2083pub struct LayoutSetParams {
2084    /// New arrangement; changing it clears measurements (U5).
2085    pub count: Option<usize>,
2086    /// New start delay, one of [`crate::rules::START_DELAYS`]; any other
2087    /// value answers `invalid_request` (field `start_delay`).
2088    pub start_delay: Option<String>,
2089    /// New timing reference.
2090    pub timing: Option<LayoutTiming>,
2091    /// Clears existing measurements; without it a project with measurements answers `confirm_required` (U5).
2092    #[serde(default)]
2093    pub confirm: bool,
2094}
2095/// Arrangement after editing and its optional inverse token.
2096#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
2097#[ts(export)]
2098pub struct LayoutMutationResult {
2099    /// The arrangement and options after the edit.
2100    pub layout: LayoutResult,
2101    /// Token for `undo` when the edit cleared measurements (U5).
2102    pub undo_token: Option<String>,
2103}
2104/// Position-duration estimate from upstream step 4 semantics.
2105#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
2106#[ts(export)]
2107pub struct PositionEstimateResult {
2108    /// Number of positions in the arrangement.
2109    pub positions: usize,
2110    /// Estimated time per position, in seconds (`step4_layout.position_seconds`).
2111    pub per_position_seconds: f64,
2112    /// Estimated time for every position, without moving the microphone, in seconds.
2113    pub total_seconds: f64,
2114}
2115/// Target control points.
2116#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
2117#[ts(export)]
2118pub struct TargetPointsParams {
2119    /// Target points as (Hz, dB); clamped to 10–24000 Hz and ±30 dB, at least two.
2120    pub points: Vec<(f64, f64)>,
2121}
2122/// One built-in target preset selection.
2123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2124#[ts(export)]
2125pub struct TargetPresetParams {
2126    /// Target preset: `flat`, `harman` or `cinema`.
2127    pub preset: String,
2128}
2129/// Target curve, selected preset, and built-in preset table.
2130#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
2131#[ts(export)]
2132pub struct TargetResult {
2133    /// Target points as (Hz, dB).
2134    pub points: Vec<(f64, f64)>,
2135    /// Preset name, or absent when the points were edited (custom).
2136    pub preset: Option<String>,
2137    /// Preset names the target menu offers.
2138    pub presets: Vec<(String, Vec<(f64, f64)>)>,
2139}
2140/// Complete calibration settings replacement.
2141#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
2142#[ts(export)]
2143pub struct CalibrationSettingsParams {
2144    /// The complete calibration settings to store.
2145    pub settings: CalibrationSettings,
2146}
2147/// Mode whose numeric specification should be returned.
2148#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
2149#[ts(export)]
2150pub struct ModeSpecParams {
2151    /// Correction mode: `zero`, `mixed`, `linear` or `ultra`.
2152    pub mode: Mode,
2153    /// Latency limit for the ultra mode, in ms.
2154    pub ultra_ms: Option<f64>,
2155    /// Whether MIMO support paths use FIR instead of IIR.
2156    #[serde(default, skip_serializing_if = "Option::is_none")]
2157    #[ts(optional)]
2158    pub mimo_fir: Option<bool>,
2159}
2160/// Presentation-neutral values underlying the upstream mode specification line.
2161#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
2162#[ts(export)]
2163pub struct ModeSpecResult {
2164    /// Room-correction filters: `iir` (IIR only) or `iir_fir` (IIR plus FIR).
2165    pub rc_filter: String,
2166    /// FIR taps the mode uses (0 without FIR).
2167    pub fir_taps: usize,
2168    /// PEQ bands per channel, when the mode uses IIR.
2169    pub peq_bands: Option<usize>,
2170    /// MIMO support filter kind (`fir` when requested, otherwise `iir`).
2171    pub mimo_filter: String,
2172    /// Delay the FIR may add ahead of the main arrival (`lift_ms`), in ms.
2173    pub lift_ms: f64,
2174    /// Octave fraction of the excess-phase smoothing (6, or 24 in linear and ultra modes).
2175    pub phase_frac: f64,
2176    /// Upper latency bound of the mode, in ms.
2177    pub max_latency_ms: f64,
2178}
2179/// Application theme preference.
2180#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
2181#[serde(rename_all = "snake_case")]
2182#[ts(export)]
2183pub enum AppTheme {
2184    /// Follow the operating-system preference.
2185    #[default]
2186    System,
2187    /// Always use the light theme.
2188    Light,
2189    /// Always use the dark theme.
2190    Dark,
2191}
2192/// The skin set the screen draws with (docs/skins, ADR 0010).
2193#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
2194#[serde(rename_all = "snake_case")]
2195#[ts(export)]
2196pub enum AppSkin {
2197    /// The instrument skin: the default tokens.
2198    #[default]
2199    Default,
2200    /// Hardware Rack: a 19-inch rack faceplate.
2201    Rack,
2202    /// Signal Matrix: a routing board of cells on a grid.
2203    Matrix,
2204}
2205/// The logo on the rail and the window (docs/DEVIATIONS.md U12). The
2206/// installed program's own icon is always `Expert`.
2207#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS, Default)]
2208#[serde(rename_all = "snake_case")]
2209#[ts(export)]
2210pub enum AppIcon {
2211    /// "전문가 모드": upstream's confirmed logo (`logo_b8.svg`).
2212    #[default]
2213    Expert,
2214    /// "킹반인 모드": SECS 2R's own logo (`apps/secs2r/icons/logo/civilian.svg`),
2215    /// a measured response and its correction on the target.
2216    Civilian,
2217}
2218/// Opt-in loopback API settings.
2219#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
2220#[ts(export)]
2221pub struct LocalApiSettings {
2222    /// Whether the app starts the local API server.
2223    pub enabled: bool,
2224    /// Loopback TCP port; zero asks the operating system for a free port.
2225    pub port: u16,
2226}
2227impl Default for LocalApiSettings {
2228    fn default() -> Self {
2229        Self {
2230            enabled: false,
2231            port: crate::local_api::DEFAULT_PORT,
2232        }
2233    }
2234}
2235
2236/// How far SECS 2R controls one application target.
2237#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
2238#[serde(rename_all = "snake_case")]
2239#[ts(export)]
2240pub enum ApplyLevel {
2241    /// Only write ordinary export files.
2242    #[default]
2243    Files,
2244    /// Apply on explicit request while the app is running.
2245    Apply,
2246    /// Keep the applied design active through the managed service.
2247    Manage,
2248}
2249
2250/// CamillaDSP application settings.
2251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2252#[ts(export)]
2253pub struct CamillaApply {
2254    /// Selected control level.
2255    pub level: ApplyLevel,
2256    /// Plain websocket address in `host:port` form.
2257    pub address: String,
2258    /// CamillaDSP executable selected by the user for managed operation.
2259    #[serde(default, skip_serializing_if = "Option::is_none")]
2260    #[ts(optional)]
2261    pub exe_path: Option<String>,
2262}
2263impl Default for CamillaApply {
2264    fn default() -> Self {
2265        Self {
2266            level: ApplyLevel::Files,
2267            address: "127.0.0.1:1234".into(),
2268            exe_path: None,
2269        }
2270    }
2271}
2272
2273/// Equalizer APO application settings.
2274#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
2275#[ts(export)]
2276pub struct EqApoApply {
2277    /// Selected control level.
2278    pub level: ApplyLevel,
2279}
2280
2281/// Settings for applying designs to external processors.
2282#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
2283#[ts(export)]
2284pub struct ApplySettings {
2285    /// CamillaDSP settings.
2286    pub camilla: CamillaApply,
2287    /// Equalizer APO settings.
2288    pub eqapo: EqApoApply,
2289}
2290
2291/// Optional device choices for a CamillaDSP application export.
2292#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
2293#[ts(export)]
2294pub struct ApplyCamillaParams {
2295    /// Capture and playback choices; absent keeps file placeholders.
2296    #[serde(default, skip_serializing_if = "Option::is_none")]
2297    #[ts(optional)]
2298    pub camilla_devices: Option<CamillaDevices>,
2299}
2300
2301/// Equalizer APO edition detected from its installed path.
2302#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
2303#[serde(rename_all = "snake_case")]
2304#[ts(export)]
2305pub enum EqApoVariant {
2306    /// The SourceForge Equalizer APO release.
2307    Official,
2308    /// EqualizerAPO-XT.
2309    Xt,
2310    /// An installation whose path does not identify either edition.
2311    Unknown,
2312}
2313
2314/// Equalizer APO installation values supplied by the operating-system adapter.
2315#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2316pub struct EqApoInstall {
2317    /// Equalizer APO configuration directory.
2318    pub config_path: String,
2319    /// Equalizer APO installation directory.
2320    pub install_path: String,
2321    /// Detected edition.
2322    pub variant: EqApoVariant,
2323}
2324
2325/// State of SECS 2R's managed lines in Equalizer APO's `config.txt`.
2326#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
2327#[serde(rename_all = "snake_case")]
2328#[ts(export)]
2329pub enum IncludeState {
2330    /// No managed marker exists.
2331    Absent,
2332    /// The marker and include line form one intact pair.
2333    Present,
2334    /// A marker exists without exactly one intact include pair.
2335    Damaged,
2336}
2337
2338/// One CamillaDSP process kept running by the host service.
2339#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2340#[ts(export)]
2341pub struct ManagedCamilla {
2342    pub exe_path: String,
2343    pub config_path: String,
2344    pub port: u16,
2345    pub design_id: String,
2346}
2347
2348/// One Equalizer APO include kept intact by the host service.
2349#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2350#[ts(export)]
2351pub struct ManagedEqApo {
2352    pub config_path: String,
2353    pub design_id: String,
2354}
2355
2356/// Targets persisted by the host service.
2357#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
2358#[ts(export)]
2359pub struct HostManaged {
2360    #[serde(default, skip_serializing_if = "Option::is_none")]
2361    #[ts(optional)]
2362    pub camilla: Option<ManagedCamilla>,
2363    #[serde(default, skip_serializing_if = "Option::is_none")]
2364    #[ts(optional)]
2365    pub eqapo: Option<ManagedEqApo>,
2366    /// Interactive session that selected these targets.
2367    #[serde(default, skip_serializing_if = "Option::is_none")]
2368    #[ts(skip)]
2369    pub owner_session_id: Option<u32>,
2370    /// Stable SID bytes of that session's user token.
2371    #[serde(default, skip_serializing_if = "Option::is_none")]
2372    #[ts(skip)]
2373    pub owner_user_sid: Option<String>,
2374}
2375
2376/// A request sent over the local managed-host pipe.
2377#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2378#[serde(tag = "method", content = "params", rename_all = "snake_case")]
2379#[ts(export)]
2380pub enum HostRequest {
2381    Status,
2382    SetManaged(HostManaged),
2383    StopManaging,
2384    Log {
2385        lines: u32,
2386    },
2387    /// Replaces the service with the signed `secs2r-host.exe` next to
2388    /// `secs2r-host.json` in `from_dir` (ADR 0020).
2389    UpdateSelf {
2390        from_dir: String,
2391    },
2392}
2393
2394/// One JSON Lines entry from the managed host log.
2395#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2396#[ts(export)]
2397pub struct HostLogLine {
2398    /// UTC timestamp in RFC 3339 form.
2399    pub timestamp: String,
2400    /// Stable machine-readable event name.
2401    pub event: String,
2402    /// Event-specific string values such as a PID, path, or reason.
2403    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
2404    pub fields: BTreeMap<String, String>,
2405}
2406
2407/// Why a managed CamillaDSP process is not simply running.
2408#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2409#[serde(tag = "kind", rename_all = "snake_case")]
2410#[ts(export)]
2411pub enum ManagedProcessState {
2412    Running,
2413    BackingOff { until: String },
2414    GivenUp { reason: String },
2415    Stopped,
2416    NoSession,
2417    OtherUser,
2418}
2419
2420/// Current CamillaDSP supervision state.
2421#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2422#[ts(export)]
2423pub struct ManagedProcess {
2424    pub state: ManagedProcessState,
2425    #[ts(type = "number | null")]
2426    pub pid: Option<u32>,
2427    pub restarts_in_window: u32,
2428}
2429
2430impl Default for ManagedProcess {
2431    fn default() -> Self {
2432        Self {
2433            state: ManagedProcessState::Stopped,
2434            pid: None,
2435            restarts_in_window: 0,
2436        }
2437    }
2438}
2439
2440/// Status returned by the managed host service.
2441#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2442#[ts(export)]
2443pub struct HostStatus {
2444    pub version: String,
2445    pub managed: HostManaged,
2446    pub camilla: ManagedProcess,
2447    pub eqapo_include: IncludeState,
2448    pub last_repair: Option<String>,
2449}
2450
2451/// One response returned over the local managed-host pipe.
2452#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2453#[serde(tag = "kind", content = "data", rename_all = "snake_case")]
2454#[ts(export)]
2455pub enum HostResponse {
2456    Status(Box<HostStatus>),
2457    Ok,
2458    Log(Vec<HostLogLine>),
2459    Error(IpcError),
2460}
2461
2462/// Windows service state visible to the app before a pipe call.
2463#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
2464#[serde(rename_all = "snake_case")]
2465#[ts(export)]
2466pub enum ServiceState {
2467    #[default]
2468    NotInstalled,
2469    Stopped,
2470    Running,
2471    Unreachable,
2472}
2473
2474/// How the running CamillaDSP configuration compares with the current result.
2475#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2476#[serde(tag = "kind", rename_all = "snake_case")]
2477#[ts(export)]
2478pub enum DesignMatch {
2479    /// The title and embedded identity match the current design.
2480    This { design_id: String },
2481    /// CamillaDSP runs another titled configuration.
2482    Other { title: String },
2483    /// There is not enough information to identify the running design.
2484    Unknown,
2485}
2486
2487/// CamillaDSP status mirrored from the platform-neutral application client.
2488#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
2489#[ts(export)]
2490pub struct CamillaStatus {
2491    pub version: String,
2492    pub state: String,
2493    pub stop_reason: Option<String>,
2494    pub title: Option<String>,
2495    pub description: Option<String>,
2496    pub config_path: Option<String>,
2497    pub capture_rate: Option<u32>,
2498    #[ts(type = "number | null")]
2499    pub clipped_samples: Option<u64>,
2500    pub processing_load_pct: Option<f64>,
2501    pub playback_peak_db: Option<Vec<f64>>,
2502}
2503
2504/// Reachability and running design reported by CamillaDSP.
2505#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
2506#[ts(export)]
2507pub struct CamillaApplyStatus {
2508    pub reachable: bool,
2509    pub error: Option<IpcError>,
2510    pub status: Option<CamillaStatus>,
2511    pub design: DesignMatch,
2512}
2513
2514/// Installed and applied state of Equalizer APO.
2515#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2516#[ts(export)]
2517pub struct EqApoApplyStatus {
2518    pub installed: bool,
2519    pub variant: EqApoVariant,
2520    pub config_path: Option<String>,
2521    pub include: IncludeState,
2522    pub applied_design_id: Option<String>,
2523}
2524
2525/// Current state of both application targets.
2526#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
2527#[ts(export)]
2528pub struct ApplyStatus {
2529    pub camilla: CamillaApplyStatus,
2530    pub eqapo: EqApoApplyStatus,
2531    /// Windows managed-host service state.
2532    #[serde(default)]
2533    pub host: ServiceState,
2534    /// Detailed host state when the service is running and reachable.
2535    #[serde(default, skip_serializing_if = "Option::is_none")]
2536    #[ts(optional)]
2537    pub host_status: Option<Box<HostStatus>>,
2538    /// Design ID of the current result, absent without one; the screen
2539    /// compares Equalizer APO's `applied_design_id` with it.
2540    #[serde(default)]
2541    pub current_design_id: Option<String>,
2542}
2543
2544/// Result of applying an exported design.
2545#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2546#[serde(tag = "kind", rename_all = "snake_case")]
2547#[ts(export)]
2548pub enum ApplyOutcome {
2549    /// The target accepted the design.
2550    Applied,
2551    /// No CamillaDSP was running; the managed host will start it with the design.
2552    HandedOver,
2553    /// CamillaDSP rejected reload and the previous path was restored.
2554    RolledBack { error: IpcError },
2555}
2556
2557/// Completed application job result.
2558#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2559#[ts(export)]
2560pub struct ApplyResult {
2561    pub outcome: ApplyOutcome,
2562    pub design_id: String,
2563    pub folder: String,
2564    pub stale: bool,
2565}
2566
2567/// Application-local settings, independent of an open project.
2568#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2569#[ts(export)]
2570pub struct AppSettings {
2571    /// Application-local compute preference; never stored in an upstream project.
2572    #[serde(default)]
2573    pub compute: ComputePreference,
2574    /// Screen language: `ko` or `en`.
2575    pub language: String,
2576    /// Application theme preference.
2577    #[serde(default)]
2578    pub theme: AppTheme,
2579    /// Logo on the rail and the window.
2580    #[serde(default)]
2581    pub icon: AppIcon,
2582    /// Skin set the screen draws with.
2583    #[serde(default)]
2584    pub skin: AppSkin,
2585    /// Opt-in loopback API server.
2586    #[serde(default)]
2587    pub local_api: LocalApiSettings,
2588    /// Applying designs to CamillaDSP and Equalizer APO.
2589    #[serde(default)]
2590    pub apply: ApplySettings,
2591    /// When to look for and install new releases.
2592    #[serde(default)]
2593    pub updates: UpdatePolicy,
2594    /// Recently opened project paths, newest first (at most 10).
2595    pub recent_projects: Vec<String>,
2596    /// Last window size in logical pixels.
2597    pub last_window_size: Option<(u32, u32)>,
2598}
2599impl Default for AppSettings {
2600    fn default() -> Self {
2601        Self {
2602            language: "ko".into(),
2603            compute: ComputePreference::Auto,
2604            theme: AppTheme::System,
2605            icon: AppIcon::Expert,
2606            skin: AppSkin::Default,
2607            local_api: LocalApiSettings::default(),
2608            apply: ApplySettings::default(),
2609            updates: UpdatePolicy::Auto,
2610            recent_projects: Vec::new(),
2611            last_window_size: None,
2612        }
2613    }
2614}
2615/// Complete application-local settings replacement.
2616#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2617#[ts(export)]
2618pub struct AppSettingsParams {
2619    /// The complete app settings to store.
2620    pub settings: AppSettings,
2621}
2622
2623/// When SECS 2R looks for and installs new releases (ADR 0020).
2624#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
2625#[serde(rename_all = "snake_case")]
2626#[ts(export)]
2627pub enum UpdatePolicy {
2628    /// Check, download in the background and install when the app closes.
2629    #[default]
2630    Auto,
2631    /// Check and report a newer release; download only on request.
2632    Notify,
2633    /// Never contact the release server.
2634    Off,
2635}
2636
2637/// When a downloaded release is installed.
2638#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
2639#[serde(rename_all = "snake_case")]
2640#[ts(export)]
2641pub enum UpdateInstall {
2642    /// When the app closes, or at once with a restart.
2643    OnExit,
2644    /// Only when the user asks (a package that needs an administrator password).
2645    OnRequest,
2646}
2647
2648/// Where the application update stands.
2649#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2650#[serde(tag = "kind", rename_all = "snake_case")]
2651#[ts(export)]
2652pub enum UpdatePhase {
2653    /// The policy is `off`, or this build has no updater.
2654    Off,
2655    /// No check has finished since the app started.
2656    Idle,
2657    /// A check is running.
2658    Checking,
2659    /// The last check found no newer release.
2660    UpToDate,
2661    /// A newer release waits for a download request (policy `notify`).
2662    Available { version: String },
2663    /// A newer release is being downloaded.
2664    Downloading {
2665        version: String,
2666        #[ts(type = "number")]
2667        received: u64,
2668        #[ts(type = "number | null")]
2669        total: Option<u64>,
2670    },
2671    /// A newer release is downloaded and verified.
2672    Ready {
2673        version: String,
2674        install: UpdateInstall,
2675    },
2676    /// The installer is starting; the app closes next.
2677    Installing { version: String },
2678    /// A newer release exists, but this portable copy is replaced by hand.
2679    Portable { version: String, url: String },
2680    /// The last check, download or install failed.
2681    Failed { reason: String },
2682}
2683
2684/// Shown once when the app starts a newer version than it ran last time.
2685#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2686#[ts(export)]
2687pub struct UpdateNotice {
2688    /// Version that downloaded the update.
2689    pub from: String,
2690    /// Version now running.
2691    pub to: String,
2692    /// The release's CHANGELOG section (Markdown).
2693    pub notes: String,
2694    /// Releases between `from` and `to` were skipped; their notes are only on
2695    /// the release page.
2696    pub skipped: bool,
2697    /// The release page of `to`.
2698    pub release_url: String,
2699}
2700
2701/// The release whose page [`Request::UpdateOpenRelease`] opens.
2702#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2703#[ts(export)]
2704pub struct UpdateOpenReleaseParams {
2705    /// A release version such as `0.36.0`, without the `v` of its tag.
2706    pub version: String,
2707}
2708
2709/// Application update state for the screen.
2710#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2711#[ts(export)]
2712pub struct UpdateStatus {
2713    /// Version now running.
2714    pub current: String,
2715    /// Stored policy.
2716    pub policy: UpdatePolicy,
2717    pub phase: UpdatePhase,
2718    /// When the last check finished, RFC 3339 UTC.
2719    pub checked_at: Option<String>,
2720    /// The one-time "updated" notice, until dismissed.
2721    pub notice: Option<UpdateNotice>,
2722}
2723
2724/// A downloaded release waiting for its first start: `update.json` in the
2725/// app's config folder, written by the app or the host shell and read by the
2726/// app on its next start. Not sent over IPC.
2727#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2728pub struct PendingUpdate {
2729    /// Version that downloaded the update.
2730    pub from: String,
2731    /// Version that will run after the install.
2732    pub to: String,
2733    /// The release's CHANGELOG section (Markdown).
2734    pub notes: String,
2735    /// The release before `to`, from the update manifest's `previous` field.
2736    #[serde(default, skip_serializing_if = "Option::is_none")]
2737    pub previous: Option<String>,
2738}
2739
2740/// App-facing state of the opt-in local API server.
2741#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2742#[ts(export)]
2743pub struct LocalApiStatus {
2744    /// Configured enabled state.
2745    pub enabled: bool,
2746    /// Configured port, or the selected free port while running with port zero.
2747    pub port: u16,
2748    /// Whether a listener is currently running.
2749    pub running: bool,
2750    /// Most recent startup failure, when present.
2751    pub error: Option<String>,
2752    /// Loopback base URL.
2753    pub url: String,
2754    /// Current bearer token.
2755    pub token: String,
2756    /// Full token-file path.
2757    pub token_path: String,
2758}
2759
2760/// Why a call failed. The screen maps each code to a written message with the
2761/// cause and the next action.
2762#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
2763#[serde(rename_all = "snake_case")]
2764#[ts(export)]
2765pub enum ErrorCode {
2766    /// The request did not parse or named an unknown method.
2767    InvalidRequest,
2768    /// A bug: the service failed in a way it does not expect.
2769    Internal,
2770    /// The method is registered but not connected yet.
2771    NotImplemented,
2772    /// The method opens a window or program and is unavailable to local API clients.
2773    UiOnly,
2774    /// The selected operation or control level is unavailable on this host.
2775    Unsupported,
2776    /// A target file could not be changed because access was denied.
2777    PermissionDenied,
2778    /// CamillaDSP or Equalizer APO refused or failed an application request.
2779    ApplyFailed,
2780    /// The call requires an open project.
2781    ProjectNotOpen,
2782    /// Opening, reading, or writing project data failed.
2783    ProjectIo,
2784    /// Another job in the same group is running.
2785    JobBusy,
2786    /// The named job does not exist.
2787    JobNotFound,
2788    /// The caller cancelled the operation.
2789    Cancelled,
2790    /// Project measurements or settings are not valid engine inputs.
2791    EngineInput,
2792    /// Explicit GPU execution was requested but no usable GPU backend is available.
2793    ComputeUnavailable,
2794    /// The requested exporter failed.
2795    ExportFailed,
2796    /// The chosen export cannot carry the result (upstream
2797    /// `filter_export_block`); `params.reason` is an [`ExportBlock`] name.
2798    ExportScope,
2799    /// Cached results no longer match project inputs.
2800    StaleInputs,
2801    /// The audio backend or selected endpoint is unavailable.
2802    DeviceUnavailable,
2803    /// Recording completed but was discarded because it had a gap or corrupt samples.
2804    RecordingFailed,
2805    /// The backend refused a pinned audio share mode.
2806    ShareModeRefused,
2807    /// The REW API cannot be reached.
2808    RewUnreachable,
2809    /// REW requires a Pro license for this operation.
2810    RewProRequired,
2811    /// REW returned an HTTP or schema failure.
2812    RewFailed,
2813    /// A measurement setting or plan field is invalid.
2814    MeasurementInput,
2815    /// Tutorial sessions cannot save or export project data.
2816    TutorialBlocked,
2817    /// A destructive edit needs explicit confirmation while measurements exist.
2818    ConfirmRequired,
2819}
2820/// A coded IPC failure with template parameters and diagnostic detail.
2821#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
2822#[ts(export)]
2823pub struct IpcError {
2824    /// Stable error code translated by the screen.
2825    pub code: ErrorCode,
2826    /// Values for the message template of `code` (file names, counts, ...).
2827    pub params: BTreeMap<String, String>,
2828    /// Whether trying the same call again can succeed.
2829    pub retryable: bool,
2830    /// The raw error text, for the details view and the log only.
2831    pub detail: String,
2832}
2833impl IpcError {
2834    /// Creates a non-retryable error without template parameters.
2835    pub fn new(code: ErrorCode, detail: impl Into<String>) -> Self {
2836        Self {
2837            code,
2838            params: BTreeMap::new(),
2839            retryable: false,
2840            detail: detail.into(),
2841        }
2842    }
2843    /// Adds one screen message-template parameter.
2844    pub fn with_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
2845        self.params.insert(key.into(), value.into());
2846        self
2847    }
2848}
2849
2850/// The reply to every call: `{"ok": true, "data": ...}` or
2851/// `{"ok": false, "error": ...}`.
2852///
2853/// The TypeScript side declares the same union in `src/ipc/client.ts`
2854/// (`Envelope`); `envelope_wire_shape` below pins the JSON both sides rely on.
2855#[derive(Debug, Clone, PartialEq)]
2856pub enum Envelope {
2857    /// The `Ok` Envelope variant.
2858    Ok(Box<Response>),
2859    /// The `Err` Envelope variant.
2860    Err(IpcError),
2861}
2862impl Serialize for Envelope {
2863    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2864        use serde::ser::SerializeStruct;
2865        let mut state = serializer.serialize_struct("Envelope", 2)?;
2866        match self {
2867            Self::Ok(data) => {
2868                state.serialize_field("ok", &true)?;
2869                state.serialize_field("data", data)?;
2870            }
2871            Self::Err(error) => {
2872                state.serialize_field("ok", &false)?;
2873                state.serialize_field("error", error)?;
2874            }
2875        }
2876        state.end()
2877    }
2878}
2879
2880#[cfg(test)]
2881mod tests {
2882    use super::*;
2883
2884    /// One value of every variant. A new variant fails to compile here until
2885    /// it gets an example, which `method_names_match_the_wire` then checks.
2886    fn examples() -> Vec<Request> {
2887        vec![
2888            Request::Bootstrap,
2889            Request::ProjectNew(ProjectNewParams {
2890                path: "x.secs".into(),
2891                preset: "2.1".into(),
2892                sub_count: 1,
2893                sample: false,
2894                tutorial: false,
2895            }),
2896            Request::ProjectOpen(ProjectPathParams {
2897                path: "x.secs".into(),
2898            }),
2899            Request::ProjectSave(ProjectSaveParams::default()),
2900            Request::ProjectInfo,
2901            Request::SampleLoad(SampleLoadParams::default()),
2902            Request::Calibrate(CalibrateParams::default()),
2903            Request::RecommendXo,
2904            Request::Mimo(MimoParams::default()),
2905            Request::Results,
2906            Request::Export(ExportParams {
2907                kind: ExportKind::CamillaDsp,
2908                folder: ".".into(),
2909                mdat_scope: None,
2910                design_name: None,
2911                camilla_devices: None,
2912                extra_rates: Vec::new(),
2913            }),
2914            Request::JobCancel(JobCancelParams { job: JobId(1) }),
2915            Request::Jobs,
2916            Request::MeasureMethods,
2917            Request::MeasureConfigure(MeasureConfigureParams::default()),
2918            Request::MeasureConfigGet,
2919            Request::MeasurementRecords,
2920            Request::MicCalibrationGet,
2921            Request::MicCalibrationSet(MicCalibrationSetParams {
2922                path: "mic.txt".into(),
2923            }),
2924            Request::MicCalibrationClear,
2925            Request::MicCalibrationAttach,
2926            Request::LevelsGet,
2927            Request::LevelsFillDefaults,
2928            Request::LevelStop,
2929            Request::MeasurePosition(MeasurePositionParams { position: 1 }),
2930            Request::PositionClear(PositionClearParams { position: 1 }),
2931            Request::Undo(UndoParams { token: "x".into() }),
2932            Request::RewLaunch,
2933            Request::MdatScan(MdatScanParams {
2934                path: "x.mdat".into(),
2935            }),
2936            Request::MdatImport(MdatImportParams {
2937                path: "x.mdat".into(),
2938                assignments: vec![],
2939                position: 1,
2940            }),
2941            Request::ConfigGet,
2942            Request::ConfigApplyPreset(ConfigPresetParams {
2943                preset: "2.1".into(),
2944                confirm: false,
2945            }),
2946            Request::ConfigSetSubCount(ConfigSubCountParams {
2947                count: 2,
2948                confirm: false,
2949            }),
2950            Request::ConfigSetSpeaker(ConfigSetSpeakerParams {
2951                index: 0,
2952                role: Some("L".into()),
2953                size: None,
2954                band_index: Some(0),
2955                out: Some(1),
2956                pol: Some("+".into()),
2957            }),
2958            Request::ConfigAddSpeaker,
2959            Request::ConfigRemoveSpeaker(ConfigIndexParams { index: 0 }),
2960            Request::ConfigRenumber,
2961            Request::ConfigValidate,
2962            Request::PatchSheetCsv(PathParams {
2963                path: "patch.csv".into(),
2964            }),
2965            Request::DspGet,
2966            Request::DspSet(DspSetParams {
2967                preset: Some("tesira".into()),
2968                fields: None,
2969            }),
2970            Request::LayoutGet,
2971            Request::LayoutSet(LayoutSetParams {
2972                count: Some(9),
2973                ..Default::default()
2974            }),
2975            Request::PositionEstimate,
2976            Request::TargetGet,
2977            Request::TargetSetPoints(TargetPointsParams {
2978                points: vec![(20.0, 0.0), (20_000.0, 0.0)],
2979            }),
2980            Request::TargetApplyPreset(TargetPresetParams {
2981                preset: "flat".into(),
2982            }),
2983            Request::TargetSave(PathParams {
2984                path: "target.json".into(),
2985            }),
2986            Request::TargetLoad(PathParams {
2987                path: "target.json".into(),
2988            }),
2989            Request::CalibrationSettingsGet,
2990            Request::CalibrationSettingsEdit(CalibrationSettingsEditParams {
2991                edit: crate::settings_edit::SettingEdit::MimoOn { on: true },
2992            }),
2993            Request::SubWarnings,
2994            Request::RewInfo,
2995            Request::RewDriverSet(RewDriverSetParams {
2996                driver: "ASIO".into(),
2997            }),
2998            Request::CalibrationSettingsSet(CalibrationSettingsParams {
2999                settings: CalibrationSettings::default(),
3000            }),
3001            Request::ModeSpec(ModeSpecParams {
3002                mode: Mode::Mixed,
3003                ultra_ms: None,
3004                mimo_fir: None,
3005            }),
3006            Request::AppSettingsGet,
3007            Request::AppSettingsSet(AppSettingsParams {
3008                settings: AppSettings::default(),
3009            }),
3010            Request::ApplyStatus,
3011            Request::ApplyCamilla(ApplyCamillaParams::default()),
3012            Request::ApplyEqapo,
3013            Request::RevertEqapo,
3014            Request::ApplySettingsSet(ApplySettings::default()),
3015            Request::HostInstall,
3016            Request::HostUninstall,
3017            Request::HostStopManaging,
3018            Request::UpdateStatus,
3019            Request::UpdateCheck,
3020            Request::UpdateDownload,
3021            Request::UpdateRestart,
3022            Request::UpdateNoticeDismiss,
3023            Request::UpdateOpenRelease(UpdateOpenReleaseParams {
3024                version: "0.36.0".into(),
3025            }),
3026            Request::LocalApiStatus,
3027            Request::LocalApiTokenRegenerate,
3028            Request::ComputeStatus,
3029            Request::OpenGpuHelp,
3030            Request::MeasurementCurves(MeasurementCurvesParams::default()),
3031            Request::ResultCurves(ResultCurvesParams {
3032                tab: ResultTab::Spl,
3033                channel: None,
3034                group: None,
3035                xo_hz: None,
3036                corrected: None,
3037            }),
3038            Request::ResultLines,
3039            Request::SubTakeValues,
3040            Request::DialogOpenProject,
3041            Request::DialogSaveProject(DialogSuggestedParams {
3042                suggested: "x.secs".into(),
3043            }),
3044            Request::DialogPickMdat,
3045            Request::DialogPickFolder(DialogPickFolderParams { start: None }),
3046            Request::DialogSaveFile(DialogSaveFileParams {
3047                kind: SaveFileKind::PatchSheet,
3048                suggested: "x.csv".into(),
3049            }),
3050            Request::DialogOpenFile(DialogOpenFileParams {
3051                kind: OpenFileKind::Target,
3052            }),
3053            Request::OpenFolder(OpenFolderParams { path: ".".into() }),
3054            Request::LevelMonitorStart(LevelMonitorStartParams::default()),
3055            Request::LevelMonitorStop,
3056            Request::LevelPlay(LevelPlayParams {
3057                role: "L".into(),
3058                master_dbfs: -20.0,
3059            }),
3060            Request::StatusWatchStart,
3061            Request::StatusWatchStop,
3062            Request::ExportFolderGet,
3063            Request::ExportFolderSet(ExportFolderSetParams { folder: ".".into() }),
3064        ]
3065    }
3066    #[test]
3067    fn method_names_match_the_wire() {
3068        let examples = examples();
3069        assert_eq!(examples.len(), Request::METHODS.len());
3070        for request in examples {
3071            let wire = serde_json::to_value(&request).unwrap();
3072            assert_eq!(wire["method"], request.method());
3073        }
3074    }
3075    #[test]
3076    fn every_method_name_is_listed_and_parses() {
3077        for request in examples() {
3078            let wire = serde_json::to_value(&request).unwrap();
3079            let parsed: Request = serde_json::from_value(wire).unwrap();
3080            assert_eq!(parsed.method(), request.method());
3081        }
3082    }
3083    #[test]
3084    fn old_app_settings_without_local_api_use_the_disabled_default() {
3085        let settings: AppSettings = serde_json::from_str(
3086            r#"{"language":"ko","recent_projects":[],"last_window_size":null}"#,
3087        )
3088        .unwrap();
3089        assert_eq!(settings.local_api, LocalApiSettings::default());
3090        assert!(!settings.local_api.enabled);
3091        assert_eq!(settings.local_api.port, crate::local_api::DEFAULT_PORT);
3092    }
3093
3094    #[test]
3095    fn calibrate_mimo_defaults_false_on_the_wire() {
3096        let old: CalibrateParams =
3097            serde_json::from_str(r#"{"settings":null,"subs_only":false}"#).unwrap();
3098        assert!(!old.mimo);
3099        assert_eq!(CalibrateParams::default(), old);
3100        let wire = serde_json::to_value(CalibrateParams {
3101            mimo: true,
3102            ..Default::default()
3103        })
3104        .unwrap();
3105        assert_eq!(wire["mimo"], true);
3106    }
3107
3108    #[test]
3109    fn mdat_scope_is_optional_on_the_wire() {
3110        let old: ExportParams =
3111            serde_json::from_str(r#"{"kind":"before_after_mdat","folder":""}"#).unwrap();
3112        assert_eq!(old.mdat_scope, None);
3113        assert!(old.extra_rates.is_empty());
3114        assert_eq!(old.mdat_scope.unwrap_or_default(), MdatScope::All);
3115        let wire = serde_json::to_value(&old).unwrap();
3116        assert!(wire.get("mdat_scope").is_none());
3117        let scoped = ExportParams {
3118            mdat_scope: Some(MdatScope::Measurements),
3119            design_name: None,
3120            camilla_devices: None,
3121            ..old
3122        };
3123        let wire = serde_json::to_value(&scoped).unwrap();
3124        assert_eq!(wire["mdat_scope"], "measurements");
3125        assert_eq!(
3126            serde_json::from_value::<ExportParams>(wire).unwrap(),
3127            scoped
3128        );
3129    }
3130    #[test]
3131    fn envelope_wire_shape() {
3132        let ok = Envelope::Ok(Box::new(Response::Bootstrap(BootstrapInfo {
3133            app_name: "SECS 2R".into(),
3134            version: "0.0.1".into(),
3135            prerelease: true,
3136            upstream: UpstreamBase {
3137                name: "SECS".into(),
3138                repository: "https://example.invalid/secs".into(),
3139                version: "Beta".into(),
3140                commit: "0".repeat(40),
3141                date: "2026-10-12".into(),
3142            },
3143            platform: Platform {
3144                os: "linux".into(),
3145                arch: "x86_64".into(),
3146            },
3147        })));
3148        let value = serde_json::to_value(ok).unwrap();
3149        assert_eq!(value["ok"], true);
3150        assert_eq!(value["data"]["method"], "bootstrap");
3151        let value = serde_json::to_value(Envelope::Err(IpcError::new(
3152            ErrorCode::InvalidRequest,
3153            "bad",
3154        )))
3155        .unwrap();
3156        assert_eq!(value["ok"], false);
3157        assert_eq!(value["error"]["code"], "invalid_request");
3158    }
3159
3160    #[test]
3161    fn compute_completion_events_are_payload_free_markers() {
3162        for (result, kind) in [
3163            (JobResult::Calibration, "calibration"),
3164            (JobResult::Mimo, "mimo"),
3165        ] {
3166            let event = JobEvent::Finished {
3167                job: JobId(1),
3168                status: JobStatus::Succeeded,
3169                result: Some(Box::new(result)),
3170                error: None,
3171            };
3172            let wire = serde_json::to_vec(&event).unwrap();
3173            let value: serde_json::Value = serde_json::from_slice(&wire).unwrap();
3174            assert_eq!(value["result"], serde_json::json!({ "kind": kind }));
3175            assert!(wire.len() < 128, "completion must not carry a design");
3176            assert_eq!(serde_json::from_slice::<JobEvent>(&wire).unwrap(), event);
3177        }
3178    }
3179
3180    #[test]
3181    fn job_snapshot_is_metadata_without_a_result_payload() {
3182        let snapshot = JobSnapshot {
3183            job: JobId(42),
3184            kind: JobKind::Mimo,
3185            status: JobStatus::Succeeded,
3186            cancellable: false,
3187            progress: None,
3188            error: None,
3189        };
3190        let wire = serde_json::to_value(&snapshot).unwrap();
3191        assert!(wire.get("result").is_none());
3192        assert_eq!(
3193            serde_json::from_value::<JobSnapshot>(wire).unwrap(),
3194            snapshot
3195        );
3196    }
3197
3198    #[test]
3199    fn filter_export_scope_follows_upstream_but_eqapo_carries_mimo_f12() {
3200        use ExportKind::*;
3201        for kind in [
3202            CamillaDsp,
3203            StagedMdat,
3204            BeforeAfterMdat,
3205            EqualizerApo,
3206            Convolution,
3207        ] {
3208            for (subs, mimo) in [(false, false), (true, false), (false, true), (true, true)] {
3209                assert_eq!(filter_export_block(kind, subs, mimo), None, "{kind:?}");
3210            }
3211        }
3212        assert_eq!(filter_export_block(FilterText, false, false), None);
3213        assert_eq!(
3214            export_block(StagedMdat, false, false, true),
3215            Some(ExportBlock::StaleCache)
3216        );
3217        assert_eq!(
3218            export_block(BeforeAfterMdat, true, true, true),
3219            Some(ExportBlock::StaleCache)
3220        );
3221        assert_eq!(export_block(CamillaDsp, true, true, true), None);
3222        assert_eq!(
3223            filter_export_block(FilterText, true, false),
3224            Some(ExportBlock::Subs)
3225        );
3226        assert_eq!(
3227            filter_export_block(FilterText, true, true),
3228            Some(ExportBlock::Subs)
3229        );
3230        assert_eq!(
3231            filter_export_block(FilterText, false, true),
3232            Some(ExportBlock::Mimo)
3233        );
3234        assert_eq!(serde_json::to_value(FilterText).unwrap(), "filter_text");
3235        assert_eq!(serde_json::to_value(EqualizerApo).unwrap(), "equalizer_apo");
3236        assert_eq!(serde_json::to_value(Convolution).unwrap(), "convolution");
3237        assert_eq!(
3238            serde_json::to_value(ExportBlock::Subs).unwrap(),
3239            ExportBlock::Subs.name()
3240        );
3241        assert_eq!(
3242            serde_json::to_value(ExportBlock::Mimo).unwrap(),
3243            ExportBlock::Mimo.name()
3244        );
3245        assert_eq!(
3246            serde_json::to_value(ErrorCode::ExportScope).unwrap(),
3247            "export_scope"
3248        );
3249    }
3250}