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