Skip to main content

secs_types/
ipc.rs

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