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