Skip to main content

maolan_engine/
message.rs

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