Skip to main content

maolan_engine/
message.rs

1use crate::clap::{ClapParameterInfo, ClapPluginInfo};
2pub use crate::connectable::{ConnectableConnection, ConnectableRef};
3#[cfg(unix)]
4use crate::lv2::Lv2PluginInfo;
5use crate::midi::io::MidiEvent;
6use crate::state::TrackHandle;
7use crate::vst3::Vst3PluginInfo;
8use crate::{kind::Kind, modulator::Modulator};
9use std::net::SocketAddr;
10use std::sync::{Arc, atomic::AtomicBool};
11use tokio::sync::mpsc::Sender;
12
13#[derive(Clone, Debug, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
14pub struct TrackColor {
15    pub r: f32,
16    pub g: f32,
17    pub b: f32,
18    pub a: f32,
19}
20
21#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
22pub struct MpeExpressionPoint {
23    pub sample_offset: usize,
24    /// Pitch bend uses 0..=16383 (8192 center); pressure and timbre use 0..=127.
25    pub value: u16,
26}
27
28#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
29pub struct MpeExpressionCurve {
30    pub points: Vec<MpeExpressionPoint>,
31}
32
33#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
34pub struct MpeNoteExpression {
35    pub pitch_bend: MpeExpressionCurve,
36    pub pressure: MpeExpressionCurve,
37    pub timbre: MpeExpressionCurve,
38}
39
40#[derive(Clone, Debug)]
41pub struct MidiNoteData {
42    pub start_sample: usize,
43    pub length_samples: usize,
44    pub pitch: u8,
45    pub velocity: u8,
46    pub channel: u8,
47    pub mpe: MpeNoteExpression,
48}
49
50#[derive(Clone, Debug)]
51pub struct MidiControllerData {
52    pub sample: usize,
53    pub controller: u8,
54    pub value: u8,
55    pub channel: u8,
56}
57
58#[derive(Debug, Clone)]
59pub struct MidiRawEventData {
60    pub sample: usize,
61    pub data: Vec<u8>,
62}
63
64#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
65pub struct JackPortInfo {
66    pub name: String,
67    pub kind: Kind,
68    pub is_input: bool,
69    pub is_output: bool,
70    pub is_physical: bool,
71    pub is_maolan: bool,
72}
73
74#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
75pub struct JackConnectionInfo {
76    pub source: String,
77    pub destination: String,
78}
79
80#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
81pub struct JackGraphInfo {
82    pub ports: Vec<JackPortInfo>,
83    pub connections: Vec<JackConnectionInfo>,
84}
85
86#[derive(Clone, Debug)]
87pub struct HwMidiEvent {
88    pub device: String,
89    pub event: MidiEvent,
90}
91
92#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
93pub struct OfflineAutomationPoint {
94    pub sample: usize,
95    pub value: f32,
96}
97
98#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
99pub enum OfflineAutomationTarget {
100    Volume,
101    Balance,
102    MidiCc {
103        channel: u8,
104        cc: u8,
105    },
106    #[cfg(unix)]
107    Lv2Parameter {
108        instance_id: usize,
109        index: u32,
110        min: f32,
111        max: f32,
112    },
113    Vst3Parameter {
114        instance_id: usize,
115        param_id: u32,
116    },
117    ClapParameter {
118        instance_id: usize,
119        param_id: u32,
120        min: f64,
121        max: f64,
122    },
123    MixOsc {
124        addr: String,
125        path: String,
126    },
127}
128
129#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
130pub struct OfflineAutomationLane {
131    pub target: OfflineAutomationTarget,
132    #[serde(default)]
133    pub visible: bool,
134    pub points: Vec<OfflineAutomationPoint>,
135}
136
137impl OfflineAutomationLane {
138    /// Linearly interpolates the lane value at `sample`, clamped to [0, 1].
139    /// Returns `None` when the lane has no points.
140    pub fn value_at(&self, sample: usize) -> Option<f32> {
141        let points = &self.points;
142        if points.is_empty() {
143            return None;
144        }
145        if sample <= points[0].sample {
146            return Some(points[0].value.clamp(0.0, 1.0));
147        }
148        let last = points.len().saturating_sub(1);
149        if sample >= points[last].sample {
150            return Some(points[last].value.clamp(0.0, 1.0));
151        }
152        for segment in points.windows(2) {
153            let left = &segment[0];
154            let right = &segment[1];
155            if sample < left.sample || sample > right.sample {
156                continue;
157            }
158            let span = right.sample.saturating_sub(left.sample).max(1) as f32;
159            let t = (sample.saturating_sub(left.sample) as f32 / span).clamp(0.0, 1.0);
160            return Some((left.value + (right.value - left.value) * t).clamp(0.0, 1.0));
161        }
162        None
163    }
164}
165
166#[derive(Clone, Debug)]
167pub struct OfflineBounceWork {
168    pub state: Arc<crate::state::StateSnapshot>,
169    pub track_name: String,
170    pub output_path: String,
171    pub start_sample: usize,
172    pub length_samples: usize,
173    pub tempo_bpm: f64,
174    pub tsig_num: u16,
175    pub tsig_denom: u16,
176    pub automation_lanes: Vec<OfflineAutomationLane>,
177    pub cancel: Arc<AtomicBool>,
178    pub apply_fader: bool,
179}
180
181#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
182pub struct PitchCorrectionPointData {
183    pub start_sample: usize,
184    pub length_samples: usize,
185    pub detected_midi_pitch: f32,
186    pub target_midi_pitch: f32,
187    pub clarity: f32,
188}
189
190#[derive(Clone, Debug, PartialEq)]
191pub struct TempoPoint {
192    pub sample: usize,
193    pub bpm: f64,
194}
195
196#[derive(Clone, Debug, PartialEq)]
197pub struct TimeSignaturePoint {
198    pub sample: usize,
199    pub numerator: u16,
200    pub denominator: u16,
201}
202
203#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
204pub struct AudioClipData {
205    #[serde(default)]
206    pub id: String,
207    pub name: String,
208    pub start: usize,
209    pub length: usize,
210    pub offset: usize,
211    pub input_channel: usize,
212    pub muted: bool,
213    pub peaks_file: Option<String>,
214    pub fade_enabled: bool,
215    pub fade_in_samples: usize,
216    pub fade_out_samples: usize,
217    pub preview_name: Option<String>,
218    pub source_name: Option<String>,
219    pub source_offset: Option<usize>,
220    pub source_length: Option<usize>,
221    pub pitch_correction_points: Vec<PitchCorrectionPointData>,
222    pub pitch_correction_frame_likeness: Option<f32>,
223    pub pitch_correction_inertia_ms: Option<u16>,
224    pub pitch_correction_formant_compensation: Option<bool>,
225    pub plugin_graph_json: Option<serde_json::Value>,
226    pub grouped_clips: Vec<AudioClipData>,
227}
228
229#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
230pub struct MidiClipData {
231    #[serde(default)]
232    pub id: String,
233    pub name: String,
234    pub start: usize,
235    pub length: usize,
236    pub offset: usize,
237    pub input_channel: usize,
238    pub muted: bool,
239    pub grouped_clips: Vec<MidiClipData>,
240}
241
242/// Generates a new unique clip identifier.
243pub fn generate_clip_id() -> String {
244    uuid::Uuid::new_v4().to_string()
245}
246
247#[derive(Clone, Debug)]
248pub struct ClipMoveFrom {
249    pub track_name: String,
250    pub clip_index: usize,
251}
252
253#[derive(Clone, Debug)]
254pub struct ClipMoveTo {
255    pub track_name: String,
256    pub sample_offset: usize,
257    pub input_channel: usize,
258}
259
260#[derive(Clone, Debug, PartialEq, Eq, Hash)]
261pub enum PluginGraphNode {
262    TrackInput,
263    TrackOutput,
264    ClapPluginInstance(usize),
265    Vst3PluginInstance(usize),
266    #[cfg(unix)]
267    Lv2PluginInstance(usize),
268}
269
270#[derive(Clone, Debug, PartialEq)]
271pub struct PluginGraphPlugin {
272    pub node: PluginGraphNode,
273    pub instance_id: usize,
274    pub format: String,
275    pub uri: String,
276    pub plugin_id: String,
277    pub name: String,
278    pub main_audio_inputs: usize,
279    pub main_audio_outputs: usize,
280    pub audio_inputs: usize,
281    pub audio_outputs: usize,
282    pub midi_inputs: usize,
283    pub midi_outputs: usize,
284    pub state: Option<serde_json::Value>,
285    pub bypassed: bool,
286}
287
288#[cfg(unix)]
289#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
290pub struct Lv2StatePortValue {
291    pub index: u32,
292    pub value: f32,
293}
294
295#[cfg(unix)]
296#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
297pub struct Lv2StateProperty {
298    pub key_uri: String,
299    pub type_uri: String,
300    pub flags: u32,
301    pub value: Vec<u8>,
302}
303
304#[cfg(unix)]
305#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
306pub struct Lv2PluginState {
307    pub port_values: Vec<Lv2StatePortValue>,
308    pub properties: Vec<Lv2StateProperty>,
309}
310
311#[cfg(unix)]
312#[derive(Clone, Debug, PartialEq)]
313pub struct Lv2ControlPortInfo {
314    pub index: u32,
315    pub name: String,
316    pub min: f32,
317    pub max: f32,
318    pub value: f32,
319}
320
321#[derive(Clone, Debug, PartialEq, Eq)]
322pub struct PluginGraphConnection {
323    pub from_node: PluginGraphNode,
324    pub from_port: usize,
325    pub to_node: PluginGraphNode,
326    pub to_port: usize,
327    pub kind: Kind,
328}
329
330pub type PluginGraphSnapshot = (Vec<PluginGraphPlugin>, Vec<PluginGraphConnection>);
331
332#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash)]
333pub enum PluginKind {
334    Clap,
335    Vst3,
336    #[cfg(unix)]
337    Lv2,
338}
339
340#[derive(Clone, Debug)]
341pub enum ProcessTask {
342    Track(TrackHandle),
343    FolderInput(TrackHandle),
344    FolderOutput(TrackHandle),
345    Plugin {
346        track: TrackHandle,
347        kind: PluginKind,
348        index: usize,
349    },
350}
351
352impl PartialEq for ProcessTask {
353    fn eq(&self, other: &Self) -> bool {
354        use ProcessTask::*;
355        match (self, other) {
356            (Track(a), Track(b))
357            | (FolderInput(a), FolderInput(b))
358            | (FolderOutput(a), FolderOutput(b)) => Arc::ptr_eq(a, b),
359            (
360                Plugin {
361                    track: a,
362                    kind: ka,
363                    index: ia,
364                },
365                Plugin {
366                    track: b,
367                    kind: kb,
368                    index: ib,
369                },
370            ) => Arc::ptr_eq(a, b) && ka == kb && ia == ib,
371            _ => false,
372        }
373    }
374}
375
376impl Eq for ProcessTask {}
377
378impl std::hash::Hash for ProcessTask {
379    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
380        use ProcessTask::*;
381        match self {
382            Track(t) | FolderInput(t) | FolderOutput(t) => {
383                Arc::as_ptr(t).hash(state);
384            }
385            Plugin { track, kind, index } => {
386                Arc::as_ptr(track).hash(state);
387                kind.hash(state);
388                index.hash(state);
389            }
390        }
391    }
392}
393
394#[derive(Clone, Debug, PartialEq, Eq, Hash)]
395pub enum Vst3GraphNode {
396    TrackInput,
397    TrackOutput,
398    PluginInstance(usize),
399}
400
401#[derive(Clone, Debug)]
402pub struct Vst3GraphPlugin {
403    pub instance_id: usize,
404    pub name: String,
405    pub path: String,
406    pub audio_inputs: usize,
407    pub audio_outputs: usize,
408    pub parameters: Vec<crate::vst3::port::ParameterInfo>,
409}
410
411#[derive(Clone, Debug, PartialEq, Eq)]
412pub struct Vst3GraphConnection {
413    pub from_node: Vst3GraphNode,
414    pub from_port: usize,
415    pub to_node: Vst3GraphNode,
416    pub to_port: usize,
417    pub kind: Kind,
418}
419
420#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
421pub struct MidiLearnBinding {
422    pub device: Option<String>,
423    pub channel: u8,
424    pub cc: u8,
425}
426
427#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
428pub enum TrackMidiLearnTarget {
429    Volume,
430    Balance,
431    Mute,
432    Solo,
433    Arm,
434    InputMonitor,
435    DiskMonitor,
436}
437
438#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
439pub enum GlobalMidiLearnTarget {
440    PlayPause,
441    Stop,
442    RecordToggle,
443}
444
445#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
446pub enum SessionMidiLearnTarget {
447    Slot {
448        track_name: String,
449        scene_index: usize,
450    },
451    Scene(usize),
452    StopTrack(String),
453    StopAll,
454}
455
456#[derive(Clone, Copy, Debug, PartialEq, Eq)]
457pub enum SessionSlotState {
458    Stopped,
459    Queued,
460    Playing,
461    Stopping,
462}
463
464#[derive(Clone, Copy, Debug, PartialEq, Eq)]
465pub enum LaunchQuantization {
466    None,
467    Beat,
468    Bar,
469    TwoBars,
470    FourBars,
471    EightBars,
472    Eighth,
473    Sixteenth,
474    ThirtySecond,
475    SixtyFourth,
476}
477
478#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
479pub enum TrackAutomationMode {
480    #[default]
481    Read,
482    Touch,
483    Latch,
484    Write,
485}
486
487impl TrackAutomationMode {
488    pub fn as_u8(self) -> u8 {
489        match self {
490            Self::Read => 0,
491            Self::Touch => 1,
492            Self::Latch => 2,
493            Self::Write => 3,
494        }
495    }
496
497    pub fn from_u8(value: u8) -> Self {
498        match value {
499            1 => Self::Touch,
500            2 => Self::Latch,
501            3 => Self::Write,
502            _ => Self::Read,
503        }
504    }
505}
506
507#[derive(Clone, Debug)]
508pub enum SessionAction {
509    LaunchClip {
510        track_name: String,
511        scene_index: usize,
512        clip_id: String,
513        launch_quantization: LaunchQuantization,
514        loop_enabled: bool,
515        loop_start_samples: usize,
516        loop_end_samples: usize,
517    },
518    StopClip {
519        track_name: String,
520        scene_index: usize,
521        launch_quantization: LaunchQuantization,
522    },
523    LaunchScene {
524        scene_index: usize,
525        launch_quantization: LaunchQuantization,
526    },
527    StopScene {
528        scene_index: usize,
529        launch_quantization: LaunchQuantization,
530    },
531    /// Queue a scene to launch when the longest currently playing clip
532    /// finishes its current pass. When the current scene has no clips, it
533    /// behaves as a clip whose length is `launch_quantization`.
534    /// Queueing a different scene replaces the queue. Playing clips stop
535    /// when the queued scene launches.
536    QueueScene {
537        scene_index: usize,
538        launch_quantization: LaunchQuantization,
539    },
540    StopAllClips,
541}
542
543#[derive(Clone, Debug)]
544pub enum Action {
545    Quit,
546    Log {
547        source: String,
548        message: String,
549    },
550    Play,
551    Pause,
552    Stop,
553    SessionPlay,
554    TransportPosition(usize),
555    TransportPositionAt {
556        sample: usize,
557        after_frames: usize,
558    },
559    JumpToEnd,
560    SetLoopEnabled(bool),
561    SetLoopRange(Option<(usize, usize)>),
562    SetPunchEnabled(bool),
563    SetPunchRange(Option<(usize, usize)>),
564    SetMetronomeEnabled(bool),
565    SetTempo(f64),
566    SetTimeSignature {
567        numerator: u16,
568        denominator: u16,
569    },
570    SetTempoMap {
571        tempo_points: Vec<TempoPoint>,
572        time_signature_points: Vec<TimeSignaturePoint>,
573    },
574    SetOscEnabled(bool),
575    SetClipPlaybackEnabled(bool),
576    SetSessionClipPlaybackEnabled(bool),
577    SetRecordEnabled(bool),
578    SetModulators(Vec<Modulator>),
579    SetTrackAutomationLanes {
580        track_name: String,
581        lanes: serde_json::Value,
582        mode: TrackAutomationMode,
583    },
584    TrackAutomationToggleLane {
585        track_name: String,
586        target: OfflineAutomationTarget,
587    },
588    TrackAutomationInsertPoint {
589        track_name: String,
590        target: OfflineAutomationTarget,
591        sample: usize,
592        value: f32,
593    },
594    TrackAutomationDeletePoint {
595        track_name: String,
596        target: OfflineAutomationTarget,
597        sample: usize,
598    },
599    TrackAutomationSetMode {
600        track_name: String,
601        mode: TrackAutomationMode,
602    },
603    SetSessionPath(String),
604    BeginHistoryGroup,
605    EndHistoryGroup,
606    ApplyGroupedActions(Vec<Action>),
607    ClearHistory,
608    BeginSessionRestore,
609    EndSessionRestore,
610    AddTrack {
611        name: String,
612        audio_ins: usize,
613        midi_ins: usize,
614        audio_outs: usize,
615        midi_outs: usize,
616        folder: bool,
617        mixosc_addr: Option<String>,
618    },
619    TrackAddAudioInput(String),
620    TrackAddAudioOutput(String),
621    TrackRemoveAudioInput(String),
622    TrackRemoveAudioOutput(String),
623    AddClip {
624        clip_id: String,
625        name: String,
626        track_name: String,
627        start: usize,
628        length: usize,
629        offset: usize,
630        input_channel: usize,
631        muted: bool,
632        peaks_file: Option<String>,
633        kind: Kind,
634        fade_enabled: bool,
635        fade_in_samples: usize,
636        fade_out_samples: usize,
637        source_name: Option<String>,
638        source_offset: Option<usize>,
639        source_length: Option<usize>,
640        preview_name: Option<String>,
641        pitch_correction_points: Vec<PitchCorrectionPointData>,
642        pitch_correction_frame_likeness: Option<f32>,
643        pitch_correction_inertia_ms: Option<u16>,
644        pitch_correction_formant_compensation: Option<bool>,
645        plugin_graph_json: Option<serde_json::Value>,
646    },
647    AddGroupedClip {
648        track_name: String,
649        kind: Kind,
650        audio_clip: Option<AudioClipData>,
651        midi_clip: Option<MidiClipData>,
652    },
653    RemoveClip {
654        track_name: String,
655        kind: Kind,
656        clip_indices: Vec<usize>,
657    },
658    MoveClipToUnused {
659        track_name: String,
660        kind: Kind,
661        clip_indices: Vec<usize>,
662    },
663    DeleteUnusedClips {
664        clip_ids: Vec<String>,
665    },
666    SetUnusedClips {
667        audio: Vec<AudioClipData>,
668        midi: Vec<MidiClipData>,
669    },
670    SetClipFade {
671        track_name: String,
672        clip_index: usize,
673        kind: Kind,
674        fade_enabled: bool,
675        fade_in_samples: usize,
676        fade_out_samples: usize,
677    },
678    SetClipBounds {
679        track_name: String,
680        clip_index: usize,
681        kind: Kind,
682        start: usize,
683        length: usize,
684        offset: usize,
685    },
686    SyncClipBounds {
687        track_name: String,
688        clip_index: usize,
689        kind: Kind,
690        start: usize,
691        length: usize,
692        offset: usize,
693    },
694    SetClipMuted {
695        track_name: String,
696        clip_index: usize,
697        kind: Kind,
698        muted: bool,
699    },
700    SetClipPluginGraphJson {
701        track_name: String,
702        clip_index: usize,
703        plugin_graph_json: Option<serde_json::Value>,
704    },
705    SetClipPitchCorrection {
706        track_name: String,
707        clip_index: usize,
708        preview_name: Option<String>,
709        source_name: Option<String>,
710        source_offset: Option<usize>,
711        source_length: Option<usize>,
712        pitch_correction_points: Vec<PitchCorrectionPointData>,
713        pitch_correction_frame_likeness: Option<f32>,
714        pitch_correction_inertia_ms: Option<u16>,
715        pitch_correction_formant_compensation: Option<bool>,
716    },
717    RenameClip {
718        track_name: String,
719        kind: Kind,
720        clip_index: usize,
721        new_name: String,
722    },
723    SetClipIdentity {
724        track_name: String,
725        kind: Kind,
726        clip_index: usize,
727        new_id: String,
728        new_name: String,
729    },
730    SetClipSourceName {
731        track_name: String,
732        kind: Kind,
733        clip_index: usize,
734        name: String,
735    },
736    RenameTrack {
737        old_name: String,
738        new_name: String,
739    },
740    RemoveTrack(String),
741    TrackLevel(String, f32),
742    TrackBalance(String, f32),
743    TrackAutomationLevel(String, f32),
744    TrackAutomationBalance(String, f32),
745    TrackMidiCc {
746        track_name: String,
747        channel: u8,
748        cc: u8,
749        value: u8,
750    },
751    TrackMeters {
752        track_name: String,
753        output_db: Vec<f32>,
754    },
755    RequestMeterSnapshot,
756    MeterSnapshot {
757        hw_out_db: Arc<Vec<f32>>,
758        hw_out_lufs: Option<crate::loudness::LoudnessValues>,
759        track_meters: Arc<Vec<(String, Vec<f32>)>>,
760    },
761    RequestTrackList,
762    TrackList(Vec<String>),
763    RequestTransportState,
764    TransportState {
765        sample: usize,
766        tempo_bpm: f64,
767        playing: bool,
768        paused: bool,
769        tsig_num: u16,
770        tsig_denom: u16,
771    },
772    TrackToggleArm(String),
773    TrackToggleMute(String),
774    TrackTogglePhase(String),
775    TrackToggleSolo(String),
776    TrackToggleMaster(String),
777    TrackToggleInputMonitor {
778        track_name: String,
779        lane: usize,
780    },
781    TrackToggleDiskMonitor {
782        track_name: String,
783        lane: usize,
784    },
785    TrackToggleMidiInputMonitor {
786        track_name: String,
787        lane: usize,
788    },
789    TrackToggleMidiDiskMonitor {
790        track_name: String,
791        lane: usize,
792    },
793    TrackSetColor {
794        track_name: String,
795        color: Option<TrackColor>,
796    },
797    TrackArmMidiLearn {
798        track_name: String,
799        target: TrackMidiLearnTarget,
800    },
801    GlobalArmMidiLearn {
802        target: GlobalMidiLearnTarget,
803    },
804    SessionArmMidiLearn {
805        target: SessionMidiLearnTarget,
806    },
807    TrackSetMidiLearnBinding {
808        track_name: String,
809        target: TrackMidiLearnTarget,
810        binding: Option<MidiLearnBinding>,
811    },
812    SetGlobalMidiLearnBinding {
813        target: GlobalMidiLearnTarget,
814        binding: Option<MidiLearnBinding>,
815    },
816    SetSessionMidiLearnBinding {
817        target: SessionMidiLearnTarget,
818        binding: Option<MidiLearnBinding>,
819    },
820    SessionMidiLearnTriggered {
821        target: SessionMidiLearnTarget,
822    },
823    TrackSetFolder {
824        track_name: String,
825        is_folder: bool,
826    },
827    TrackSetParent {
828        track_name: String,
829        parent_name: Option<String>,
830    },
831    TrackToggleFolder {
832        track_name: String,
833    },
834    TrackSetMidiLaneChannel {
835        track_name: String,
836        lane: usize,
837        channel: Option<u8>,
838    },
839    TrackSetMpeZone {
840        track_name: String,
841        manager_channel: u8,
842        member_count: u8,
843    },
844    TrackSetMpePitchBendSensitivity {
845        track_name: String,
846        channel: u8,
847        semitones: u8,
848    },
849    TrackSetFrozen {
850        track_name: String,
851        frozen: bool,
852    },
853    TrackSetSessionSlot {
854        track_name: String,
855        scene_index: usize,
856        clip_id: Option<String>,
857    },
858    TrackSetSessionSlotPlayEnabled {
859        track_name: String,
860        scene_index: usize,
861        enabled: bool,
862    },
863    TrackSetSessionSlotStopEnabled {
864        track_name: String,
865        scene_index: usize,
866        enabled: bool,
867    },
868    TrackOfflineBounce {
869        track_name: String,
870        output_path: String,
871        start_sample: usize,
872        length_samples: usize,
873        automation_lanes: Vec<OfflineAutomationLane>,
874        apply_fader: bool,
875    },
876    TrackOfflineBounceCancel {
877        track_name: String,
878    },
879    TrackOfflineBounceCancelAll,
880    TrackOfflineBounceCanceled {
881        track_name: String,
882    },
883    TrackOfflineBounceProgress {
884        track_name: String,
885        progress: f32,
886        operation: Option<String>,
887    },
888    PianoKey {
889        track_name: String,
890        note: u8,
891        velocity: u8,
892        on: bool,
893    },
894    ModifyMidiNotes {
895        track_name: String,
896        clip_index: usize,
897        note_indices: Vec<usize>,
898        new_notes: Vec<MidiNoteData>,
899        old_notes: Vec<MidiNoteData>,
900    },
901    ModifyMidiControllers {
902        track_name: String,
903        clip_index: usize,
904        controller_indices: Vec<usize>,
905        new_controllers: Vec<MidiControllerData>,
906        old_controllers: Vec<MidiControllerData>,
907    },
908    DeleteMidiControllers {
909        track_name: String,
910        clip_index: usize,
911        controller_indices: Vec<usize>,
912        deleted_controllers: Vec<(usize, MidiControllerData)>,
913    },
914    InsertMidiControllers {
915        track_name: String,
916        clip_index: usize,
917        controllers: Vec<(usize, MidiControllerData)>,
918    },
919    DeleteMidiNotes {
920        track_name: String,
921        clip_index: usize,
922        note_indices: Vec<usize>,
923        deleted_notes: Vec<(usize, MidiNoteData)>,
924    },
925    InsertMidiNotes {
926        track_name: String,
927        clip_index: usize,
928        notes: Vec<(usize, MidiNoteData)>,
929    },
930    SetStepRecording(bool),
931    StepRecordMidiNote {
932        device: String,
933        channel: u8,
934        pitch: u8,
935        velocity: u8,
936    },
937    SetMidiSysExEvents {
938        track_name: String,
939        clip_index: usize,
940        new_sysex_events: Vec<MidiRawEventData>,
941        old_sysex_events: Vec<MidiRawEventData>,
942    },
943    TrackClearDefaultPassthrough {
944        track_name: String,
945    },
946    TrackClearPlugins {
947        track_name: String,
948    },
949    #[cfg(unix)]
950    TrackSetLv2PluginState {
951        track_name: String,
952        instance_id: usize,
953        state: Vec<u8>,
954    },
955    #[cfg(unix)]
956    ClipSetLv2PluginState {
957        track_name: String,
958        clip_idx: usize,
959        instance_id: usize,
960        state: Vec<u8>,
961    },
962    #[cfg(unix)]
963    TrackLv2SnapshotState {
964        track_name: String,
965        instance_id: usize,
966    },
967    #[cfg(unix)]
968    ClipLv2SnapshotState {
969        track_name: String,
970        clip_idx: usize,
971        instance_id: usize,
972    },
973    #[cfg(unix)]
974    TrackLv2StateSnapshot {
975        track_name: String,
976        instance_id: usize,
977        state: Vec<u8>,
978    },
979    #[cfg(unix)]
980    TrackGetLv2PluginControls {
981        track_name: String,
982        instance_id: usize,
983    },
984    #[cfg(unix)]
985    ClipGetLv2PluginControls {
986        track_name: String,
987        clip_idx: usize,
988        instance_id: usize,
989    },
990    #[cfg(unix)]
991    TrackLv2PluginControls {
992        track_name: String,
993        instance_id: usize,
994        controls: Vec<Lv2ControlPortInfo>,
995        instance_access_handle: Option<usize>,
996    },
997    #[cfg(unix)]
998    ClipLv2PluginControls {
999        track_name: String,
1000        clip_idx: usize,
1001        instance_id: usize,
1002        controls: Vec<Lv2ControlPortInfo>,
1003        instance_access_handle: Option<usize>,
1004    },
1005    #[cfg(unix)]
1006    TrackGetLv2Midnam {
1007        track_name: String,
1008    },
1009    #[cfg(unix)]
1010    TrackLv2Midnam {
1011        track_name: String,
1012        note_names: std::collections::HashMap<u8, String>,
1013    },
1014    TrackGetClapNoteNames {
1015        track_name: String,
1016    },
1017    TrackClapNoteNames {
1018        track_name: String,
1019        note_names: std::collections::HashMap<u8, String>,
1020    },
1021    #[cfg(unix)]
1022    TrackSetLv2ControlValue {
1023        track_name: String,
1024        instance_id: usize,
1025        index: u32,
1026        value: f32,
1027    },
1028    #[cfg(unix)]
1029    ClipSetLv2ControlValue {
1030        track_name: String,
1031        clip_idx: usize,
1032        instance_id: usize,
1033        index: u32,
1034        value: f32,
1035    },
1036    #[cfg(unix)]
1037    ClipLv2StateSnapshot {
1038        track_name: String,
1039        clip_idx: usize,
1040        instance_id: usize,
1041        state: Vec<u8>,
1042    },
1043    TrackGetPluginGraph {
1044        track_name: String,
1045        include_state: bool,
1046    },
1047    TrackPluginGraph {
1048        track_name: String,
1049        plugins: Vec<PluginGraphPlugin>,
1050        connections: Vec<PluginGraphConnection>,
1051        connectable_connections: Vec<ConnectableConnection>,
1052    },
1053    TrackConnectPluginAudio {
1054        track_name: String,
1055        from_node: PluginGraphNode,
1056        from_port: usize,
1057        to_node: PluginGraphNode,
1058        to_port: usize,
1059    },
1060    TrackConnectPluginMidi {
1061        track_name: String,
1062        from_node: PluginGraphNode,
1063        from_port: usize,
1064        to_node: PluginGraphNode,
1065        to_port: usize,
1066    },
1067    TrackDisconnectPluginAudio {
1068        track_name: String,
1069        from_node: PluginGraphNode,
1070        from_port: usize,
1071        to_node: PluginGraphNode,
1072        to_port: usize,
1073    },
1074    TrackDisconnectPluginMidi {
1075        track_name: String,
1076        from_node: PluginGraphNode,
1077        from_port: usize,
1078        to_node: PluginGraphNode,
1079        to_port: usize,
1080    },
1081    TrackConnectAudio {
1082        track_name: String,
1083        from: ConnectableRef,
1084        from_port: usize,
1085        to: ConnectableRef,
1086        to_port: usize,
1087    },
1088    TrackDisconnectAudio {
1089        track_name: String,
1090        from: ConnectableRef,
1091        from_port: usize,
1092        to: ConnectableRef,
1093        to_port: usize,
1094    },
1095    TrackConnectMidi {
1096        track_name: String,
1097        from: ConnectableRef,
1098        from_port: usize,
1099        to: ConnectableRef,
1100        to_port: usize,
1101    },
1102    TrackDisconnectMidi {
1103        track_name: String,
1104        from: ConnectableRef,
1105        from_port: usize,
1106        to: ConnectableRef,
1107        to_port: usize,
1108    },
1109    #[cfg(unix)]
1110    ListLv2Plugins,
1111    #[cfg(unix)]
1112    Lv2Plugins(Vec<Lv2PluginInfo>),
1113    #[cfg(unix)]
1114    Lv2PluginsUnavailable {
1115        error: String,
1116    },
1117    ListVst3Plugins,
1118    Vst3Plugins(Vec<Vst3PluginInfo>),
1119    Vst3PluginsUnavailable {
1120        error: String,
1121    },
1122    ListClapPlugins,
1123    ListClapPluginsWithCapabilities,
1124    ClapPlugins(Vec<ClapPluginInfo>),
1125    ClapPluginsUnavailable {
1126        error: String,
1127    },
1128    TrackSetClapParameter {
1129        track_name: String,
1130        instance_id: usize,
1131        param_id: u32,
1132        value: f64,
1133    },
1134    ClipSetClapParameter {
1135        track_name: String,
1136        clip_idx: usize,
1137        instance_id: usize,
1138        param_id: u32,
1139        value: f64,
1140    },
1141    TrackSetClapParameterAt {
1142        track_name: String,
1143        instance_id: usize,
1144        param_id: u32,
1145        value: f64,
1146        frame: u32,
1147    },
1148    TrackBeginClapParameterEdit {
1149        track_name: String,
1150        instance_id: usize,
1151        param_id: u32,
1152        frame: u32,
1153    },
1154    TrackEndClapParameterEdit {
1155        track_name: String,
1156        instance_id: usize,
1157        param_id: u32,
1158        frame: u32,
1159    },
1160    TrackGetClapParameters {
1161        track_name: String,
1162        instance_id: usize,
1163    },
1164    TrackClapParameters {
1165        track_name: String,
1166        instance_id: usize,
1167        parameters: Vec<ClapParameterInfo>,
1168    },
1169    TrackClapSnapshotState {
1170        track_name: String,
1171        instance_id: usize,
1172    },
1173    ClipClapSnapshotState {
1174        track_name: String,
1175        clip_idx: usize,
1176        instance_id: usize,
1177    },
1178    TrackClapStateSnapshot {
1179        track_name: String,
1180        instance_id: usize,
1181        plugin_id: String,
1182        state: crate::clap::ClapPluginState,
1183    },
1184    ClipClapStateSnapshot {
1185        track_name: String,
1186        clip_idx: usize,
1187        instance_id: usize,
1188        plugin_id: String,
1189        state: crate::clap::ClapPluginState,
1190    },
1191    TrackClapStateDirty {
1192        track_name: String,
1193        instance_id: usize,
1194    },
1195    ClipClapStateDirty {
1196        track_name: String,
1197        clip_idx: usize,
1198        instance_id: usize,
1199    },
1200    TrackClapRestoreState {
1201        track_name: String,
1202        instance_id: usize,
1203        state: crate::clap::ClapPluginState,
1204    },
1205    ClipClapRestoreState {
1206        track_name: String,
1207        clip_idx: usize,
1208        instance_id: usize,
1209        state: crate::clap::ClapPluginState,
1210    },
1211    TrackSnapshotAllClapStates {
1212        track_name: String,
1213    },
1214    TrackSnapshotAllClapStatesDone {
1215        track_name: String,
1216    },
1217    TrackLoadClapPlugin {
1218        track_name: String,
1219        plugin_id: String,
1220        instance_id: Option<usize>,
1221    },
1222    TrackUnloadClapPlugin {
1223        track_name: String,
1224        plugin_id: String,
1225    },
1226    TrackUnloadClapPluginInstance {
1227        track_name: String,
1228        instance_id: usize,
1229    },
1230    TrackShowClapGui {
1231        track_name: String,
1232        instance_id: usize,
1233    },
1234    ClipShowClapGui {
1235        track_name: String,
1236        clip_idx: usize,
1237        instance_id: usize,
1238    },
1239    TrackLoadVst3Plugin {
1240        track_name: String,
1241        plugin_id: String,
1242        instance_id: Option<usize>,
1243    },
1244    TrackUnloadVst3Plugin {
1245        track_name: String,
1246        plugin_id: String,
1247    },
1248    TrackUnloadVst3PluginInstance {
1249        track_name: String,
1250        instance_id: usize,
1251    },
1252    TrackShowVst3Gui {
1253        track_name: String,
1254        instance_id: usize,
1255    },
1256    ClipShowVst3Gui {
1257        track_name: String,
1258        clip_idx: usize,
1259        instance_id: usize,
1260    },
1261    #[cfg(unix)]
1262    TrackLoadLv2Plugin {
1263        track_name: String,
1264        plugin_uri: String,
1265        instance_id: Option<usize>,
1266    },
1267    #[cfg(unix)]
1268    TrackUnloadLv2Plugin {
1269        track_name: String,
1270        plugin_uri: String,
1271    },
1272    #[cfg(unix)]
1273    TrackUnloadLv2PluginInstance {
1274        track_name: String,
1275        instance_id: usize,
1276    },
1277    TrackShowLv2Gui {
1278        track_name: String,
1279        instance_id: usize,
1280    },
1281    ClipShowLv2Gui {
1282        track_name: String,
1283        clip_idx: usize,
1284        instance_id: usize,
1285    },
1286    TrackSetPluginResourceDir {
1287        track_name: String,
1288        instance_id: usize,
1289        format: String,
1290        directory: String,
1291        shared: bool,
1292    },
1293    TrackClapCollectResources {
1294        track_name: String,
1295        instance_id: usize,
1296    },
1297    TrackClapResourceFiles {
1298        track_name: String,
1299        instance_id: usize,
1300        files: Vec<(u32, String)>,
1301    },
1302    ClipSetPluginResourceDir {
1303        track_name: String,
1304        clip_idx: usize,
1305        instance_id: usize,
1306        format: String,
1307        directory: String,
1308        shared: bool,
1309    },
1310    ClipClapCollectResources {
1311        track_name: String,
1312        clip_idx: usize,
1313        instance_id: usize,
1314    },
1315    ClipClapResourceFiles {
1316        track_name: String,
1317        clip_idx: usize,
1318        instance_id: usize,
1319        files: Vec<(u32, String)>,
1320    },
1321    TrackGetVst3Graph {
1322        track_name: String,
1323    },
1324    TrackVst3Graph {
1325        track_name: String,
1326        plugins: Vec<Vst3GraphPlugin>,
1327        connections: Vec<Vst3GraphConnection>,
1328    },
1329    TrackSetVst3Parameter {
1330        track_name: String,
1331        instance_id: usize,
1332        param_id: u32,
1333        value: f32,
1334    },
1335    TrackSetPluginBypassed {
1336        track_name: String,
1337        instance_id: usize,
1338        format: String,
1339        bypassed: bool,
1340    },
1341    TrackGetVst3Parameters {
1342        track_name: String,
1343        instance_id: usize,
1344    },
1345    TrackVst3Parameters {
1346        track_name: String,
1347        instance_id: usize,
1348        parameters: Vec<crate::vst3::port::ParameterInfo>,
1349    },
1350    TrackVst3SnapshotState {
1351        track_name: String,
1352        instance_id: usize,
1353    },
1354    ClipVst3SnapshotState {
1355        track_name: String,
1356        clip_idx: usize,
1357        instance_id: usize,
1358    },
1359    TrackVst3StateSnapshot {
1360        track_name: String,
1361        instance_id: usize,
1362        state: crate::vst3::state::Vst3PluginState,
1363    },
1364    ClipVst3StateSnapshot {
1365        track_name: String,
1366        clip_idx: usize,
1367        instance_id: usize,
1368        state: crate::vst3::state::Vst3PluginState,
1369    },
1370    TrackVst3RestoreState {
1371        track_name: String,
1372        instance_id: usize,
1373        state: crate::vst3::state::Vst3PluginState,
1374    },
1375    ClipVst3RestoreState {
1376        track_name: String,
1377        clip_idx: usize,
1378        instance_id: usize,
1379        state: crate::vst3::state::Vst3PluginState,
1380    },
1381    TrackConnectVst3Audio {
1382        track_name: String,
1383        from_node: Vst3GraphNode,
1384        from_port: usize,
1385        to_node: Vst3GraphNode,
1386        to_port: usize,
1387    },
1388    TrackDisconnectVst3Audio {
1389        track_name: String,
1390        from_node: Vst3GraphNode,
1391        from_port: usize,
1392        to_node: Vst3GraphNode,
1393        to_port: usize,
1394    },
1395    ClipMove {
1396        kind: Kind,
1397        from: ClipMoveFrom,
1398        to: ClipMoveTo,
1399        copy: bool,
1400    },
1401    Connect {
1402        from_track: String,
1403        from_port: usize,
1404        to_track: String,
1405        to_port: usize,
1406        kind: Kind,
1407    },
1408    Disconnect {
1409        from_track: String,
1410        from_port: usize,
1411        to_track: String,
1412        to_port: usize,
1413        kind: Kind,
1414    },
1415    OpenAudioDevice {
1416        device: String,
1417        input_device: Option<String>,
1418        sample_rate_hz: i32,
1419        bits: i32,
1420        exclusive: bool,
1421        period_frames: usize,
1422        nperiods: usize,
1423        sync_mode: bool,
1424        actual_period_frames: usize,
1425        input_channels: usize,
1426        output_channels: usize,
1427        bytes_per_frame: usize,
1428    },
1429    JackAddAudioInputPort,
1430    JackRemoveAudioInputPort(usize),
1431    JackAddAudioOutputPort,
1432    JackRemoveAudioOutputPort(usize),
1433    JackGetGraph,
1434    JackGraph(JackGraphInfo),
1435    JackConnect {
1436        source: String,
1437        destination: String,
1438    },
1439    JackDisconnect {
1440        source: String,
1441        destination: String,
1442    },
1443    OpenMidiInputDevice(String),
1444    OpenMidiOutputDevice(String),
1445    RequestSessionDiagnostics,
1446    RequestMidiLearnMappingsReport,
1447    ClearAllMidiLearnBindings,
1448    SessionDiagnosticsReport {
1449        track_count: usize,
1450        frozen_track_count: usize,
1451        audio_clip_count: usize,
1452        midi_clip_count: usize,
1453        #[cfg(unix)]
1454        lv2_instance_count: usize,
1455        vst3_instance_count: usize,
1456        clap_instance_count: usize,
1457        pending_requests: usize,
1458        workers_total: usize,
1459        workers_ready: usize,
1460        pending_hw_midi_events: usize,
1461        playing: bool,
1462        transport_running: bool,
1463        transport_sample: usize,
1464        tempo_bpm: f64,
1465        sample_rate_hz: usize,
1466        cycle_samples: usize,
1467    },
1468    MidiLearnMappingsReport {
1469        lines: Vec<String>,
1470    },
1471    HWInfo {
1472        channels: usize,
1473        rate: usize,
1474        input: bool,
1475    },
1476    MarkHistorySavePoint,
1477    HistoryState {
1478        dirty: bool,
1479    },
1480    Undo,
1481    Redo,
1482    Session(SessionAction),
1483    SessionRuntimeReport {
1484        track_name: String,
1485        scene_index: usize,
1486        state: SessionSlotState,
1487        play_position_samples: usize,
1488        elapsed_samples: usize,
1489    },
1490    Panic,
1491}
1492
1493#[derive(Clone, Debug)]
1494pub enum Message {
1495    Ready(usize),
1496    TracksFinished,
1497
1498    /// Dispatch a render-plan node to a worker (Phase 2, see `LOCKLESS.md`).
1499    NodeJob(crate::executor::NodeJob),
1500    /// A worker completed a plan node. Stale `epoch`s are dropped.
1501    NodeDone {
1502        worker_id: usize,
1503        epoch: u64,
1504        node: u32,
1505        output_linear: Vec<f32>,
1506        parameter_updates: Vec<Action>,
1507        latency_changed: bool,
1508    },
1509    ProcessOfflineBounce(OfflineBounceWork),
1510    Channel(Sender<Self>),
1511
1512    Request(Action),
1513    OscRequest {
1514        action: Action,
1515        reply_to: SocketAddr,
1516    },
1517    Response(Result<Action, String>),
1518    HWMidiEvents(Vec<HwMidiEvent>),
1519    HWMidiOutEvents(Vec<HwMidiEvent>),
1520    ClearHWMidiOutEvents,
1521    StartAudioPreview {
1522        samples: Arc<Vec<f32>>,
1523        channels: usize,
1524        start_sample: usize,
1525    },
1526    StopAudioPreview,
1527    HWSetPlaying(bool),
1528    HWSetOutputGainBalance {
1529        gain: f32,
1530        balance: f32,
1531    },
1532    HWOpenMidiInputDevice(String),
1533    HWOpenMidiOutputDevice(String),
1534    HWCloseMidiDevices,
1535    HWFinished,
1536    OfflineBounceFinished {
1537        result: Result<Action, String>,
1538    },
1539}
1540
1541#[cfg(test)]
1542mod tests {
1543    use super::{AudioClipData, MidiClipData, PitchCorrectionPointData};
1544    use serde_json::json;
1545
1546    #[test]
1547    fn audio_clip_data_serde_round_trips_nested_groups() {
1548        let clip = AudioClipData {
1549            id: String::new(),
1550            name: "group.wav".to_string(),
1551            start: 12,
1552            length: 96,
1553            offset: 3,
1554            input_channel: 1,
1555            muted: true,
1556            peaks_file: Some("peaks/group.json".to_string()),
1557            fade_enabled: false,
1558            fade_in_samples: 10,
1559            fade_out_samples: 20,
1560            preview_name: Some("preview.wav".to_string()),
1561            source_name: Some("source.wav".to_string()),
1562            source_offset: Some(4),
1563            source_length: Some(88),
1564            pitch_correction_points: vec![PitchCorrectionPointData {
1565                start_sample: 7,
1566                length_samples: 11,
1567                detected_midi_pitch: 60.1,
1568                target_midi_pitch: 61.2,
1569                clarity: 0.8,
1570            }],
1571            pitch_correction_frame_likeness: Some(0.5),
1572            pitch_correction_inertia_ms: Some(123),
1573            pitch_correction_formant_compensation: Some(false),
1574            plugin_graph_json: Some(json!({"plugins":[],"connections":[{"kind":"Audio"}]})),
1575            grouped_clips: vec![AudioClipData {
1576                name: "child.wav".to_string(),
1577                start: 0,
1578                length: 48,
1579                ..AudioClipData::default()
1580            }],
1581        };
1582
1583        let value = serde_json::to_value(&clip).expect("serialize");
1584        let restored: AudioClipData = serde_json::from_value(value).expect("deserialize");
1585
1586        assert_eq!(restored.name, clip.name);
1587        assert_eq!(restored.preview_name, clip.preview_name);
1588        assert_eq!(restored.source_name, clip.source_name);
1589        assert_eq!(restored.plugin_graph_json, clip.plugin_graph_json);
1590        assert_eq!(restored.grouped_clips.len(), 1);
1591        assert_eq!(restored.grouped_clips[0].name, "child.wav");
1592        assert_eq!(restored.pitch_correction_points[0].target_midi_pitch, 61.2);
1593    }
1594
1595    #[test]
1596    fn midi_clip_data_serde_round_trips_nested_groups() {
1597        let clip = MidiClipData {
1598            id: String::new(),
1599            name: "group.mid".to_string(),
1600            start: 5,
1601            length: 64,
1602            offset: 2,
1603            input_channel: 3,
1604            muted: true,
1605            grouped_clips: vec![MidiClipData {
1606                name: "child.mid".to_string(),
1607                start: 0,
1608                length: 32,
1609                ..MidiClipData::default()
1610            }],
1611        };
1612
1613        let value = serde_json::to_value(&clip).expect("serialize");
1614        let restored: MidiClipData = serde_json::from_value(value).expect("deserialize");
1615
1616        assert_eq!(restored.name, clip.name);
1617        assert_eq!(restored.grouped_clips.len(), 1);
1618        assert_eq!(restored.grouped_clips[0].name, "child.mid");
1619    }
1620
1621    #[test]
1622    fn pitch_correction_point_data_serde_round_trips() {
1623        let point = PitchCorrectionPointData {
1624            start_sample: 10,
1625            length_samples: 20,
1626            detected_midi_pitch: 57.5,
1627            target_midi_pitch: 58.0,
1628            clarity: 0.9,
1629        };
1630
1631        let value = serde_json::to_value(&point).expect("serialize");
1632        let restored: PitchCorrectionPointData =
1633            serde_json::from_value(value).expect("deserialize");
1634
1635        assert_eq!(restored.start_sample, 10);
1636        assert_eq!(restored.length_samples, 20);
1637        assert_eq!(restored.detected_midi_pitch, 57.5);
1638        assert_eq!(restored.target_midi_pitch, 58.0);
1639        assert_eq!(restored.clarity, 0.9);
1640    }
1641
1642    #[test]
1643    fn audio_clip_data_deserializes_with_omitted_optional_fields() {
1644        let restored: AudioClipData = serde_json::from_value(json!({
1645            "name": "clip.wav",
1646            "start": 1,
1647            "length": 2,
1648            "offset": 3,
1649            "input_channel": 0,
1650            "muted": false,
1651            "fade_enabled": true,
1652            "fade_in_samples": 240,
1653            "fade_out_samples": 240,
1654            "pitch_correction_points": [],
1655            "grouped_clips": []
1656        }))
1657        .expect("deserialize");
1658
1659        assert_eq!(restored.name, "clip.wav");
1660        assert!(restored.peaks_file.is_none());
1661        assert!(restored.preview_name.is_none());
1662        assert!(restored.source_name.is_none());
1663        assert!(restored.source_offset.is_none());
1664        assert!(restored.source_length.is_none());
1665        assert!(restored.pitch_correction_points.is_empty());
1666        assert!(restored.plugin_graph_json.is_none());
1667    }
1668}