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        hw_out_lufs: Option<crate::loudness::LoudnessValues>,
779        track_meters: Arc<Vec<(String, Vec<f32>)>>,
780    },
781    RequestTrackList,
782    TrackList(Vec<String>),
783    RequestTransportState,
784    TransportState {
785        sample: usize,
786        tempo_bpm: f64,
787        playing: bool,
788        paused: bool,
789        tsig_num: u16,
790        tsig_denom: u16,
791    },
792    TrackToggleArm(String),
793    TrackToggleMute(String),
794    TrackTogglePhase(String),
795    TrackToggleSolo(String),
796    TrackToggleMaster(String),
797    TrackToggleInputMonitor {
798        track_name: String,
799        lane: usize,
800    },
801    TrackToggleDiskMonitor {
802        track_name: String,
803        lane: usize,
804    },
805    TrackToggleMidiInputMonitor {
806        track_name: String,
807        lane: usize,
808    },
809    TrackToggleMidiDiskMonitor {
810        track_name: String,
811        lane: usize,
812    },
813    TrackSetColor {
814        track_name: String,
815        color: Option<TrackColor>,
816    },
817    TrackArmMidiLearn {
818        track_name: String,
819        target: TrackMidiLearnTarget,
820    },
821    GlobalArmMidiLearn {
822        target: GlobalMidiLearnTarget,
823    },
824    SessionArmMidiLearn {
825        target: SessionMidiLearnTarget,
826    },
827    TrackSetMidiLearnBinding {
828        track_name: String,
829        target: TrackMidiLearnTarget,
830        binding: Option<MidiLearnBinding>,
831    },
832    SetGlobalMidiLearnBinding {
833        target: GlobalMidiLearnTarget,
834        binding: Option<MidiLearnBinding>,
835    },
836    SetSessionMidiLearnBinding {
837        target: SessionMidiLearnTarget,
838        binding: Option<MidiLearnBinding>,
839    },
840    SessionMidiLearnTriggered {
841        target: SessionMidiLearnTarget,
842    },
843    TrackSetFolder {
844        track_name: String,
845        is_folder: bool,
846    },
847    TrackSetParent {
848        track_name: String,
849        parent_name: Option<String>,
850    },
851    TrackToggleFolder {
852        track_name: String,
853    },
854    TrackSetMidiLaneChannel {
855        track_name: String,
856        lane: usize,
857        channel: Option<u8>,
858    },
859    TrackSetMpeZone {
860        track_name: String,
861        manager_channel: u8,
862        member_count: u8,
863    },
864    TrackSetMpePitchBendSensitivity {
865        track_name: String,
866        channel: u8,
867        semitones: u8,
868    },
869    TrackSetFrozen {
870        track_name: String,
871        frozen: bool,
872    },
873    TrackSetSessionSlot {
874        track_name: String,
875        scene_index: usize,
876        clip_id: Option<String>,
877    },
878    TrackSetSessionSlotPlayEnabled {
879        track_name: String,
880        scene_index: usize,
881        enabled: bool,
882    },
883    TrackSetSessionSlotStopEnabled {
884        track_name: String,
885        scene_index: usize,
886        enabled: bool,
887    },
888    TrackOfflineBounce {
889        track_name: String,
890        output_path: String,
891        start_sample: usize,
892        length_samples: usize,
893        automation_lanes: Vec<OfflineAutomationLane>,
894        apply_fader: bool,
895    },
896    TrackOfflineBounceCancel {
897        track_name: String,
898    },
899    TrackOfflineBounceCancelAll,
900    TrackOfflineBounceCanceled {
901        track_name: String,
902    },
903    TrackOfflineBounceProgress {
904        track_name: String,
905        progress: f32,
906        operation: Option<String>,
907    },
908    PianoKey {
909        track_name: String,
910        note: u8,
911        velocity: u8,
912        on: bool,
913    },
914    ModifyMidiNotes {
915        track_name: String,
916        clip_index: usize,
917        note_indices: Vec<usize>,
918        new_notes: Vec<MidiNoteData>,
919        old_notes: Vec<MidiNoteData>,
920    },
921    ModifyMidiControllers {
922        track_name: String,
923        clip_index: usize,
924        controller_indices: Vec<usize>,
925        new_controllers: Vec<MidiControllerData>,
926        old_controllers: Vec<MidiControllerData>,
927    },
928    DeleteMidiControllers {
929        track_name: String,
930        clip_index: usize,
931        controller_indices: Vec<usize>,
932        deleted_controllers: Vec<(usize, MidiControllerData)>,
933    },
934    InsertMidiControllers {
935        track_name: String,
936        clip_index: usize,
937        controllers: Vec<(usize, MidiControllerData)>,
938    },
939    DeleteMidiNotes {
940        track_name: String,
941        clip_index: usize,
942        note_indices: Vec<usize>,
943        deleted_notes: Vec<(usize, MidiNoteData)>,
944    },
945    InsertMidiNotes {
946        track_name: String,
947        clip_index: usize,
948        notes: Vec<(usize, MidiNoteData)>,
949    },
950    SetStepRecording(bool),
951    StepRecordMidiNote {
952        device: String,
953        channel: u8,
954        pitch: u8,
955        velocity: u8,
956    },
957    SetMidiSysExEvents {
958        track_name: String,
959        clip_index: usize,
960        new_sysex_events: Vec<MidiRawEventData>,
961        old_sysex_events: Vec<MidiRawEventData>,
962    },
963    TrackClearDefaultPassthrough {
964        track_name: String,
965    },
966    TrackClearPlugins {
967        track_name: String,
968    },
969    #[cfg(unix)]
970    TrackSetLv2PluginState {
971        track_name: String,
972        instance_id: usize,
973        state: Vec<u8>,
974    },
975    #[cfg(unix)]
976    ClipSetLv2PluginState {
977        track_name: String,
978        clip_idx: usize,
979        instance_id: usize,
980        state: Vec<u8>,
981    },
982    #[cfg(unix)]
983    TrackLv2SnapshotState {
984        track_name: String,
985        instance_id: usize,
986    },
987    #[cfg(unix)]
988    ClipLv2SnapshotState {
989        track_name: String,
990        clip_idx: usize,
991        instance_id: usize,
992    },
993    #[cfg(unix)]
994    TrackLv2StateSnapshot {
995        track_name: String,
996        instance_id: usize,
997        state: Vec<u8>,
998    },
999    #[cfg(unix)]
1000    TrackGetLv2PluginControls {
1001        track_name: String,
1002        instance_id: usize,
1003    },
1004    #[cfg(unix)]
1005    ClipGetLv2PluginControls {
1006        track_name: String,
1007        clip_idx: usize,
1008        instance_id: usize,
1009    },
1010    #[cfg(unix)]
1011    TrackLv2PluginControls {
1012        track_name: String,
1013        instance_id: usize,
1014        controls: Vec<Lv2ControlPortInfo>,
1015        instance_access_handle: Option<usize>,
1016    },
1017    #[cfg(unix)]
1018    ClipLv2PluginControls {
1019        track_name: String,
1020        clip_idx: usize,
1021        instance_id: usize,
1022        controls: Vec<Lv2ControlPortInfo>,
1023        instance_access_handle: Option<usize>,
1024    },
1025    #[cfg(unix)]
1026    TrackGetLv2Midnam {
1027        track_name: String,
1028    },
1029    #[cfg(unix)]
1030    TrackLv2Midnam {
1031        track_name: String,
1032        note_names: std::collections::HashMap<u8, String>,
1033    },
1034    TrackGetClapNoteNames {
1035        track_name: String,
1036    },
1037    TrackClapNoteNames {
1038        track_name: String,
1039        note_names: std::collections::HashMap<u8, String>,
1040    },
1041    #[cfg(unix)]
1042    TrackSetLv2ControlValue {
1043        track_name: String,
1044        instance_id: usize,
1045        index: u32,
1046        value: f32,
1047    },
1048    #[cfg(unix)]
1049    ClipSetLv2ControlValue {
1050        track_name: String,
1051        clip_idx: usize,
1052        instance_id: usize,
1053        index: u32,
1054        value: f32,
1055    },
1056    #[cfg(unix)]
1057    ClipLv2StateSnapshot {
1058        track_name: String,
1059        clip_idx: usize,
1060        instance_id: usize,
1061        state: Vec<u8>,
1062    },
1063    TrackGetPluginGraph {
1064        track_name: String,
1065        include_state: bool,
1066    },
1067    TrackPluginGraph {
1068        track_name: String,
1069        plugins: Vec<PluginGraphPlugin>,
1070        connections: Vec<PluginGraphConnection>,
1071        connectable_connections: Vec<ConnectableConnection>,
1072    },
1073    TrackConnectPluginAudio {
1074        track_name: String,
1075        from_node: PluginGraphNode,
1076        from_port: usize,
1077        to_node: PluginGraphNode,
1078        to_port: usize,
1079    },
1080    TrackConnectPluginMidi {
1081        track_name: String,
1082        from_node: PluginGraphNode,
1083        from_port: usize,
1084        to_node: PluginGraphNode,
1085        to_port: usize,
1086    },
1087    TrackDisconnectPluginAudio {
1088        track_name: String,
1089        from_node: PluginGraphNode,
1090        from_port: usize,
1091        to_node: PluginGraphNode,
1092        to_port: usize,
1093    },
1094    TrackDisconnectPluginMidi {
1095        track_name: String,
1096        from_node: PluginGraphNode,
1097        from_port: usize,
1098        to_node: PluginGraphNode,
1099        to_port: usize,
1100    },
1101    TrackConnectAudio {
1102        track_name: String,
1103        from: ConnectableRef,
1104        from_port: usize,
1105        to: ConnectableRef,
1106        to_port: usize,
1107    },
1108    TrackDisconnectAudio {
1109        track_name: String,
1110        from: ConnectableRef,
1111        from_port: usize,
1112        to: ConnectableRef,
1113        to_port: usize,
1114    },
1115    TrackConnectMidi {
1116        track_name: String,
1117        from: ConnectableRef,
1118        from_port: usize,
1119        to: ConnectableRef,
1120        to_port: usize,
1121    },
1122    TrackDisconnectMidi {
1123        track_name: String,
1124        from: ConnectableRef,
1125        from_port: usize,
1126        to: ConnectableRef,
1127        to_port: usize,
1128    },
1129    #[cfg(unix)]
1130    ListLv2Plugins,
1131    #[cfg(unix)]
1132    Lv2Plugins(Vec<Lv2PluginInfo>),
1133    #[cfg(unix)]
1134    Lv2PluginsUnavailable {
1135        error: String,
1136    },
1137    ListVst3Plugins,
1138    Vst3Plugins(Vec<Vst3PluginInfo>),
1139    Vst3PluginsUnavailable {
1140        error: String,
1141    },
1142    ListClapPlugins,
1143    ListClapPluginsWithCapabilities,
1144    ClapPlugins(Vec<ClapPluginInfo>),
1145    ClapPluginsUnavailable {
1146        error: String,
1147    },
1148    TrackSetClapParameter {
1149        track_name: String,
1150        instance_id: usize,
1151        param_id: u32,
1152        value: f64,
1153    },
1154    ClipSetClapParameter {
1155        track_name: String,
1156        clip_idx: usize,
1157        instance_id: usize,
1158        param_id: u32,
1159        value: f64,
1160    },
1161    ClipGetClapParameters {
1162        track_name: String,
1163        clip_idx: usize,
1164        instance_id: usize,
1165    },
1166    ClipClapParameters {
1167        track_name: String,
1168        clip_idx: usize,
1169        instance_id: usize,
1170        parameters: Vec<ClapParameterInfo>,
1171    },
1172    TrackSetClapParameterAt {
1173        track_name: String,
1174        instance_id: usize,
1175        param_id: u32,
1176        value: f64,
1177        frame: u32,
1178    },
1179    TrackBeginClapParameterEdit {
1180        track_name: String,
1181        instance_id: usize,
1182        param_id: u32,
1183        frame: u32,
1184    },
1185    TrackEndClapParameterEdit {
1186        track_name: String,
1187        instance_id: usize,
1188        param_id: u32,
1189        frame: u32,
1190    },
1191    TrackGetClapParameters {
1192        track_name: String,
1193        instance_id: usize,
1194    },
1195    TrackClapParameters {
1196        track_name: String,
1197        instance_id: usize,
1198        parameters: Vec<ClapParameterInfo>,
1199    },
1200    TrackClapSnapshotState {
1201        track_name: String,
1202        instance_id: usize,
1203    },
1204    ClipClapSnapshotState {
1205        track_name: String,
1206        clip_idx: usize,
1207        instance_id: usize,
1208    },
1209    TrackClapStateSnapshot {
1210        track_name: String,
1211        instance_id: usize,
1212        plugin_id: String,
1213        state: crate::clap::ClapPluginState,
1214    },
1215    ClipClapStateSnapshot {
1216        track_name: String,
1217        clip_idx: usize,
1218        instance_id: usize,
1219        plugin_id: String,
1220        state: crate::clap::ClapPluginState,
1221    },
1222    TrackClapStateDirty {
1223        track_name: String,
1224        instance_id: usize,
1225    },
1226    ClipClapStateDirty {
1227        track_name: String,
1228        clip_idx: usize,
1229        instance_id: usize,
1230    },
1231    TrackClapRestoreState {
1232        track_name: String,
1233        instance_id: usize,
1234        state: crate::clap::ClapPluginState,
1235    },
1236    ClipClapRestoreState {
1237        track_name: String,
1238        clip_idx: usize,
1239        instance_id: usize,
1240        state: crate::clap::ClapPluginState,
1241    },
1242    TrackSnapshotAllClapStates {
1243        track_name: String,
1244    },
1245    TrackSnapshotAllClapStatesDone {
1246        track_name: String,
1247    },
1248    TrackLoadClapPlugin {
1249        track_name: String,
1250        plugin_id: String,
1251        instance_id: Option<usize>,
1252    },
1253    TrackUnloadClapPlugin {
1254        track_name: String,
1255        plugin_id: String,
1256    },
1257    TrackUnloadClapPluginInstance {
1258        track_name: String,
1259        instance_id: usize,
1260    },
1261    TrackShowClapGui {
1262        track_name: String,
1263        instance_id: usize,
1264    },
1265    ClipShowClapGui {
1266        track_name: String,
1267        clip_idx: usize,
1268        instance_id: usize,
1269    },
1270    TrackLoadVst3Plugin {
1271        track_name: String,
1272        plugin_id: String,
1273        instance_id: Option<usize>,
1274    },
1275    TrackUnloadVst3Plugin {
1276        track_name: String,
1277        plugin_id: String,
1278    },
1279    TrackUnloadVst3PluginInstance {
1280        track_name: String,
1281        instance_id: usize,
1282    },
1283    TrackShowVst3Gui {
1284        track_name: String,
1285        instance_id: usize,
1286    },
1287    ClipShowVst3Gui {
1288        track_name: String,
1289        clip_idx: usize,
1290        instance_id: usize,
1291    },
1292    #[cfg(unix)]
1293    TrackLoadLv2Plugin {
1294        track_name: String,
1295        plugin_uri: String,
1296        instance_id: Option<usize>,
1297    },
1298    #[cfg(unix)]
1299    TrackUnloadLv2Plugin {
1300        track_name: String,
1301        plugin_uri: String,
1302    },
1303    #[cfg(unix)]
1304    TrackUnloadLv2PluginInstance {
1305        track_name: String,
1306        instance_id: usize,
1307    },
1308    TrackShowLv2Gui {
1309        track_name: String,
1310        instance_id: usize,
1311    },
1312    ClipShowLv2Gui {
1313        track_name: String,
1314        clip_idx: usize,
1315        instance_id: usize,
1316    },
1317    TrackSetPluginResourceDir {
1318        track_name: String,
1319        instance_id: usize,
1320        format: String,
1321        directory: String,
1322        shared: bool,
1323    },
1324    TrackClapCollectResources {
1325        track_name: String,
1326        instance_id: usize,
1327    },
1328    TrackClapResourceFiles {
1329        track_name: String,
1330        instance_id: usize,
1331        files: Vec<(u32, String)>,
1332    },
1333    ClipSetPluginResourceDir {
1334        track_name: String,
1335        clip_idx: usize,
1336        instance_id: usize,
1337        format: String,
1338        directory: String,
1339        shared: bool,
1340    },
1341    ClipClapCollectResources {
1342        track_name: String,
1343        clip_idx: usize,
1344        instance_id: usize,
1345    },
1346    ClipClapResourceFiles {
1347        track_name: String,
1348        clip_idx: usize,
1349        instance_id: usize,
1350        files: Vec<(u32, String)>,
1351    },
1352    TrackGetVst3Graph {
1353        track_name: String,
1354    },
1355    TrackVst3Graph {
1356        track_name: String,
1357        plugins: Vec<Vst3GraphPlugin>,
1358        connections: Vec<Vst3GraphConnection>,
1359    },
1360    TrackSetVst3Parameter {
1361        track_name: String,
1362        instance_id: usize,
1363        param_id: u32,
1364        value: f32,
1365    },
1366    ClipSetVst3Parameter {
1367        track_name: String,
1368        clip_idx: usize,
1369        instance_id: usize,
1370        param_id: u32,
1371        value: f32,
1372    },
1373    TrackSetPluginBypassed {
1374        track_name: String,
1375        instance_id: usize,
1376        format: String,
1377        bypassed: bool,
1378    },
1379    TrackGetVst3Parameters {
1380        track_name: String,
1381        instance_id: usize,
1382    },
1383    TrackVst3Parameters {
1384        track_name: String,
1385        instance_id: usize,
1386        parameters: Vec<crate::vst3::port::ParameterInfo>,
1387    },
1388    ClipGetVst3Parameters {
1389        track_name: String,
1390        clip_idx: usize,
1391        instance_id: usize,
1392    },
1393    ClipVst3Parameters {
1394        track_name: String,
1395        clip_idx: usize,
1396        instance_id: usize,
1397        parameters: Vec<crate::vst3::port::ParameterInfo>,
1398    },
1399    TrackVst3SnapshotState {
1400        track_name: String,
1401        instance_id: usize,
1402    },
1403    ClipVst3SnapshotState {
1404        track_name: String,
1405        clip_idx: usize,
1406        instance_id: usize,
1407    },
1408    TrackVst3StateSnapshot {
1409        track_name: String,
1410        instance_id: usize,
1411        state: crate::vst3::state::Vst3PluginState,
1412    },
1413    ClipVst3StateSnapshot {
1414        track_name: String,
1415        clip_idx: usize,
1416        instance_id: usize,
1417        state: crate::vst3::state::Vst3PluginState,
1418    },
1419    TrackVst3RestoreState {
1420        track_name: String,
1421        instance_id: usize,
1422        state: crate::vst3::state::Vst3PluginState,
1423    },
1424    ClipVst3RestoreState {
1425        track_name: String,
1426        clip_idx: usize,
1427        instance_id: usize,
1428        state: crate::vst3::state::Vst3PluginState,
1429    },
1430    TrackConnectVst3Audio {
1431        track_name: String,
1432        from_node: Vst3GraphNode,
1433        from_port: usize,
1434        to_node: Vst3GraphNode,
1435        to_port: usize,
1436    },
1437    TrackDisconnectVst3Audio {
1438        track_name: String,
1439        from_node: Vst3GraphNode,
1440        from_port: usize,
1441        to_node: Vst3GraphNode,
1442        to_port: usize,
1443    },
1444    ClipMove {
1445        kind: Kind,
1446        from: ClipMoveFrom,
1447        to: ClipMoveTo,
1448        copy: bool,
1449    },
1450    Connect {
1451        from_track: String,
1452        from_port: usize,
1453        to_track: String,
1454        to_port: usize,
1455        kind: Kind,
1456    },
1457    Disconnect {
1458        from_track: String,
1459        from_port: usize,
1460        to_track: String,
1461        to_port: usize,
1462        kind: Kind,
1463    },
1464    OpenAudioDevice {
1465        device: String,
1466        input_device: Option<String>,
1467        sample_rate_hz: i32,
1468        bits: i32,
1469        exclusive: bool,
1470        period_frames: usize,
1471        nperiods: usize,
1472        sync_mode: bool,
1473        actual_period_frames: usize,
1474        input_channels: usize,
1475        output_channels: usize,
1476        bytes_per_frame: usize,
1477    },
1478    JackAddAudioInputPort,
1479    JackRemoveAudioInputPort(usize),
1480    JackAddAudioOutputPort,
1481    JackRemoveAudioOutputPort(usize),
1482    JackGetGraph,
1483    JackGraph(JackGraphInfo),
1484    JackConnect {
1485        source: String,
1486        destination: String,
1487    },
1488    JackDisconnect {
1489        source: String,
1490        destination: String,
1491    },
1492    OpenMidiInputDevice(String),
1493    OpenMidiOutputDevice(String),
1494    RequestSessionDiagnostics,
1495    RequestMidiLearnMappingsReport,
1496    ClearAllMidiLearnBindings,
1497    SessionDiagnosticsReport {
1498        track_count: usize,
1499        frozen_track_count: usize,
1500        audio_clip_count: usize,
1501        midi_clip_count: usize,
1502        #[cfg(unix)]
1503        lv2_instance_count: usize,
1504        vst3_instance_count: usize,
1505        clap_instance_count: usize,
1506        pending_requests: usize,
1507        workers_total: usize,
1508        workers_ready: usize,
1509        pending_hw_midi_events: usize,
1510        playing: bool,
1511        transport_running: bool,
1512        transport_sample: usize,
1513        tempo_bpm: f64,
1514        sample_rate_hz: usize,
1515        cycle_samples: usize,
1516    },
1517    MidiLearnMappingsReport {
1518        lines: Vec<String>,
1519    },
1520    HWInfo {
1521        channels: usize,
1522        rate: usize,
1523        input: bool,
1524    },
1525    MarkHistorySavePoint,
1526    HistoryState {
1527        dirty: bool,
1528    },
1529    Undo,
1530    Redo,
1531    Session(SessionAction),
1532    SessionRuntimeReport {
1533        track_name: String,
1534        scene_index: usize,
1535        state: SessionSlotState,
1536        play_position_samples: usize,
1537        elapsed_samples: usize,
1538    },
1539    Panic,
1540}
1541
1542#[derive(Clone, Debug)]
1543pub enum Message {
1544    Ready(usize),
1545    TracksFinished,
1546
1547    /// Dispatch a render-plan node to a worker (Phase 2, see `LOCKLESS.md`).
1548    NodeJob(crate::executor::NodeJob),
1549    /// A worker completed a plan node. Stale `epoch`s are dropped.
1550    NodeDone {
1551        worker_id: usize,
1552        epoch: u64,
1553        node: u32,
1554        output_linear: Vec<f32>,
1555        parameter_updates: Vec<Action>,
1556        latency_changed: bool,
1557    },
1558    ProcessOfflineBounce(OfflineBounceWork),
1559    Channel(Sender<Self>),
1560
1561    Request(Action),
1562    OscRequest {
1563        action: Action,
1564        reply_to: SocketAddr,
1565    },
1566    Response(Result<Action, String>),
1567    HWMidiEvents(Vec<HwMidiEvent>),
1568    HWMidiOutEvents(Vec<HwMidiEvent>),
1569    ClearHWMidiOutEvents,
1570    StartAudioPreview {
1571        samples: Arc<Vec<f32>>,
1572        channels: usize,
1573        start_sample: usize,
1574    },
1575    StopAudioPreview,
1576    HWSetPlaying(bool),
1577    HWSetOutputGainBalance {
1578        gain: f32,
1579        balance: f32,
1580    },
1581    HWOpenMidiInputDevice(String),
1582    HWOpenMidiOutputDevice(String),
1583    HWCloseMidiDevices,
1584    HWFinished,
1585    OfflineBounceFinished {
1586        result: Result<Action, String>,
1587    },
1588}
1589
1590#[cfg(test)]
1591mod tests {
1592    use super::{AudioClipData, MidiClipData, PitchCorrectionPointData};
1593    use serde_json::json;
1594
1595    #[test]
1596    fn audio_clip_data_serde_round_trips_nested_groups() {
1597        let clip = AudioClipData {
1598            id: String::new(),
1599            name: "group.wav".to_string(),
1600            start: 12,
1601            length: 96,
1602            offset: 3,
1603            input_channel: 1,
1604            muted: true,
1605            reversed: false,
1606            gain_db: 0.0,
1607            peaks_file: Some("peaks/group.json".to_string()),
1608            fade_enabled: false,
1609            fade_in_samples: 10,
1610            fade_out_samples: 20,
1611            preview_name: Some("preview.wav".to_string()),
1612            source_name: Some("source.wav".to_string()),
1613            source_offset: Some(4),
1614            source_length: Some(88),
1615            pitch_correction_points: vec![PitchCorrectionPointData {
1616                start_sample: 7,
1617                length_samples: 11,
1618                detected_midi_pitch: 60.1,
1619                target_midi_pitch: 61.2,
1620                clarity: 0.8,
1621            }],
1622            pitch_correction_frame_likeness: Some(0.5),
1623            pitch_correction_inertia_ms: Some(123),
1624            pitch_correction_formant_compensation: Some(false),
1625            plugin_graph_json: Some(json!({"plugins":[],"connections":[{"kind":"Audio"}]})),
1626            grouped_clips: vec![AudioClipData {
1627                name: "child.wav".to_string(),
1628                start: 0,
1629                length: 48,
1630                ..AudioClipData::default()
1631            }],
1632        };
1633
1634        let value = serde_json::to_value(&clip).expect("serialize");
1635        let restored: AudioClipData = serde_json::from_value(value).expect("deserialize");
1636
1637        assert_eq!(restored.name, clip.name);
1638        assert_eq!(restored.preview_name, clip.preview_name);
1639        assert_eq!(restored.source_name, clip.source_name);
1640        assert_eq!(restored.plugin_graph_json, clip.plugin_graph_json);
1641        assert_eq!(restored.grouped_clips.len(), 1);
1642        assert_eq!(restored.grouped_clips[0].name, "child.wav");
1643        assert_eq!(restored.pitch_correction_points[0].target_midi_pitch, 61.2);
1644    }
1645
1646    #[test]
1647    fn midi_clip_data_serde_round_trips_nested_groups() {
1648        let clip = MidiClipData {
1649            id: String::new(),
1650            name: "group.mid".to_string(),
1651            start: 5,
1652            length: 64,
1653            offset: 2,
1654            input_channel: 3,
1655            muted: true,
1656            reversed: false,
1657            grouped_clips: vec![MidiClipData {
1658                name: "child.mid".to_string(),
1659                start: 0,
1660                length: 32,
1661                ..MidiClipData::default()
1662            }],
1663        };
1664
1665        let value = serde_json::to_value(&clip).expect("serialize");
1666        let restored: MidiClipData = serde_json::from_value(value).expect("deserialize");
1667
1668        assert_eq!(restored.name, clip.name);
1669        assert_eq!(restored.grouped_clips.len(), 1);
1670        assert_eq!(restored.grouped_clips[0].name, "child.mid");
1671    }
1672
1673    #[test]
1674    fn pitch_correction_point_data_serde_round_trips() {
1675        let point = PitchCorrectionPointData {
1676            start_sample: 10,
1677            length_samples: 20,
1678            detected_midi_pitch: 57.5,
1679            target_midi_pitch: 58.0,
1680            clarity: 0.9,
1681        };
1682
1683        let value = serde_json::to_value(&point).expect("serialize");
1684        let restored: PitchCorrectionPointData =
1685            serde_json::from_value(value).expect("deserialize");
1686
1687        assert_eq!(restored.start_sample, 10);
1688        assert_eq!(restored.length_samples, 20);
1689        assert_eq!(restored.detected_midi_pitch, 57.5);
1690        assert_eq!(restored.target_midi_pitch, 58.0);
1691        assert_eq!(restored.clarity, 0.9);
1692    }
1693
1694    #[test]
1695    fn audio_clip_data_deserializes_with_omitted_optional_fields() {
1696        let restored: AudioClipData = serde_json::from_value(json!({
1697            "name": "clip.wav",
1698            "start": 1,
1699            "length": 2,
1700            "offset": 3,
1701            "input_channel": 0,
1702            "muted": false,
1703            "fade_enabled": true,
1704            "fade_in_samples": 240,
1705            "fade_out_samples": 240,
1706            "pitch_correction_points": [],
1707            "grouped_clips": []
1708        }))
1709        .expect("deserialize");
1710
1711        assert_eq!(restored.name, "clip.wav");
1712        assert!(restored.peaks_file.is_none());
1713        assert!(restored.preview_name.is_none());
1714        assert!(restored.source_name.is_none());
1715        assert!(restored.source_offset.is_none());
1716        assert!(restored.source_length.is_none());
1717        assert!(restored.pitch_correction_points.is_empty());
1718        assert!(restored.plugin_graph_json.is_none());
1719    }
1720}