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    ClipGetClapParameters {
1142        track_name: String,
1143        clip_idx: usize,
1144        instance_id: usize,
1145    },
1146    ClipClapParameters {
1147        track_name: String,
1148        clip_idx: usize,
1149        instance_id: usize,
1150        parameters: Vec<ClapParameterInfo>,
1151    },
1152    TrackSetClapParameterAt {
1153        track_name: String,
1154        instance_id: usize,
1155        param_id: u32,
1156        value: f64,
1157        frame: u32,
1158    },
1159    TrackBeginClapParameterEdit {
1160        track_name: String,
1161        instance_id: usize,
1162        param_id: u32,
1163        frame: u32,
1164    },
1165    TrackEndClapParameterEdit {
1166        track_name: String,
1167        instance_id: usize,
1168        param_id: u32,
1169        frame: u32,
1170    },
1171    TrackGetClapParameters {
1172        track_name: String,
1173        instance_id: usize,
1174    },
1175    TrackClapParameters {
1176        track_name: String,
1177        instance_id: usize,
1178        parameters: Vec<ClapParameterInfo>,
1179    },
1180    TrackClapSnapshotState {
1181        track_name: String,
1182        instance_id: usize,
1183    },
1184    ClipClapSnapshotState {
1185        track_name: String,
1186        clip_idx: usize,
1187        instance_id: usize,
1188    },
1189    TrackClapStateSnapshot {
1190        track_name: String,
1191        instance_id: usize,
1192        plugin_id: String,
1193        state: crate::clap::ClapPluginState,
1194    },
1195    ClipClapStateSnapshot {
1196        track_name: String,
1197        clip_idx: usize,
1198        instance_id: usize,
1199        plugin_id: String,
1200        state: crate::clap::ClapPluginState,
1201    },
1202    TrackClapStateDirty {
1203        track_name: String,
1204        instance_id: usize,
1205    },
1206    ClipClapStateDirty {
1207        track_name: String,
1208        clip_idx: usize,
1209        instance_id: usize,
1210    },
1211    TrackClapRestoreState {
1212        track_name: String,
1213        instance_id: usize,
1214        state: crate::clap::ClapPluginState,
1215    },
1216    ClipClapRestoreState {
1217        track_name: String,
1218        clip_idx: usize,
1219        instance_id: usize,
1220        state: crate::clap::ClapPluginState,
1221    },
1222    TrackSnapshotAllClapStates {
1223        track_name: String,
1224    },
1225    TrackSnapshotAllClapStatesDone {
1226        track_name: String,
1227    },
1228    TrackLoadClapPlugin {
1229        track_name: String,
1230        plugin_id: String,
1231        instance_id: Option<usize>,
1232    },
1233    TrackUnloadClapPlugin {
1234        track_name: String,
1235        plugin_id: String,
1236    },
1237    TrackUnloadClapPluginInstance {
1238        track_name: String,
1239        instance_id: usize,
1240    },
1241    TrackShowClapGui {
1242        track_name: String,
1243        instance_id: usize,
1244    },
1245    ClipShowClapGui {
1246        track_name: String,
1247        clip_idx: usize,
1248        instance_id: usize,
1249    },
1250    TrackLoadVst3Plugin {
1251        track_name: String,
1252        plugin_id: String,
1253        instance_id: Option<usize>,
1254    },
1255    TrackUnloadVst3Plugin {
1256        track_name: String,
1257        plugin_id: String,
1258    },
1259    TrackUnloadVst3PluginInstance {
1260        track_name: String,
1261        instance_id: usize,
1262    },
1263    TrackShowVst3Gui {
1264        track_name: String,
1265        instance_id: usize,
1266    },
1267    ClipShowVst3Gui {
1268        track_name: String,
1269        clip_idx: usize,
1270        instance_id: usize,
1271    },
1272    #[cfg(unix)]
1273    TrackLoadLv2Plugin {
1274        track_name: String,
1275        plugin_uri: String,
1276        instance_id: Option<usize>,
1277    },
1278    #[cfg(unix)]
1279    TrackUnloadLv2Plugin {
1280        track_name: String,
1281        plugin_uri: String,
1282    },
1283    #[cfg(unix)]
1284    TrackUnloadLv2PluginInstance {
1285        track_name: String,
1286        instance_id: usize,
1287    },
1288    TrackShowLv2Gui {
1289        track_name: String,
1290        instance_id: usize,
1291    },
1292    ClipShowLv2Gui {
1293        track_name: String,
1294        clip_idx: usize,
1295        instance_id: usize,
1296    },
1297    TrackSetPluginResourceDir {
1298        track_name: String,
1299        instance_id: usize,
1300        format: String,
1301        directory: String,
1302        shared: bool,
1303    },
1304    TrackClapCollectResources {
1305        track_name: String,
1306        instance_id: usize,
1307    },
1308    TrackClapResourceFiles {
1309        track_name: String,
1310        instance_id: usize,
1311        files: Vec<(u32, String)>,
1312    },
1313    ClipSetPluginResourceDir {
1314        track_name: String,
1315        clip_idx: usize,
1316        instance_id: usize,
1317        format: String,
1318        directory: String,
1319        shared: bool,
1320    },
1321    ClipClapCollectResources {
1322        track_name: String,
1323        clip_idx: usize,
1324        instance_id: usize,
1325    },
1326    ClipClapResourceFiles {
1327        track_name: String,
1328        clip_idx: usize,
1329        instance_id: usize,
1330        files: Vec<(u32, String)>,
1331    },
1332    TrackGetVst3Graph {
1333        track_name: String,
1334    },
1335    TrackVst3Graph {
1336        track_name: String,
1337        plugins: Vec<Vst3GraphPlugin>,
1338        connections: Vec<Vst3GraphConnection>,
1339    },
1340    TrackSetVst3Parameter {
1341        track_name: String,
1342        instance_id: usize,
1343        param_id: u32,
1344        value: f32,
1345    },
1346    ClipSetVst3Parameter {
1347        track_name: String,
1348        clip_idx: usize,
1349        instance_id: usize,
1350        param_id: u32,
1351        value: f32,
1352    },
1353    TrackSetPluginBypassed {
1354        track_name: String,
1355        instance_id: usize,
1356        format: String,
1357        bypassed: bool,
1358    },
1359    TrackGetVst3Parameters {
1360        track_name: String,
1361        instance_id: usize,
1362    },
1363    TrackVst3Parameters {
1364        track_name: String,
1365        instance_id: usize,
1366        parameters: Vec<crate::vst3::port::ParameterInfo>,
1367    },
1368    ClipGetVst3Parameters {
1369        track_name: String,
1370        clip_idx: usize,
1371        instance_id: usize,
1372    },
1373    ClipVst3Parameters {
1374        track_name: String,
1375        clip_idx: usize,
1376        instance_id: usize,
1377        parameters: Vec<crate::vst3::port::ParameterInfo>,
1378    },
1379    TrackVst3SnapshotState {
1380        track_name: String,
1381        instance_id: usize,
1382    },
1383    ClipVst3SnapshotState {
1384        track_name: String,
1385        clip_idx: usize,
1386        instance_id: usize,
1387    },
1388    TrackVst3StateSnapshot {
1389        track_name: String,
1390        instance_id: usize,
1391        state: crate::vst3::state::Vst3PluginState,
1392    },
1393    ClipVst3StateSnapshot {
1394        track_name: String,
1395        clip_idx: usize,
1396        instance_id: usize,
1397        state: crate::vst3::state::Vst3PluginState,
1398    },
1399    TrackVst3RestoreState {
1400        track_name: String,
1401        instance_id: usize,
1402        state: crate::vst3::state::Vst3PluginState,
1403    },
1404    ClipVst3RestoreState {
1405        track_name: String,
1406        clip_idx: usize,
1407        instance_id: usize,
1408        state: crate::vst3::state::Vst3PluginState,
1409    },
1410    TrackConnectVst3Audio {
1411        track_name: String,
1412        from_node: Vst3GraphNode,
1413        from_port: usize,
1414        to_node: Vst3GraphNode,
1415        to_port: usize,
1416    },
1417    TrackDisconnectVst3Audio {
1418        track_name: String,
1419        from_node: Vst3GraphNode,
1420        from_port: usize,
1421        to_node: Vst3GraphNode,
1422        to_port: usize,
1423    },
1424    ClipMove {
1425        kind: Kind,
1426        from: ClipMoveFrom,
1427        to: ClipMoveTo,
1428        copy: bool,
1429    },
1430    Connect {
1431        from_track: String,
1432        from_port: usize,
1433        to_track: String,
1434        to_port: usize,
1435        kind: Kind,
1436    },
1437    Disconnect {
1438        from_track: String,
1439        from_port: usize,
1440        to_track: String,
1441        to_port: usize,
1442        kind: Kind,
1443    },
1444    OpenAudioDevice {
1445        device: String,
1446        input_device: Option<String>,
1447        sample_rate_hz: i32,
1448        bits: i32,
1449        exclusive: bool,
1450        period_frames: usize,
1451        nperiods: usize,
1452        sync_mode: bool,
1453        actual_period_frames: usize,
1454        input_channels: usize,
1455        output_channels: usize,
1456        bytes_per_frame: usize,
1457    },
1458    JackAddAudioInputPort,
1459    JackRemoveAudioInputPort(usize),
1460    JackAddAudioOutputPort,
1461    JackRemoveAudioOutputPort(usize),
1462    JackGetGraph,
1463    JackGraph(JackGraphInfo),
1464    JackConnect {
1465        source: String,
1466        destination: String,
1467    },
1468    JackDisconnect {
1469        source: String,
1470        destination: String,
1471    },
1472    OpenMidiInputDevice(String),
1473    OpenMidiOutputDevice(String),
1474    RequestSessionDiagnostics,
1475    RequestMidiLearnMappingsReport,
1476    ClearAllMidiLearnBindings,
1477    SessionDiagnosticsReport {
1478        track_count: usize,
1479        frozen_track_count: usize,
1480        audio_clip_count: usize,
1481        midi_clip_count: usize,
1482        #[cfg(unix)]
1483        lv2_instance_count: usize,
1484        vst3_instance_count: usize,
1485        clap_instance_count: usize,
1486        pending_requests: usize,
1487        workers_total: usize,
1488        workers_ready: usize,
1489        pending_hw_midi_events: usize,
1490        playing: bool,
1491        transport_running: bool,
1492        transport_sample: usize,
1493        tempo_bpm: f64,
1494        sample_rate_hz: usize,
1495        cycle_samples: usize,
1496    },
1497    MidiLearnMappingsReport {
1498        lines: Vec<String>,
1499    },
1500    HWInfo {
1501        channels: usize,
1502        rate: usize,
1503        input: bool,
1504    },
1505    MarkHistorySavePoint,
1506    HistoryState {
1507        dirty: bool,
1508    },
1509    Undo,
1510    Redo,
1511    Session(SessionAction),
1512    SessionRuntimeReport {
1513        track_name: String,
1514        scene_index: usize,
1515        state: SessionSlotState,
1516        play_position_samples: usize,
1517        elapsed_samples: usize,
1518    },
1519    Panic,
1520}
1521
1522#[derive(Clone, Debug)]
1523pub enum Message {
1524    Ready(usize),
1525    TracksFinished,
1526
1527    /// Dispatch a render-plan node to a worker (Phase 2, see `LOCKLESS.md`).
1528    NodeJob(crate::executor::NodeJob),
1529    /// A worker completed a plan node. Stale `epoch`s are dropped.
1530    NodeDone {
1531        worker_id: usize,
1532        epoch: u64,
1533        node: u32,
1534        output_linear: Vec<f32>,
1535        parameter_updates: Vec<Action>,
1536        latency_changed: bool,
1537    },
1538    ProcessOfflineBounce(OfflineBounceWork),
1539    Channel(Sender<Self>),
1540
1541    Request(Action),
1542    OscRequest {
1543        action: Action,
1544        reply_to: SocketAddr,
1545    },
1546    Response(Result<Action, String>),
1547    HWMidiEvents(Vec<HwMidiEvent>),
1548    HWMidiOutEvents(Vec<HwMidiEvent>),
1549    ClearHWMidiOutEvents,
1550    StartAudioPreview {
1551        samples: Arc<Vec<f32>>,
1552        channels: usize,
1553        start_sample: usize,
1554    },
1555    StopAudioPreview,
1556    HWSetPlaying(bool),
1557    HWSetOutputGainBalance {
1558        gain: f32,
1559        balance: f32,
1560    },
1561    HWOpenMidiInputDevice(String),
1562    HWOpenMidiOutputDevice(String),
1563    HWCloseMidiDevices,
1564    HWFinished,
1565    OfflineBounceFinished {
1566        result: Result<Action, String>,
1567    },
1568}
1569
1570#[cfg(test)]
1571mod tests {
1572    use super::{AudioClipData, MidiClipData, PitchCorrectionPointData};
1573    use serde_json::json;
1574
1575    #[test]
1576    fn audio_clip_data_serde_round_trips_nested_groups() {
1577        let clip = AudioClipData {
1578            id: String::new(),
1579            name: "group.wav".to_string(),
1580            start: 12,
1581            length: 96,
1582            offset: 3,
1583            input_channel: 1,
1584            muted: true,
1585            peaks_file: Some("peaks/group.json".to_string()),
1586            fade_enabled: false,
1587            fade_in_samples: 10,
1588            fade_out_samples: 20,
1589            preview_name: Some("preview.wav".to_string()),
1590            source_name: Some("source.wav".to_string()),
1591            source_offset: Some(4),
1592            source_length: Some(88),
1593            pitch_correction_points: vec![PitchCorrectionPointData {
1594                start_sample: 7,
1595                length_samples: 11,
1596                detected_midi_pitch: 60.1,
1597                target_midi_pitch: 61.2,
1598                clarity: 0.8,
1599            }],
1600            pitch_correction_frame_likeness: Some(0.5),
1601            pitch_correction_inertia_ms: Some(123),
1602            pitch_correction_formant_compensation: Some(false),
1603            plugin_graph_json: Some(json!({"plugins":[],"connections":[{"kind":"Audio"}]})),
1604            grouped_clips: vec![AudioClipData {
1605                name: "child.wav".to_string(),
1606                start: 0,
1607                length: 48,
1608                ..AudioClipData::default()
1609            }],
1610        };
1611
1612        let value = serde_json::to_value(&clip).expect("serialize");
1613        let restored: AudioClipData = serde_json::from_value(value).expect("deserialize");
1614
1615        assert_eq!(restored.name, clip.name);
1616        assert_eq!(restored.preview_name, clip.preview_name);
1617        assert_eq!(restored.source_name, clip.source_name);
1618        assert_eq!(restored.plugin_graph_json, clip.plugin_graph_json);
1619        assert_eq!(restored.grouped_clips.len(), 1);
1620        assert_eq!(restored.grouped_clips[0].name, "child.wav");
1621        assert_eq!(restored.pitch_correction_points[0].target_midi_pitch, 61.2);
1622    }
1623
1624    #[test]
1625    fn midi_clip_data_serde_round_trips_nested_groups() {
1626        let clip = MidiClipData {
1627            id: String::new(),
1628            name: "group.mid".to_string(),
1629            start: 5,
1630            length: 64,
1631            offset: 2,
1632            input_channel: 3,
1633            muted: true,
1634            grouped_clips: vec![MidiClipData {
1635                name: "child.mid".to_string(),
1636                start: 0,
1637                length: 32,
1638                ..MidiClipData::default()
1639            }],
1640        };
1641
1642        let value = serde_json::to_value(&clip).expect("serialize");
1643        let restored: MidiClipData = serde_json::from_value(value).expect("deserialize");
1644
1645        assert_eq!(restored.name, clip.name);
1646        assert_eq!(restored.grouped_clips.len(), 1);
1647        assert_eq!(restored.grouped_clips[0].name, "child.mid");
1648    }
1649
1650    #[test]
1651    fn pitch_correction_point_data_serde_round_trips() {
1652        let point = PitchCorrectionPointData {
1653            start_sample: 10,
1654            length_samples: 20,
1655            detected_midi_pitch: 57.5,
1656            target_midi_pitch: 58.0,
1657            clarity: 0.9,
1658        };
1659
1660        let value = serde_json::to_value(&point).expect("serialize");
1661        let restored: PitchCorrectionPointData =
1662            serde_json::from_value(value).expect("deserialize");
1663
1664        assert_eq!(restored.start_sample, 10);
1665        assert_eq!(restored.length_samples, 20);
1666        assert_eq!(restored.detected_midi_pitch, 57.5);
1667        assert_eq!(restored.target_midi_pitch, 58.0);
1668        assert_eq!(restored.clarity, 0.9);
1669    }
1670
1671    #[test]
1672    fn audio_clip_data_deserializes_with_omitted_optional_fields() {
1673        let restored: AudioClipData = serde_json::from_value(json!({
1674            "name": "clip.wav",
1675            "start": 1,
1676            "length": 2,
1677            "offset": 3,
1678            "input_channel": 0,
1679            "muted": false,
1680            "fade_enabled": true,
1681            "fade_in_samples": 240,
1682            "fade_out_samples": 240,
1683            "pitch_correction_points": [],
1684            "grouped_clips": []
1685        }))
1686        .expect("deserialize");
1687
1688        assert_eq!(restored.name, "clip.wav");
1689        assert!(restored.peaks_file.is_none());
1690        assert!(restored.preview_name.is_none());
1691        assert!(restored.source_name.is_none());
1692        assert!(restored.source_offset.is_none());
1693        assert!(restored.source_length.is_none());
1694        assert!(restored.pitch_correction_points.is_empty());
1695        assert!(restored.plugin_graph_json.is_none());
1696    }
1697}