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