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