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