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