use crate::clap::{ClapParameterInfo, ClapPluginInfo};
#[cfg(all(unix, not(target_os = "macos")))]
use crate::lv2::Lv2PluginInfo;
use crate::midi::io::MidiEvent;
use crate::vst3::Vst3PluginInfo;
use crate::{kind::Kind, mutex::UnsafeMutex, track::Track};
use std::sync::{Arc, atomic::AtomicBool};
use tokio::sync::mpsc::Sender;
#[derive(Clone, Debug)]
pub struct MidiNoteData {
pub start_sample: usize,
pub length_samples: usize,
pub pitch: u8,
pub velocity: u8,
pub channel: u8,
}
#[derive(Clone, Debug)]
pub struct MidiControllerData {
pub sample: usize,
pub controller: u8,
pub value: u8,
pub channel: u8,
}
#[derive(Debug, Clone)]
pub struct MidiRawEventData {
pub sample: usize,
pub data: Vec<u8>,
}
#[derive(Clone, Debug)]
pub struct HwMidiEvent {
pub device: String,
pub event: MidiEvent,
}
#[derive(Clone, Debug)]
pub struct OfflineAutomationPoint {
pub sample: usize,
pub value: f32,
}
#[derive(Clone, Debug)]
pub enum OfflineAutomationTarget {
Volume,
Balance,
Mute,
#[cfg(all(unix, not(target_os = "macos")))]
Lv2Parameter {
instance_id: usize,
index: u32,
min: f32,
max: f32,
},
Vst3Parameter {
instance_id: usize,
param_id: u32,
},
ClapParameter {
instance_id: usize,
param_id: u32,
min: f64,
max: f64,
},
}
#[derive(Clone, Debug)]
pub struct OfflineAutomationLane {
pub target: OfflineAutomationTarget,
pub points: Vec<OfflineAutomationPoint>,
}
#[derive(Clone, Debug)]
pub struct OfflineBounceWork {
pub state: Arc<UnsafeMutex<crate::state::State>>,
pub track_name: String,
pub output_path: String,
pub start_sample: usize,
pub length_samples: usize,
pub tempo_bpm: f64,
pub tsig_num: u16,
pub tsig_denom: u16,
pub automation_lanes: Vec<OfflineAutomationLane>,
pub cancel: Arc<AtomicBool>,
}
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct PitchCorrectionPointData {
pub start_sample: usize,
pub length_samples: usize,
pub detected_midi_pitch: f32,
pub target_midi_pitch: f32,
pub clarity: f32,
}
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct AudioClipData {
pub name: String,
pub start: usize,
pub length: usize,
pub offset: usize,
pub input_channel: usize,
pub muted: bool,
pub peaks_file: Option<String>,
pub fade_enabled: bool,
pub fade_in_samples: usize,
pub fade_out_samples: usize,
pub preview_name: Option<String>,
pub source_name: Option<String>,
pub source_offset: Option<usize>,
pub source_length: Option<usize>,
pub pitch_correction_points: Vec<PitchCorrectionPointData>,
pub pitch_correction_frame_likeness: Option<f32>,
pub pitch_correction_inertia_ms: Option<u16>,
pub pitch_correction_formant_compensation: Option<bool>,
pub plugin_graph_json: Option<serde_json::Value>,
pub grouped_clips: Vec<AudioClipData>,
}
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct MidiClipData {
pub name: String,
pub start: usize,
pub length: usize,
pub offset: usize,
pub input_channel: usize,
pub muted: bool,
pub grouped_clips: Vec<MidiClipData>,
}
#[derive(Clone, Debug)]
pub struct ClipMoveFrom {
pub track_name: String,
pub clip_index: usize,
}
#[derive(Clone, Debug)]
pub struct ClipMoveTo {
pub track_name: String,
pub sample_offset: usize,
pub input_channel: usize,
}
#[cfg(unix)]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum PluginGraphNode {
TrackInput,
TrackOutput,
Lv2PluginInstance(usize),
Vst3PluginInstance(usize),
ClapPluginInstance(usize),
}
#[cfg(unix)]
#[derive(Clone, Debug, PartialEq)]
pub struct PluginGraphPlugin {
pub node: PluginGraphNode,
pub instance_id: usize,
pub format: String,
pub uri: String,
pub plugin_id: String,
pub name: String,
pub main_audio_inputs: usize,
pub main_audio_outputs: usize,
pub audio_inputs: usize,
pub audio_outputs: usize,
pub midi_inputs: usize,
pub midi_outputs: usize,
pub state: Option<serde_json::Value>,
}
#[cfg(unix)]
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Lv2StatePortValue {
pub index: u32,
pub value: f32,
}
#[cfg(unix)]
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Lv2StateProperty {
pub key_uri: String,
pub type_uri: String,
pub flags: u32,
pub value: Vec<u8>,
}
#[cfg(unix)]
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Lv2PluginState {
pub port_values: Vec<Lv2StatePortValue>,
pub properties: Vec<Lv2StateProperty>,
}
#[cfg(all(unix, not(target_os = "macos")))]
#[derive(Clone, Debug, PartialEq)]
pub struct Lv2ControlPortInfo {
pub index: u32,
pub name: String,
pub min: f32,
pub max: f32,
pub value: f32,
}
#[cfg(unix)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PluginGraphConnection {
pub from_node: PluginGraphNode,
pub from_port: usize,
pub to_node: PluginGraphNode,
pub to_port: usize,
pub kind: Kind,
}
#[cfg(unix)]
pub type PluginGraphSnapshot = (Vec<PluginGraphPlugin>, Vec<PluginGraphConnection>);
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Vst3GraphNode {
TrackInput,
TrackOutput,
PluginInstance(usize),
}
#[derive(Clone, Debug)]
pub struct Vst3GraphPlugin {
pub instance_id: usize,
pub name: String,
pub path: String,
pub audio_inputs: usize,
pub audio_outputs: usize,
pub parameters: Vec<crate::vst3::port::ParameterInfo>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Vst3GraphConnection {
pub from_node: Vst3GraphNode,
pub from_port: usize,
pub to_node: Vst3GraphNode,
pub to_port: usize,
pub kind: Kind,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct MidiLearnBinding {
pub device: Option<String>,
pub channel: u8,
pub cc: u8,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum TrackMidiLearnTarget {
Volume,
Balance,
Mute,
Solo,
Arm,
InputMonitor,
DiskMonitor,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum GlobalMidiLearnTarget {
PlayPause,
Stop,
RecordToggle,
}
#[derive(Clone, Debug)]
pub enum Action {
Quit,
Play,
Pause,
Stop,
TransportPosition(usize),
JumpToEnd,
SetLoopEnabled(bool),
SetLoopRange(Option<(usize, usize)>),
SetPunchEnabled(bool),
SetPunchRange(Option<(usize, usize)>),
SetMetronomeEnabled(bool),
SetTempo(f64),
SetTimeSignature {
numerator: u16,
denominator: u16,
},
SetOscEnabled(bool),
SetClipPlaybackEnabled(bool),
SetRecordEnabled(bool),
SetSessionPath(String),
BeginHistoryGroup,
EndHistoryGroup,
ClearHistory,
BeginSessionRestore,
EndSessionRestore,
AddTrack {
name: String,
audio_ins: usize,
midi_ins: usize,
audio_outs: usize,
midi_outs: usize,
},
TrackAddAudioInput(String),
TrackAddAudioOutput(String),
TrackRemoveAudioInput(String),
TrackRemoveAudioOutput(String),
AddClip {
name: String,
track_name: String,
start: usize,
length: usize,
offset: usize,
input_channel: usize,
muted: bool,
peaks_file: Option<String>,
kind: Kind,
fade_enabled: bool,
fade_in_samples: usize,
fade_out_samples: usize,
source_name: Option<String>,
source_offset: Option<usize>,
source_length: Option<usize>,
preview_name: Option<String>,
pitch_correction_points: Vec<PitchCorrectionPointData>,
pitch_correction_frame_likeness: Option<f32>,
pitch_correction_inertia_ms: Option<u16>,
pitch_correction_formant_compensation: Option<bool>,
plugin_graph_json: Option<serde_json::Value>,
},
AddGroupedClip {
track_name: String,
kind: Kind,
audio_clip: Option<AudioClipData>,
midi_clip: Option<MidiClipData>,
},
RemoveClip {
track_name: String,
kind: Kind,
clip_indices: Vec<usize>,
},
SetClipFade {
track_name: String,
clip_index: usize,
kind: Kind,
fade_enabled: bool,
fade_in_samples: usize,
fade_out_samples: usize,
},
SetClipBounds {
track_name: String,
clip_index: usize,
kind: Kind,
start: usize,
length: usize,
offset: usize,
},
SetClipMuted {
track_name: String,
clip_index: usize,
kind: Kind,
muted: bool,
},
SetClipPluginGraphJson {
track_name: String,
clip_index: usize,
plugin_graph_json: Option<serde_json::Value>,
},
SetClipPitchCorrection {
track_name: String,
clip_index: usize,
preview_name: Option<String>,
source_name: Option<String>,
source_offset: Option<usize>,
source_length: Option<usize>,
pitch_correction_points: Vec<PitchCorrectionPointData>,
pitch_correction_frame_likeness: Option<f32>,
pitch_correction_inertia_ms: Option<u16>,
pitch_correction_formant_compensation: Option<bool>,
},
RenameClip {
track_name: String,
kind: Kind,
clip_index: usize,
new_name: String,
},
RenameTrack {
old_name: String,
new_name: String,
},
RemoveTrack(String),
TrackLevel(String, f32),
TrackBalance(String, f32),
TrackAutomationLevel(String, f32),
TrackAutomationBalance(String, f32),
TrackAutomationMute(String, bool),
TrackMeters {
track_name: String,
output_db: Vec<f32>,
},
RequestMeterSnapshot,
MeterSnapshot {
hw_out_db: Arc<Vec<f32>>,
track_meters: Arc<Vec<(String, Vec<f32>)>>,
},
TrackToggleArm(String),
TrackToggleMute(String),
TrackTogglePhase(String),
TrackToggleSolo(String),
TrackToggleInputMonitor(String),
TrackToggleDiskMonitor(String),
TrackArmMidiLearn {
track_name: String,
target: TrackMidiLearnTarget,
},
GlobalArmMidiLearn {
target: GlobalMidiLearnTarget,
},
TrackSetMidiLearnBinding {
track_name: String,
target: TrackMidiLearnTarget,
binding: Option<MidiLearnBinding>,
},
SetGlobalMidiLearnBinding {
target: GlobalMidiLearnTarget,
binding: Option<MidiLearnBinding>,
},
TrackSetVcaMaster {
track_name: String,
master_track: Option<String>,
},
TrackSetMidiLaneChannel {
track_name: String,
lane: usize,
channel: Option<u8>,
},
TrackSetFrozen {
track_name: String,
frozen: bool,
},
TrackOfflineBounce {
track_name: String,
output_path: String,
start_sample: usize,
length_samples: usize,
automation_lanes: Vec<OfflineAutomationLane>,
},
TrackOfflineBounceCancel {
track_name: String,
},
TrackOfflineBounceCanceled {
track_name: String,
},
TrackOfflineBounceProgress {
track_name: String,
progress: f32,
operation: Option<String>,
},
PianoKey {
track_name: String,
note: u8,
velocity: u8,
on: bool,
},
ModifyMidiNotes {
track_name: String,
clip_index: usize,
note_indices: Vec<usize>,
new_notes: Vec<MidiNoteData>,
old_notes: Vec<MidiNoteData>,
},
ModifyMidiControllers {
track_name: String,
clip_index: usize,
controller_indices: Vec<usize>,
new_controllers: Vec<MidiControllerData>,
old_controllers: Vec<MidiControllerData>,
},
DeleteMidiControllers {
track_name: String,
clip_index: usize,
controller_indices: Vec<usize>,
deleted_controllers: Vec<(usize, MidiControllerData)>,
},
InsertMidiControllers {
track_name: String,
clip_index: usize,
controllers: Vec<(usize, MidiControllerData)>,
},
DeleteMidiNotes {
track_name: String,
clip_index: usize,
note_indices: Vec<usize>,
deleted_notes: Vec<(usize, MidiNoteData)>,
},
InsertMidiNotes {
track_name: String,
clip_index: usize,
notes: Vec<(usize, MidiNoteData)>,
},
SetMidiSysExEvents {
track_name: String,
clip_index: usize,
new_sysex_events: Vec<MidiRawEventData>,
old_sysex_events: Vec<MidiRawEventData>,
},
#[cfg(all(unix, not(target_os = "macos")))]
TrackLoadLv2Plugin {
track_name: String,
plugin_uri: String,
},
TrackClearDefaultPassthrough {
track_name: String,
},
#[cfg(all(unix, not(target_os = "macos")))]
TrackSetLv2PluginState {
track_name: String,
instance_id: usize,
state: Lv2PluginState,
},
#[cfg(all(unix, not(target_os = "macos")))]
ClipSetLv2PluginState {
track_name: String,
clip_idx: usize,
instance_id: usize,
state: Lv2PluginState,
},
#[cfg(all(unix, not(target_os = "macos")))]
TrackUnloadLv2PluginInstance {
track_name: String,
instance_id: usize,
},
#[cfg(all(unix, not(target_os = "macos")))]
TrackGetLv2PluginControls {
track_name: String,
instance_id: usize,
},
#[cfg(all(unix, not(target_os = "macos")))]
ClipGetLv2PluginControls {
track_name: String,
clip_idx: usize,
instance_id: usize,
},
#[cfg(all(unix, not(target_os = "macos")))]
TrackLv2PluginControls {
track_name: String,
instance_id: usize,
controls: Vec<Lv2ControlPortInfo>,
instance_access_handle: Option<usize>,
},
#[cfg(all(unix, not(target_os = "macos")))]
ClipLv2PluginControls {
track_name: String,
clip_idx: usize,
instance_id: usize,
controls: Vec<Lv2ControlPortInfo>,
instance_access_handle: Option<usize>,
},
#[cfg(all(unix, not(target_os = "macos")))]
TrackGetLv2Midnam {
track_name: String,
},
#[cfg(all(unix, not(target_os = "macos")))]
TrackLv2Midnam {
track_name: String,
note_names: std::collections::HashMap<u8, String>,
},
TrackGetClapNoteNames {
track_name: String,
},
TrackClapNoteNames {
track_name: String,
note_names: std::collections::HashMap<u8, String>,
},
#[cfg(all(unix, not(target_os = "macos")))]
TrackSetLv2ControlValue {
track_name: String,
instance_id: usize,
index: u32,
value: f32,
},
#[cfg(all(unix, not(target_os = "macos")))]
ClipSetLv2ControlValue {
track_name: String,
clip_idx: usize,
instance_id: usize,
index: u32,
value: f32,
},
#[cfg(all(unix, not(target_os = "macos")))]
ClipLv2StateSnapshot {
track_name: String,
clip_idx: usize,
instance_id: usize,
state: Lv2PluginState,
},
#[cfg(unix)]
TrackGetPluginGraph {
track_name: String,
},
#[cfg(unix)]
TrackPluginGraph {
track_name: String,
plugins: Vec<PluginGraphPlugin>,
connections: Vec<PluginGraphConnection>,
},
#[cfg(unix)]
TrackConnectPluginAudio {
track_name: String,
from_node: PluginGraphNode,
from_port: usize,
to_node: PluginGraphNode,
to_port: usize,
},
#[cfg(unix)]
TrackConnectPluginMidi {
track_name: String,
from_node: PluginGraphNode,
from_port: usize,
to_node: PluginGraphNode,
to_port: usize,
},
#[cfg(unix)]
TrackDisconnectPluginAudio {
track_name: String,
from_node: PluginGraphNode,
from_port: usize,
to_node: PluginGraphNode,
to_port: usize,
},
#[cfg(unix)]
TrackDisconnectPluginMidi {
track_name: String,
from_node: PluginGraphNode,
from_port: usize,
to_node: PluginGraphNode,
to_port: usize,
},
#[cfg(all(unix, not(target_os = "macos")))]
ListLv2Plugins,
#[cfg(all(unix, not(target_os = "macos")))]
Lv2Plugins(Vec<Lv2PluginInfo>),
ListVst3Plugins,
Vst3Plugins(Vec<Vst3PluginInfo>),
ListClapPlugins,
ListClapPluginsWithCapabilities,
ClapPlugins(Vec<ClapPluginInfo>),
TrackLoadClapPlugin {
track_name: String,
plugin_path: String,
},
TrackUnloadClapPlugin {
track_name: String,
plugin_path: String,
},
TrackSetClapParameter {
track_name: String,
instance_id: usize,
param_id: u32,
value: f64,
},
ClipSetClapParameter {
track_name: String,
clip_idx: usize,
instance_id: usize,
param_id: u32,
value: f64,
},
TrackSetClapParameterAt {
track_name: String,
instance_id: usize,
param_id: u32,
value: f64,
frame: u32,
},
TrackBeginClapParameterEdit {
track_name: String,
instance_id: usize,
param_id: u32,
frame: u32,
},
TrackEndClapParameterEdit {
track_name: String,
instance_id: usize,
param_id: u32,
frame: u32,
},
TrackGetClapParameters {
track_name: String,
instance_id: usize,
},
TrackClapParameters {
track_name: String,
instance_id: usize,
parameters: Vec<ClapParameterInfo>,
},
TrackClapSnapshotState {
track_name: String,
instance_id: usize,
},
ClipClapSnapshotState {
track_name: String,
clip_idx: usize,
instance_id: usize,
},
TrackGetClapProcessor {
track_name: String,
instance_id: usize,
},
ClipGetClapProcessor {
track_name: String,
clip_idx: usize,
instance_id: usize,
},
TrackClapProcessor {
track_name: String,
instance_id: usize,
processor: Arc<crate::clap::ClapProcessor>,
},
ClipClapProcessor {
track_name: String,
clip_idx: usize,
instance_id: usize,
processor: Arc<crate::clap::ClapProcessor>,
},
TrackClapStateSnapshot {
track_name: String,
instance_id: usize,
plugin_path: String,
state: crate::clap::ClapPluginState,
},
ClipClapStateSnapshot {
track_name: String,
clip_idx: usize,
instance_id: usize,
plugin_path: String,
state: crate::clap::ClapPluginState,
},
TrackClapRestoreState {
track_name: String,
instance_id: usize,
state: crate::clap::ClapPluginState,
},
ClipClapRestoreState {
track_name: String,
clip_idx: usize,
instance_id: usize,
state: crate::clap::ClapPluginState,
},
TrackSnapshotAllClapStates {
track_name: String,
},
TrackSnapshotAllClapStatesDone {
track_name: String,
},
TrackLoadVst3Plugin {
track_name: String,
plugin_path: String,
},
TrackUnloadVst3PluginInstance {
track_name: String,
instance_id: usize,
},
TrackGetVst3Graph {
track_name: String,
},
TrackVst3Graph {
track_name: String,
plugins: Vec<Vst3GraphPlugin>,
connections: Vec<Vst3GraphConnection>,
},
TrackSetVst3Parameter {
track_name: String,
instance_id: usize,
param_id: u32,
value: f32,
},
TrackGetVst3Parameters {
track_name: String,
instance_id: usize,
},
TrackVst3Parameters {
track_name: String,
instance_id: usize,
parameters: Vec<crate::vst3::port::ParameterInfo>,
},
TrackGetVst3Processor {
track_name: String,
instance_id: usize,
},
ClipGetVst3Processor {
track_name: String,
clip_idx: usize,
instance_id: usize,
},
TrackVst3Processor {
track_name: String,
instance_id: usize,
processor: Arc<crate::vst3::Vst3Processor>,
},
ClipVst3Processor {
track_name: String,
clip_idx: usize,
instance_id: usize,
processor: Arc<crate::vst3::Vst3Processor>,
},
TrackVst3SnapshotState {
track_name: String,
instance_id: usize,
},
ClipVst3SnapshotState {
track_name: String,
clip_idx: usize,
instance_id: usize,
},
TrackVst3StateSnapshot {
track_name: String,
instance_id: usize,
state: crate::vst3::state::Vst3PluginState,
},
ClipVst3StateSnapshot {
track_name: String,
clip_idx: usize,
instance_id: usize,
state: crate::vst3::state::Vst3PluginState,
},
TrackVst3RestoreState {
track_name: String,
instance_id: usize,
state: crate::vst3::state::Vst3PluginState,
},
TrackConnectVst3Audio {
track_name: String,
from_node: Vst3GraphNode,
from_port: usize,
to_node: Vst3GraphNode,
to_port: usize,
},
TrackDisconnectVst3Audio {
track_name: String,
from_node: Vst3GraphNode,
from_port: usize,
to_node: Vst3GraphNode,
to_port: usize,
},
ClipMove {
kind: Kind,
from: ClipMoveFrom,
to: ClipMoveTo,
copy: bool,
},
Connect {
from_track: String,
from_port: usize,
to_track: String,
to_port: usize,
kind: Kind,
},
Disconnect {
from_track: String,
from_port: usize,
to_track: String,
to_port: usize,
kind: Kind,
},
OpenAudioDevice {
device: String,
input_device: Option<String>,
sample_rate_hz: i32,
bits: i32,
exclusive: bool,
period_frames: usize,
nperiods: usize,
sync_mode: bool,
},
JackAddAudioInputPort,
JackRemoveAudioInputPort(usize),
JackAddAudioOutputPort,
JackRemoveAudioOutputPort(usize),
OpenMidiInputDevice(String),
OpenMidiOutputDevice(String),
RequestSessionDiagnostics,
RequestMidiLearnMappingsReport,
ClearAllMidiLearnBindings,
SessionDiagnosticsReport {
track_count: usize,
frozen_track_count: usize,
audio_clip_count: usize,
midi_clip_count: usize,
#[cfg(all(unix, not(target_os = "macos")))]
lv2_instance_count: usize,
vst3_instance_count: usize,
clap_instance_count: usize,
pending_requests: usize,
workers_total: usize,
workers_ready: usize,
pending_hw_midi_events: usize,
playing: bool,
transport_sample: usize,
tempo_bpm: f64,
sample_rate_hz: usize,
cycle_samples: usize,
},
MidiLearnMappingsReport {
lines: Vec<String>,
},
HWInfo {
channels: usize,
rate: usize,
input: bool,
},
Undo,
Redo,
Panic,
}
#[derive(Clone, Debug)]
pub enum Message {
Ready(usize),
Finished {
worker_id: usize,
track_name: String,
output_linear: Vec<f32>,
process_epoch: usize,
},
TracksFinished,
ProcessTrack(Arc<UnsafeMutex<Box<Track>>>),
ProcessOfflineBounce(OfflineBounceWork),
Channel(Sender<Self>),
Request(Action),
Response(Result<Action, String>),
HWMidiEvents(Vec<HwMidiEvent>),
HWMidiOutEvents(Vec<HwMidiEvent>),
ClearHWMidiOutEvents,
HWFinished,
OfflineBounceFinished {
result: Result<Action, String>,
},
}
#[cfg(test)]
mod tests {
use super::{AudioClipData, MidiClipData, PitchCorrectionPointData};
use serde_json::json;
#[test]
fn audio_clip_data_serde_round_trips_nested_groups() {
let clip = AudioClipData {
name: "group.wav".to_string(),
start: 12,
length: 96,
offset: 3,
input_channel: 1,
muted: true,
peaks_file: Some("peaks/group.json".to_string()),
fade_enabled: false,
fade_in_samples: 10,
fade_out_samples: 20,
preview_name: Some("preview.wav".to_string()),
source_name: Some("source.wav".to_string()),
source_offset: Some(4),
source_length: Some(88),
pitch_correction_points: vec![PitchCorrectionPointData {
start_sample: 7,
length_samples: 11,
detected_midi_pitch: 60.1,
target_midi_pitch: 61.2,
clarity: 0.8,
}],
pitch_correction_frame_likeness: Some(0.5),
pitch_correction_inertia_ms: Some(123),
pitch_correction_formant_compensation: Some(false),
plugin_graph_json: Some(json!({"plugins":[],"connections":[{"kind":"Audio"}]})),
grouped_clips: vec![AudioClipData {
name: "child.wav".to_string(),
start: 0,
length: 48,
..AudioClipData::default()
}],
};
let value = serde_json::to_value(&clip).expect("serialize");
let restored: AudioClipData = serde_json::from_value(value).expect("deserialize");
assert_eq!(restored.name, clip.name);
assert_eq!(restored.preview_name, clip.preview_name);
assert_eq!(restored.source_name, clip.source_name);
assert_eq!(restored.plugin_graph_json, clip.plugin_graph_json);
assert_eq!(restored.grouped_clips.len(), 1);
assert_eq!(restored.grouped_clips[0].name, "child.wav");
assert_eq!(restored.pitch_correction_points[0].target_midi_pitch, 61.2);
}
#[test]
fn midi_clip_data_serde_round_trips_nested_groups() {
let clip = MidiClipData {
name: "group.mid".to_string(),
start: 5,
length: 64,
offset: 2,
input_channel: 3,
muted: true,
grouped_clips: vec![MidiClipData {
name: "child.mid".to_string(),
start: 0,
length: 32,
..MidiClipData::default()
}],
};
let value = serde_json::to_value(&clip).expect("serialize");
let restored: MidiClipData = serde_json::from_value(value).expect("deserialize");
assert_eq!(restored.name, clip.name);
assert_eq!(restored.grouped_clips.len(), 1);
assert_eq!(restored.grouped_clips[0].name, "child.mid");
}
#[test]
fn pitch_correction_point_data_serde_round_trips() {
let point = PitchCorrectionPointData {
start_sample: 10,
length_samples: 20,
detected_midi_pitch: 57.5,
target_midi_pitch: 58.0,
clarity: 0.9,
};
let value = serde_json::to_value(&point).expect("serialize");
let restored: PitchCorrectionPointData =
serde_json::from_value(value).expect("deserialize");
assert_eq!(restored.start_sample, 10);
assert_eq!(restored.length_samples, 20);
assert_eq!(restored.detected_midi_pitch, 57.5);
assert_eq!(restored.target_midi_pitch, 58.0);
assert_eq!(restored.clarity, 0.9);
}
#[test]
fn audio_clip_data_deserializes_with_omitted_optional_fields() {
let restored: AudioClipData = serde_json::from_value(json!({
"name": "clip.wav",
"start": 1,
"length": 2,
"offset": 3,
"input_channel": 0,
"muted": false,
"fade_enabled": true,
"fade_in_samples": 240,
"fade_out_samples": 240,
"pitch_correction_points": [],
"grouped_clips": []
}))
.expect("deserialize");
assert_eq!(restored.name, "clip.wav");
assert!(restored.peaks_file.is_none());
assert!(restored.preview_name.is_none());
assert!(restored.source_name.is_none());
assert!(restored.source_offset.is_none());
assert!(restored.source_length.is_none());
assert!(restored.pitch_correction_points.is_empty());
assert!(restored.plugin_graph_json.is_none());
}
}