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