1use midly::{
2 Arena, Format, Header, MetaMessage, Smf, Timing, TrackEvent, TrackEventKind,
3 live::LiveEvent,
4 num::{u15, u24, u28},
5};
6#[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
7use std::fs::read_dir;
8use std::{
9 collections::{HashMap, VecDeque},
10 fs::File,
11 net::{SocketAddr, UdpSocket},
12 path::{Path, PathBuf},
13 sync::{
14 Arc,
15 atomic::{AtomicBool, Ordering},
16 },
17 time::{Duration, Instant, SystemTime, UNIX_EPOCH},
18};
19use tokio::sync::mpsc::{Receiver, Sender, channel};
20use tokio::task::JoinHandle;
21use tracing::error;
22
23type HwDeviceInfo = (usize, usize, usize, ((usize, usize), (usize, usize)));
24
25pub fn parse_automation_lanes(
26 value: &serde_json::Value,
27) -> Vec<crate::message::OfflineAutomationLane> {
28 serde_json::from_value(value.clone()).unwrap_or_else(|_| {
29 if let Some(array) = value.as_array() {
30 array
31 .iter()
32 .filter_map(|item| serde_json::from_value(item.clone()).ok())
33 .collect()
34 } else {
35 vec![]
36 }
37 })
38}
39
40#[cfg(target_os = "linux")]
41use crate::hw::alsa::{HwDriver, HwOptions, MidiHub};
42#[cfg(target_os = "macos")]
43use crate::hw::coreaudio::{HwDriver, HwOptions, MidiHub};
44#[cfg(unix)]
45use crate::hw::jack::JackRuntime;
46#[cfg(target_os = "windows")]
47use crate::hw::options::HwOptions;
48#[cfg(target_os = "freebsd")]
49use crate::hw::oss as hw;
50#[cfg(target_os = "freebsd")]
51use crate::hw::oss::{HwDriver, HwOptions, MidiHub};
52#[cfg(target_os = "openbsd")]
53use crate::hw::sndio::{HwDriver, HwOptions, MidiHub};
54#[cfg(target_os = "windows")]
55use crate::hw::wasapi::{self, HwDriver, MidiHub};
56#[cfg(target_os = "linux")]
57use crate::workers::alsa_worker::HwWorker;
58#[cfg(target_os = "macos")]
59use crate::workers::coreaudio_worker::HwWorker;
60#[cfg(target_os = "freebsd")]
61use crate::workers::oss_worker::HwWorker;
62#[cfg(target_os = "openbsd")]
63use crate::workers::sndio_worker::HwWorker;
64#[cfg(target_os = "windows")]
65use crate::workers::wasapi_worker::HwWorker;
66use crate::{
67 audio::clip::AudioClip,
68 audio::io::AudioIO,
69 history::{History, UndoEntry, create_inverse_actions, should_record},
70 hw::{
71 config,
72 traits::{HwDevice, HwWorkerDriver},
73 },
74 kind::Kind,
75 message::{
76 Action, HwMidiEvent, LaunchQuantization, Message, MidiControllerData, MidiNoteData,
77 OfflineAutomationLane, OfflineAutomationPoint, PluginKind, ProcessTask, SessionAction,
78 SessionSlotState,
79 },
80 midi::clip::MIDIClip,
81 midi::io::{MIDIIO, MidiEvent},
82 mutex::UnsafeMutex,
83 osc::{OscArg, OscServer, build_error_packet, build_osc_packet},
84 routing,
85 state::State,
86 track::{SessionSlot, Track},
87 workers::worker::Worker,
88};
89
90#[derive(Debug)]
91struct WorkerData {
92 tx: Sender<Message>,
93 handle: JoinHandle<()>,
94}
95
96impl WorkerData {
97 pub fn new(tx: Sender<Message>, handle: JoinHandle<()>) -> Self {
98 Self { tx, handle }
99 }
100}
101
102#[derive(Debug, Clone)]
103struct RecordingSession {
104 start_sample: usize,
105 samples: Vec<f32>,
106 channels: usize,
107 file_name: String,
108
109 stripe_peaks: Vec<Vec<[f32; 2]>>,
110
111 current_stripe_frames: usize,
112}
113
114const RECORDING_STRIPE_FRAMES: usize = 256;
115
116#[derive(Debug, Clone)]
117struct MidiRecordingSession {
118 start_sample: usize,
119 events: Vec<(u64, Vec<u8>)>,
120 file_name: String,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Hash)]
124struct MidiHwInRoute {
125 device: String,
126 to_track: String,
127 to_port: usize,
128}
129
130#[derive(Debug, Clone, PartialEq, Eq, Hash)]
131struct MidiHwOutRoute {
132 from_track: String,
133 from_port: usize,
134 device: String,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Hash)]
138struct MidiHwThruRoute {
139 from_device: String,
140 to_device: String,
141}
142
143struct OfflineBounceJob {
144 cancel: Arc<AtomicBool>,
145}
146
147#[cfg(unix)]
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149enum JackTransportPlaySync {
150 Start,
151 Stop,
152}
153
154#[derive(Clone, Copy)]
155#[cfg(unix)]
156struct AudioOpenRequest<'a> {
157 device: &'a str,
158 input_device: Option<&'a str>,
159 sample_rate_hz: i32,
160 bits: i32,
161 exclusive: bool,
162 period_frames: usize,
163 nperiods: usize,
164 sync_mode: bool,
165}
166
167struct ClipAddRequest<'a> {
168 clip_id: &'a str,
169 name: &'a str,
170 track_name: &'a str,
171 start: usize,
172 length: usize,
173 offset: usize,
174 input_channel: usize,
175 muted: bool,
176 peaks_file: Option<String>,
177 kind: Kind,
178 fade_enabled: bool,
179 fade_in_samples: usize,
180 fade_out_samples: usize,
181 source_name: Option<String>,
182 source_offset: Option<usize>,
183 source_length: Option<usize>,
184 preview_name: Option<String>,
185 pitch_correction_points: Vec<crate::message::PitchCorrectionPointData>,
186 pitch_correction_frame_likeness: Option<f32>,
187 pitch_correction_inertia_ms: Option<u16>,
188 pitch_correction_formant_compensation: Option<bool>,
189 plugin_graph_json: Option<serde_json::Value>,
190}
191
192#[cfg(unix)]
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194struct JackTransportSyncDecision {
195 play_sync: Option<JackTransportPlaySync>,
196 position_sync: Option<usize>,
197}
198
199#[derive(Clone, Debug, PartialEq, Eq)]
200enum MidiLearnSlot {
201 Track(String, crate::message::TrackMidiLearnTarget),
202 Global(crate::message::GlobalMidiLearnTarget),
203 Session(crate::message::SessionMidiLearnTarget),
204}
205
206pub struct Engine {
207 clients: Vec<Sender<Message>>,
208 rx: Receiver<Message>,
209 state: Arc<UnsafeMutex<State>>,
210 tx: Sender<Message>,
211 workers: Vec<WorkerData>,
212 hw_driver: Option<Arc<UnsafeMutex<HwDriver>>>,
213 #[cfg(unix)]
214 jack_runtime: Option<Arc<UnsafeMutex<JackRuntime>>>,
215 midi_hub: Arc<UnsafeMutex<MidiHub>>,
216 hw_worker: Option<WorkerData>,
217 osc_server: Option<OscServer>,
218 osc_reply_socket: Option<UdpSocket>,
219 osc_reply_target: Option<SocketAddr>,
220 pending_hw_midi_events: Vec<MidiEvent>,
221 pending_hw_midi_events_by_device: HashMap<String, Vec<MidiEvent>>,
222 pending_hw_midi_out_events: Vec<MidiEvent>,
223 pending_hw_midi_out_events_by_device: Vec<HwMidiEvent>,
224 active_hw_notes_by_track: HashMap<String, std::collections::HashSet<(String, u8, u8)>>,
225 active_hw_notes_cycle_start: HashMap<String, std::collections::HashSet<(String, u8, u8)>>,
226 midi_hw_in_routes: Vec<MidiHwInRoute>,
227 midi_hw_out_routes: Vec<MidiHwOutRoute>,
228 midi_hw_thru_routes: Vec<MidiHwThruRoute>,
229 ready_workers: Vec<usize>,
230 pending_requests: VecDeque<Action>,
231 awaiting_hwfinished: bool,
232 handling_hwfinished: bool,
233 track_process_epoch: usize,
234 transport_panic_flush_pending: bool,
235 transport_restart_pending: bool,
236 notified_loop_wrap_sample: Option<usize>,
237 transport_sample: usize,
238
239 hw_input_latency_frames: usize,
240
241 hw_output_latency_frames: usize,
242 loop_enabled: bool,
243 loop_range_samples: Option<(usize, usize)>,
244 metronome_enabled: bool,
245 tempo_bpm: f64,
246 tsig_num: u16,
247 tsig_denom: u16,
248 tempo_points: Vec<crate::message::TempoPoint>,
249 time_signature_points: Vec<crate::message::TimeSignaturePoint>,
250 punch_enabled: bool,
251 punch_range_samples: Option<(usize, usize)>,
252 audio_recordings: std::collections::HashMap<String, RecordingSession>,
253 midi_recordings: std::collections::HashMap<String, MidiRecordingSession>,
254 completed_audio_recordings: Vec<(String, RecordingSession)>,
255 completed_midi_recordings: Vec<(String, MidiRecordingSession)>,
256 playing: bool,
257 transport_running: bool,
258 clip_playback_enabled: bool,
259 session_clip_playback_enabled: bool,
260 session_transport_sample: usize,
261 record_enabled: bool,
262 step_recording_enabled: bool,
263 session_dir: Option<PathBuf>,
264 hw_out_level_db: f32,
265 hw_out_balance: f32,
266 hw_out_muted: bool,
267 last_hw_out_meter_publish: Option<Instant>,
268 #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
269 last_hw_out_meter_linear: Vec<f32>,
270 hw_out_peak_hold_linear: Vec<f32>,
271 #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
272 hw_out_meter_publish_phase: bool,
273 last_track_meter_publish: Option<Instant>,
274 last_session_report_publish: Option<Instant>,
275 session_report_state: HashMap<(String, usize), SessionSlotState>,
276 track_meter_linear_by_track: HashMap<String, Vec<f32>>,
277 task_processing_started_at: HashMap<String, Instant>,
278 cycle_tasks: Vec<ProcessTask>,
279 cycle_task_deps: HashMap<String, Vec<String>>,
280 cycle_tasks_running: Vec<ProcessTask>,
281 cycle_tasks_finished: Vec<ProcessTask>,
282 latest_hw_out_meter_db: Arc<Vec<f32>>,
283 latest_track_meter_snapshot: Arc<Vec<(String, Vec<f32>)>>,
284 history: History,
285 history_group: Option<UndoEntry>,
286 history_suspended: bool,
287 offline_bounce_jobs: HashMap<String, OfflineBounceJob>,
288 pending_midi_learn: Option<(String, crate::message::TrackMidiLearnTarget, Option<String>)>,
289 pending_global_midi_learn: Option<crate::message::GlobalMidiLearnTarget>,
290 pending_session_midi_learn: Option<crate::message::SessionMidiLearnTarget>,
291 global_midi_learn_play_pause: Option<crate::message::MidiLearnBinding>,
292 global_midi_learn_stop: Option<crate::message::MidiLearnBinding>,
293 global_midi_learn_record_toggle: Option<crate::message::MidiLearnBinding>,
294 session_midi_learn_slots: HashMap<(String, usize), crate::message::MidiLearnBinding>,
295 session_midi_learn_scenes: HashMap<usize, crate::message::MidiLearnBinding>,
296 session_midi_learn_stop_track: HashMap<String, crate::message::MidiLearnBinding>,
297 session_midi_learn_stop_all: Option<crate::message::MidiLearnBinding>,
298 midi_cc_gate: HashMap<(String, u8, u8), bool>,
299 modulators: Vec<crate::modulator::Modulator>,
300 modulator_values: Option<Arc<std::collections::HashMap<usize, f32>>>,
301}
302
303type MidiEditParseResult = (
304 Vec<MidiNoteData>,
305 Vec<MidiControllerData>,
306 Vec<(u64, Vec<u8>)>,
307);
308
309impl Engine {
310 pub fn state(&self) -> Arc<UnsafeMutex<State>> {
311 self.state.clone()
312 }
313
314 fn resolve_plugin_identifier(
315 &self,
316 kind: PluginKind,
317 identifier: &str,
318 ) -> Result<String, String> {
319 if identifier.is_empty() {
320 return Err("plugin identifier is empty".to_string());
321 }
322 if identifier.contains('/')
323 || identifier.contains('\\')
324 || identifier.contains("::")
325 || identifier.contains('#')
326 || identifier.contains("://")
327 || identifier.starts_with("file:")
328 || Path::new(identifier).exists()
329 {
330 return Ok(identifier.to_string());
331 }
332
333 match kind {
334 PluginKind::Clap => {
335 let plugins =
336 crate::plugins::scan_plugins::<crate::plugins::types::ClapPluginInfo>("clap")
337 .map_err(|e| format!("failed to scan CLAP plugins: {e}"))?;
338 plugins
339 .into_iter()
340 .find(|p| !p.id.is_empty() && p.id == identifier)
341 .map(|p| p.path)
342 .ok_or_else(|| format!("CLAP plugin ID not found: {identifier}"))
343 }
344 PluginKind::Vst3 => {
345 let plugins =
346 crate::plugins::scan_plugins::<crate::plugins::types::Vst3PluginInfo>("vst3")
347 .map_err(|e| format!("failed to scan VST3 plugins: {e}"))?;
348 plugins
349 .into_iter()
350 .find(|p| !p.id.is_empty() && p.id == identifier)
351 .map(|p| p.path)
352 .ok_or_else(|| format!("VST3 plugin ID not found: {identifier}"))
353 }
354 PluginKind::Lv2 => {
355 let plugins =
356 crate::plugins::scan_plugins::<crate::plugins::types::Lv2PluginInfo>("lv2")
357 .map_err(|e| format!("failed to scan LV2 plugins: {e}"))?;
358 plugins
359 .into_iter()
360 .find(|p| p.uri == identifier)
361 .map(|p| p.uri)
362 .ok_or_else(|| format!("LV2 plugin URI not found: {identifier}"))
363 }
364 }
365 }
366
367 fn timing_at_sample(&self, sample: usize) -> (f64, u16, u16) {
368 let bpm = self
369 .tempo_points
370 .iter()
371 .filter(|p| p.sample <= sample)
372 .max_by_key(|p| p.sample)
373 .map(|p| p.bpm)
374 .unwrap_or(self.tempo_bpm)
375 .max(1.0);
376 let (num, den) = self
377 .time_signature_points
378 .iter()
379 .filter(|p| p.sample <= sample)
380 .max_by_key(|p| p.sample)
381 .map(|p| (p.numerator.max(1), p.denominator.max(1)))
382 .unwrap_or((self.tsig_num.max(1), self.tsig_denom.max(1)));
383 (bpm, num, den)
384 }
385
386 fn update_global_tempo_from_map(&mut self) {
387 let (bpm, num, den) = self.timing_at_sample(0);
388 self.tempo_bpm = bpm;
389 self.tsig_num = num;
390 self.tsig_denom = den;
391 }
392
393 const METRONOME_TRACK: &'static str = "metronome";
394 const METRONOME_DEFAULT_LEVEL_DB: f32 = -10.0;
395 const MIDI_CC_ALL_SOUND_OFF: u8 = 120;
396 const MIDI_CC_SUSTAIN_PEDAL: u8 = 64;
397
398 fn default_clip_plugin_graph_json(audio_ins: usize, audio_outs: usize) -> serde_json::Value {
399 let connections = (0..audio_ins.min(audio_outs))
400 .map(|port| {
401 serde_json::json!({
402 "from_node": "TrackInput",
403 "from_port": port,
404 "to_node": "TrackOutput",
405 "to_port": port,
406 "kind": "Audio",
407 })
408 })
409 .collect::<Vec<_>>();
410 serde_json::json!({
411 "plugins": [],
412 "connections": connections,
413 })
414 }
415
416 fn meter_linear_to_db(peak: f32) -> f32 {
417 if peak <= 1.0e-6 {
418 -90.0
419 } else {
420 (20.0 * peak.log10()).clamp(-90.0, 20.0)
421 }
422 }
423
424 fn note_off_events_for_track(&mut self, track_name: &str) -> Vec<HwMidiEvent> {
425 let Some(active) = self.active_hw_notes_by_track.remove(track_name) else {
426 return vec![];
427 };
428 let mut channels = std::collections::HashSet::<(String, u8)>::new();
429 let mut events = Vec::with_capacity(active.len() * 2);
430 for (device, channel, pitch) in active {
431 channels.insert((device.clone(), channel));
432 events.push(HwMidiEvent {
433 device,
434 event: MidiEvent::new(0, vec![0x80 | channel.min(15), pitch.min(127), 64]),
435 });
436 }
437 for (device, channel) in channels {
438 events.push(HwMidiEvent {
439 device,
440 event: MidiEvent::new(
441 0,
442 vec![0xB0 | channel.min(15), Self::MIDI_CC_SUSTAIN_PEDAL, 0],
443 ),
444 });
445 }
446 events
447 }
448
449 fn set_clip_plugin_graph_json(
450 &mut self,
451 track_name: &str,
452 clip_index: usize,
453 plugin_graph_json: Option<serde_json::Value>,
454 ) {
455 if let Some(track) = self.state.lock().tracks.get(track_name) {
456 let track = track.lock();
457 if let Some(clip) = track.audio.clips.get_mut(clip_index) {
458 clip.plugin_graph_json = plugin_graph_json;
459 }
460 }
461 }
462
463 fn update_active_hw_notes_for_track(&mut self, track_name: &str, device: &str, data: &[u8]) {
464 let Some(status) = data.first().copied() else {
465 return;
466 };
467 let channel = status & 0x0F;
468 match status & 0xF0 {
469 0x80 => {
470 if let Some(&pitch) = data.get(1)
471 && let Some(active) = self.active_hw_notes_by_track.get_mut(track_name)
472 {
473 active.remove(&(device.to_string(), channel, pitch));
474 if active.is_empty() {
475 self.active_hw_notes_by_track.remove(track_name);
476 }
477 }
478 }
479 0x90 => {
480 let Some(&pitch) = data.get(1) else {
481 return;
482 };
483 let velocity = data.get(2).copied().unwrap_or(0);
484 if velocity == 0 {
485 if let Some(active) = self.active_hw_notes_by_track.get_mut(track_name) {
486 active.remove(&(device.to_string(), channel, pitch));
487 if active.is_empty() {
488 self.active_hw_notes_by_track.remove(track_name);
489 }
490 }
491 } else {
492 self.active_hw_notes_by_track
493 .entry(track_name.to_string())
494 .or_default()
495 .insert((device.to_string(), channel, pitch));
496 }
497 }
498 _ => {}
499 }
500 }
501
502 fn note_off_events_for_all_active_tracks(&mut self) -> Vec<HwMidiEvent> {
503 let track_names: Vec<String> = self.active_hw_notes_by_track.keys().cloned().collect();
504 let mut events = Vec::new();
505 for track_name in track_names {
506 events.extend(self.note_off_events_for_track(&track_name));
507 }
508 events
509 }
510
511 fn panic_events_for_all_hw_midi_outputs(&self) -> Vec<HwMidiEvent> {
512 let mut active_channels = std::collections::HashSet::<(String, u8)>::new();
513 for active in self.active_hw_notes_by_track.values() {
514 for (device, channel, _pitch) in active {
515 active_channels.insert((device.clone(), *channel));
516 }
517 }
518 let mut events = Vec::with_capacity(active_channels.len());
519 for (device, channel) in active_channels {
520 events.push(HwMidiEvent {
521 device,
522 event: MidiEvent::new(0, vec![0xB0 | channel, Self::MIDI_CC_ALL_SOUND_OFF, 0]),
523 });
524 }
525 events
526 }
527
528 fn note_off_events_for_active_snapshot(
529 &self,
530 snapshot: &HashMap<String, std::collections::HashSet<(String, u8, u8)>>,
531 frame: u32,
532 ) -> Vec<HwMidiEvent> {
533 let mut channels = std::collections::HashSet::<(String, u8)>::new();
534 let mut events = Vec::new();
535 for active in snapshot.values() {
536 for (device, channel, pitch) in active {
537 channels.insert((device.clone(), *channel));
538 events.push(HwMidiEvent {
539 device: device.clone(),
540 event: MidiEvent::new(
541 frame,
542 vec![0x80 | (*channel).min(15), (*pitch).min(127), 64],
543 ),
544 });
545 }
546 }
547 for (device, channel) in channels {
548 events.push(HwMidiEvent {
549 device,
550 event: MidiEvent::new(
551 frame,
552 vec![0xB0 | channel.min(15), Self::MIDI_CC_SUSTAIN_PEDAL, 0],
553 ),
554 });
555 }
556 events
557 }
558
559 fn parse_midi_clip_for_edit(
560 path: &Path,
561 sample_rate: f64,
562 clip_start: usize,
563 ) -> Result<MidiEditParseResult, String> {
564 let bytes = std::fs::read(path).map_err(|e| e.to_string())?;
565 let smf = Smf::parse(&bytes).map_err(|e| e.to_string())?;
566 let Timing::Metrical(ppq) = smf.header.timing else {
567 return Ok((vec![], vec![], vec![]));
568 };
569 let ppq = u64::from(ppq.as_int().max(1));
570
571 let mut tempo_changes: Vec<(u64, u32)> = vec![(0, 500_000)];
572 for track in &smf.tracks {
573 let mut tick = 0_u64;
574 for event in track {
575 tick = tick.saturating_add(event.delta.as_int() as u64);
576 if let TrackEventKind::Meta(MetaMessage::Tempo(us_per_q)) = event.kind {
577 tempo_changes.push((tick, us_per_q.as_int()));
578 }
579 }
580 }
581 tempo_changes.sort_by_key(|(tick, _)| *tick);
582 let mut normalized_tempos: Vec<(u64, u32)> = Vec::with_capacity(tempo_changes.len());
583 for (tick, tempo) in tempo_changes {
584 if let Some(last) = normalized_tempos.last_mut()
585 && last.0 == tick
586 {
587 last.1 = tempo;
588 } else {
589 normalized_tempos.push((tick, tempo));
590 }
591 }
592 let tempo_changes = normalized_tempos;
593
594 let ticks_to_samples = |tick: u64| -> usize {
595 let mut total_us: u128 = 0;
596 let mut prev_tick = 0_u64;
597 let mut current_tempo_us = 500_000_u32;
598 for (change_tick, tempo_us) in &tempo_changes {
599 if *change_tick > tick {
600 break;
601 }
602 let seg_ticks = change_tick.saturating_sub(prev_tick);
603 total_us = total_us.saturating_add(
604 u128::from(seg_ticks).saturating_mul(u128::from(current_tempo_us))
605 / u128::from(ppq),
606 );
607 prev_tick = *change_tick;
608 current_tempo_us = *tempo_us;
609 }
610 let rem = tick.saturating_sub(prev_tick);
611 total_us = total_us.saturating_add(
612 u128::from(rem).saturating_mul(u128::from(current_tempo_us)) / u128::from(ppq),
613 );
614 ((total_us as f64 / 1_000_000.0) * sample_rate).round() as usize
615 };
616
617 let mut notes = Vec::<MidiNoteData>::new();
618 let mut controllers = Vec::<MidiControllerData>::new();
619 let mut passthrough_events = Vec::<(u64, Vec<u8>)>::new();
620 let mut active_notes: HashMap<(u8, u8), Vec<(u64, u8)>> = HashMap::new();
621
622 for track in &smf.tracks {
623 let mut tick = 0_u64;
624 for event in track {
625 tick = tick.saturating_add(event.delta.as_int() as u64);
626 match event.kind {
627 TrackEventKind::Midi { channel, message } => {
628 let channel_u8 = channel.as_int();
629 match message {
630 midly::MidiMessage::NoteOn { key, vel } => {
631 let pitch = key.as_int();
632 let velocity = vel.as_int();
633 if velocity == 0 {
634 if let Some(starts) = active_notes.get_mut(&(channel_u8, pitch))
635 && let Some((start_tick, start_vel)) = starts.pop()
636 {
637 let start_sample = ticks_to_samples(start_tick);
638 let end_sample = ticks_to_samples(tick);
639 notes.push(MidiNoteData {
640 start_sample,
641 length_samples: end_sample
642 .saturating_sub(start_sample)
643 .max(1),
644 pitch,
645 velocity: start_vel,
646 channel: channel_u8,
647 });
648 }
649 } else {
650 active_notes
651 .entry((channel_u8, pitch))
652 .or_default()
653 .push((tick, velocity));
654 }
655 }
656 midly::MidiMessage::NoteOff { key, .. } => {
657 let pitch = key.as_int();
658 if let Some(starts) = active_notes.get_mut(&(channel_u8, pitch))
659 && let Some((start_tick, start_vel)) = starts.pop()
660 {
661 let start_sample = ticks_to_samples(start_tick);
662 let end_sample = ticks_to_samples(tick);
663 notes.push(MidiNoteData {
664 start_sample,
665 length_samples: end_sample
666 .saturating_sub(start_sample)
667 .max(1),
668 pitch,
669 velocity: start_vel,
670 channel: channel_u8,
671 });
672 }
673 }
674 midly::MidiMessage::Controller { controller, value } => {
675 controllers.push(MidiControllerData {
676 sample: ticks_to_samples(tick),
677 controller: controller.as_int(),
678 value: value.as_int(),
679 channel: channel_u8,
680 });
681 }
682 _ => {
683 let mut data = Vec::with_capacity(3);
684 if (LiveEvent::Midi { channel, message })
685 .write(&mut data)
686 .is_ok()
687 {
688 passthrough_events.push((ticks_to_samples(tick) as u64, data));
689 }
690 }
691 }
692 }
693 TrackEventKind::SysEx(payload) => {
694 let mut data = Vec::with_capacity(payload.len() + 2);
695 data.push(0xF0);
696 data.extend_from_slice(payload);
697 if data.last().copied() != Some(0xF7) {
698 data.push(0xF7);
699 }
700 passthrough_events.push((ticks_to_samples(tick) as u64, data));
701 }
702 TrackEventKind::Escape(payload) => {
703 let mut data = Vec::with_capacity(payload.len() + 1);
704 data.push(0xF7);
705 data.extend_from_slice(payload);
706 passthrough_events.push((ticks_to_samples(tick) as u64, data));
707 }
708 _ => {}
709 }
710 }
711 }
712
713 for ((channel, pitch), starts) in active_notes {
714 for (start_tick, velocity) in starts {
715 let start_sample = ticks_to_samples(start_tick);
716 let end_sample = ticks_to_samples(start_tick.saturating_add(ppq / 8));
717 notes.push(MidiNoteData {
718 start_sample,
719 length_samples: end_sample.saturating_sub(start_sample).max(1),
720 pitch,
721 velocity,
722 channel,
723 });
724 }
725 }
726
727 notes.sort_by_key(|n| (n.start_sample, n.pitch));
728 controllers.sort_by_key(|c| (c.sample, c.controller));
729 passthrough_events.sort_by_key(|(sample, _)| *sample);
730
731 let min_sample = notes
732 .iter()
733 .map(|n| n.start_sample)
734 .chain(controllers.iter().map(|c| c.sample))
735 .chain(passthrough_events.iter().map(|(s, _)| *s as usize))
736 .min()
737 .unwrap_or(0);
738 if min_sample >= clip_start && clip_start > 0 {
739 for note in &mut notes {
740 note.start_sample = note.start_sample.saturating_sub(clip_start);
741 }
742 for ctrl in &mut controllers {
743 ctrl.sample = ctrl.sample.saturating_sub(clip_start);
744 }
745 for (sample, _) in &mut passthrough_events {
746 *sample = sample.saturating_sub(clip_start as u64);
747 }
748 }
749
750 Ok((notes, controllers, passthrough_events))
751 }
752
753 fn midi_events_from_notes_and_controllers(
754 notes: &[MidiNoteData],
755 controllers: &[MidiControllerData],
756 ) -> Vec<(u64, Vec<u8>)> {
757 let mut events: Vec<(u64, u8, Vec<u8>)> = Vec::new();
758 for note in notes {
759 let channel = note.channel.min(15);
760 let pitch = note.pitch.min(127);
761 let velocity = note.velocity.min(127);
762 let start = note.start_sample as u64;
763 let end = note.start_sample.saturating_add(note.length_samples).max(1) as u64;
764 events.push((start, 2, vec![0x90 | channel, pitch, velocity]));
765 events.push((end, 0, vec![0x80 | channel, pitch, 64]));
766 }
767 for ctrl in controllers {
768 let channel = ctrl.channel.min(15);
769 let controller = ctrl.controller.min(127);
770 let value = ctrl.value.min(127);
771 events.push((
772 ctrl.sample as u64,
773 1,
774 vec![0xB0 | channel, controller, value],
775 ));
776 }
777 events.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
778 events
779 .into_iter()
780 .map(|(sample, _, data)| (sample, data))
781 .collect()
782 }
783
784 fn is_track_frozen(&self, track_name: &str) -> bool {
785 self.state
786 .lock()
787 .tracks
788 .get(track_name)
789 .map(|track| track.lock().frozen())
790 .unwrap_or(false)
791 }
792
793 async fn reject_if_track_frozen(&mut self, track_name: &str, operation: &str) -> bool {
794 if self.is_track_frozen(track_name) {
795 self.notify_clients(Err(format!(
796 "Track '{track_name}' is frozen; {operation} is blocked"
797 )))
798 .await;
799 true
800 } else {
801 false
802 }
803 }
804
805 fn apply_midi_edit_action(&mut self, action: &Action) -> Result<(), String> {
806 let (track_name, clip_index) = match action {
807 Action::ModifyMidiNotes {
808 track_name,
809 clip_index,
810 ..
811 }
812 | Action::InsertMidiNotes {
813 track_name,
814 clip_index,
815 ..
816 }
817 | Action::DeleteMidiNotes {
818 track_name,
819 clip_index,
820 ..
821 }
822 | Action::ModifyMidiControllers {
823 track_name,
824 clip_index,
825 ..
826 }
827 | Action::InsertMidiControllers {
828 track_name,
829 clip_index,
830 ..
831 }
832 | Action::DeleteMidiControllers {
833 track_name,
834 clip_index,
835 ..
836 }
837 | Action::SetMidiSysExEvents {
838 track_name,
839 clip_index,
840 ..
841 } => (track_name, *clip_index),
842 _ => return Ok(()),
843 };
844
845 let track_handle = self
846 .state
847 .lock()
848 .tracks
849 .get(track_name)
850 .cloned()
851 .ok_or_else(|| format!("Track not found: {track_name}"))?;
852 let (clip_name, clip_path, sample_rate, clip_start) = {
853 let track = track_handle.lock();
854 if clip_index >= track.midi.clips.len() {
855 return Err(format!(
856 "Invalid MIDI clip index {clip_index} for '{track_name}'"
857 ));
858 }
859 let clip = &track.midi.clips[clip_index];
860 let clip_name = clip.name.clone();
861 let clip_path = track.resolve_clip_path(&clip_name);
862 (clip_name, clip_path, track.sample_rate, clip.start)
863 };
864
865 let (mut notes, mut controllers, mut passthrough_events) =
866 Self::parse_midi_clip_for_edit(&clip_path, sample_rate, clip_start)?;
867
868 match action {
869 Action::ModifyMidiNotes {
870 note_indices,
871 new_notes,
872 ..
873 } => {
874 for (idx, new_note) in note_indices.iter().zip(new_notes.iter()) {
875 if let Some(note) = notes.get_mut(*idx) {
876 *note = new_note.clone();
877 }
878 }
879 }
880 Action::DeleteMidiNotes { note_indices, .. } => {
881 let mut indices = note_indices.clone();
882 indices.sort_unstable();
883 indices.dedup();
884 for idx in indices.into_iter().rev() {
885 if idx < notes.len() {
886 notes.remove(idx);
887 }
888 }
889 }
890 Action::InsertMidiNotes {
891 notes: inserted, ..
892 } => {
893 let mut sorted = inserted.clone();
894 sorted.sort_unstable_by_key(|(idx, _)| *idx);
895 for (idx, note) in sorted {
896 let at = idx.min(notes.len());
897 notes.insert(at, note);
898 }
899 }
900 Action::ModifyMidiControllers {
901 controller_indices,
902 new_controllers,
903 ..
904 } => {
905 for (idx, new_ctrl) in controller_indices.iter().zip(new_controllers.iter()) {
906 if let Some(ctrl) = controllers.get_mut(*idx) {
907 *ctrl = new_ctrl.clone();
908 }
909 }
910 }
911 Action::DeleteMidiControllers {
912 controller_indices, ..
913 } => {
914 let mut indices = controller_indices.clone();
915 indices.sort_unstable();
916 indices.dedup();
917 for idx in indices.into_iter().rev() {
918 if idx < controllers.len() {
919 controllers.remove(idx);
920 }
921 }
922 }
923 Action::InsertMidiControllers {
924 controllers: inserted,
925 ..
926 } => {
927 let mut sorted = inserted.clone();
928 sorted.sort_unstable_by_key(|(idx, _)| *idx);
929 for (idx, ctrl) in sorted {
930 let at = idx.min(controllers.len());
931 controllers.insert(at, ctrl);
932 }
933 }
934 Action::SetMidiSysExEvents {
935 new_sysex_events, ..
936 } => {
937 passthrough_events
938 .retain(|(_, data)| !matches!(data.first(), Some(0xF0) | Some(0xF7)));
939 passthrough_events.extend(
940 new_sysex_events
941 .iter()
942 .map(|ev| (ev.sample as u64, ev.data.clone())),
943 );
944 }
945 _ => {}
946 }
947
948 notes.sort_by_key(|n| (n.start_sample, n.pitch));
949 controllers.sort_by_key(|c| (c.sample, c.controller));
950 passthrough_events.sort_by_key(|(sample, _)| *sample);
951 let mut events = Self::midi_events_from_notes_and_controllers(¬es, &controllers);
952 events.extend(passthrough_events);
953 events.sort_by_key(|(sample, _)| *sample);
954 Self::write_midi_file(&clip_path, sample_rate.max(1.0) as u32, &events)?;
955 track_handle.lock().invalidate_midi_clip_cache(&clip_name);
956 Ok(())
957 }
958
959 const METER_PUBLISH_INTERVAL: Duration = Duration::from_millis(50);
960 const SESSION_RUNTIME_REPORT_INTERVAL: Duration = Duration::from_millis(50);
961 const TRACK_PROCESS_TIMEOUT: Duration = Duration::from_millis(250);
962 #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
963 const HW_OUT_METER_LINEAR_EPSILON: f32 = 0.0025;
964
965 #[cfg(all(unix, not(target_os = "macos")))]
966 fn session_plugins_dir(&self) -> Option<PathBuf> {
967 self.session_dir.as_ref().map(|d| d.join("plugins"))
968 }
969
970 fn session_audio_dir(&self) -> Option<PathBuf> {
971 self.session_dir.as_ref().map(|d| d.join("audio"))
972 }
973
974 fn session_midi_dir(&self) -> Option<PathBuf> {
975 self.session_dir.as_ref().map(|d| d.join("midi"))
976 }
977
978 fn session_peaks_dir(&self) -> Option<PathBuf> {
979 self.session_dir.as_ref().map(|d| d.join("peaks"))
980 }
981
982 fn ensure_session_subdirs(&self) {
983 if let Some(root) = &self.session_dir {
984 let _ = std::fs::create_dir_all(root.join("plugins"));
985 let _ = std::fs::create_dir_all(root.join("audio"));
986 let _ = std::fs::create_dir_all(root.join("midi"));
987 let _ = std::fs::create_dir_all(root.join("peaks"));
988 }
989 }
990
991 fn finalize_midi_hw_devices(mut devices: Vec<String>) -> Vec<String> {
992 devices.sort();
993 devices.dedup();
994 devices
995 }
996
997 #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"))]
998 fn discover_midi_hw_devices_from_dir(path: &str, prefixes: &[&str]) -> Vec<String> {
999 let devices = read_dir(path)
1000 .map(|rd| {
1001 rd.filter_map(Result::ok)
1002 .map(|e| e.path())
1003 .filter_map(|path| {
1004 let name = path.file_name()?.to_str()?;
1005 prefixes
1006 .iter()
1007 .any(|prefix| name.starts_with(prefix))
1008 .then(|| path.to_string_lossy().into_owned())
1009 })
1010 .collect()
1011 })
1012 .unwrap_or_default();
1013 Self::finalize_midi_hw_devices(devices)
1014 }
1015
1016 fn discover_midi_hw_devices() -> Vec<String> {
1017 #[cfg(target_os = "freebsd")]
1018 let devices = Self::discover_midi_hw_devices_from_dir("/dev", &["umidi", "midi"]);
1019 #[cfg(target_os = "linux")]
1020 let devices = Self::discover_midi_hw_devices_from_dir("/dev/snd", &["midiC"]);
1021 #[cfg(target_os = "openbsd")]
1022 let devices = Self::discover_midi_hw_devices_from_dir("/dev", &["midi"]);
1023 #[cfg(target_os = "windows")]
1024 let devices = {
1025 let mut devices = wasapi::list_midi_input_devices();
1026 devices.extend(wasapi::list_midi_output_devices());
1027 Self::finalize_midi_hw_devices(devices)
1028 };
1029 #[cfg(target_os = "macos")]
1030 let devices = {
1031 let mut devices = Vec::new();
1032 for source in coremidi::Sources {
1033 if let Some(name) = source.display_name() {
1034 devices.push(name);
1035 }
1036 }
1037 for dest in coremidi::Destinations {
1038 if let Some(name) = dest.display_name() {
1039 devices.push(name);
1040 }
1041 }
1042 Self::finalize_midi_hw_devices(devices)
1043 };
1044 devices
1045 }
1046
1047 pub fn new(rx: Receiver<Message>, tx: Sender<Message>) -> Self {
1048 Self {
1049 rx,
1050 tx,
1051 clients: vec![],
1052 state: Arc::new(UnsafeMutex::new(State::default())),
1053 workers: vec![],
1054 hw_driver: None,
1055 #[cfg(unix)]
1056 jack_runtime: None,
1057 midi_hub: Arc::new(UnsafeMutex::new(MidiHub::default())),
1058 hw_worker: None,
1059 osc_server: None,
1060 osc_reply_socket: None,
1061 osc_reply_target: None,
1062 pending_hw_midi_events: vec![],
1063 pending_hw_midi_events_by_device: HashMap::new(),
1064 pending_hw_midi_out_events: vec![],
1065 pending_hw_midi_out_events_by_device: vec![],
1066 active_hw_notes_by_track: HashMap::new(),
1067 active_hw_notes_cycle_start: HashMap::new(),
1068 midi_hw_in_routes: vec![],
1069 midi_hw_out_routes: vec![],
1070 midi_hw_thru_routes: vec![],
1071 ready_workers: vec![],
1072 pending_requests: VecDeque::new(),
1073 awaiting_hwfinished: false,
1074 handling_hwfinished: false,
1075 track_process_epoch: 0,
1076 transport_panic_flush_pending: false,
1077 transport_restart_pending: false,
1078 notified_loop_wrap_sample: None,
1079 transport_sample: 0,
1080 hw_input_latency_frames: 0,
1081 hw_output_latency_frames: 0,
1082 loop_enabled: false,
1083 loop_range_samples: None,
1084 metronome_enabled: false,
1085 tempo_bpm: 120.0,
1086 tsig_num: 4,
1087 tsig_denom: 4,
1088 tempo_points: vec![crate::message::TempoPoint {
1089 sample: 0,
1090 bpm: 120.0,
1091 }],
1092 time_signature_points: vec![crate::message::TimeSignaturePoint {
1093 sample: 0,
1094 numerator: 4,
1095 denominator: 4,
1096 }],
1097 punch_enabled: false,
1098 punch_range_samples: None,
1099 audio_recordings: std::collections::HashMap::new(),
1100 midi_recordings: std::collections::HashMap::new(),
1101 completed_audio_recordings: Vec::new(),
1102 completed_midi_recordings: Vec::new(),
1103 playing: false,
1104 transport_running: false,
1105 clip_playback_enabled: true,
1106 session_clip_playback_enabled: false,
1107 session_transport_sample: 0,
1108 record_enabled: false,
1109 step_recording_enabled: false,
1110 session_dir: None,
1111 hw_out_level_db: 0.0,
1112 hw_out_balance: 0.0,
1113 hw_out_muted: false,
1114 last_hw_out_meter_publish: None,
1115 #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
1116 last_hw_out_meter_linear: vec![],
1117 hw_out_peak_hold_linear: vec![],
1118 #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
1119 hw_out_meter_publish_phase: false,
1120 last_track_meter_publish: None,
1121 last_session_report_publish: None,
1122 session_report_state: HashMap::new(),
1123 track_meter_linear_by_track: HashMap::new(),
1124 task_processing_started_at: HashMap::new(),
1125 cycle_tasks: Vec::new(),
1126 cycle_task_deps: HashMap::new(),
1127 cycle_tasks_running: Vec::new(),
1128 cycle_tasks_finished: Vec::new(),
1129 latest_hw_out_meter_db: Arc::new(Vec::new()),
1130 latest_track_meter_snapshot: Arc::new(Vec::new()),
1131 history: History::default(),
1132 history_group: None,
1133 history_suspended: false,
1134 offline_bounce_jobs: HashMap::new(),
1135 pending_midi_learn: None,
1136 pending_global_midi_learn: None,
1137 pending_session_midi_learn: None,
1138 global_midi_learn_play_pause: None,
1139 global_midi_learn_stop: None,
1140 global_midi_learn_record_toggle: None,
1141 session_midi_learn_slots: HashMap::new(),
1142 session_midi_learn_scenes: HashMap::new(),
1143 session_midi_learn_stop_track: HashMap::new(),
1144 session_midi_learn_stop_all: None,
1145 midi_cc_gate: HashMap::new(),
1146 modulators: Vec::new(),
1147 modulator_values: None,
1148 }
1149 }
1150
1151 fn hw_driver_cycle_samples(&self) -> Option<usize> {
1152 self.hw_driver.as_ref().map(|o| o.lock().cycle_samples())
1153 }
1154
1155 #[cfg(unix)]
1156 fn jack_cycle_samples(&self) -> Option<usize> {
1157 self.jack_runtime.as_ref().map(|j| j.lock().buffer_size)
1158 }
1159
1160 #[cfg(not(unix))]
1161 fn jack_cycle_samples(&self) -> Option<usize> {
1162 None
1163 }
1164
1165 fn current_cycle_samples(&self) -> usize {
1166 self.hw_driver_cycle_samples()
1167 .or_else(|| self.jack_cycle_samples())
1168 .unwrap_or(0)
1169 }
1170
1171 fn sample_rate(&self) -> f64 {
1172 if let Some(hw) = &self.hw_driver {
1173 hw.lock().sample_rate() as f64
1174 } else {
1175 #[cfg(unix)]
1176 {
1177 self.jack_runtime
1178 .as_ref()
1179 .map(|j| j.lock().sample_rate as f64)
1180 .unwrap_or(48_000.0)
1181 }
1182 #[cfg(not(unix))]
1183 {
1184 48_000.0
1185 }
1186 }
1187 }
1188
1189 fn active_transport_sample(&self) -> usize {
1190 if self.session_clip_playback_enabled && self.playing {
1191 self.session_transport_sample
1192 } else {
1193 self.transport_sample
1194 }
1195 }
1196
1197 fn compute_modulator_values(
1198 &self,
1199 sample: usize,
1200 ) -> Arc<std::collections::HashMap<usize, f32>> {
1201 let sample_rate = self.sample_rate();
1202 let (bpm, tsig_num, tsig_denom) = self.timing_at_sample(sample);
1203 let values: std::collections::HashMap<usize, f32> = self
1204 .modulators
1205 .iter()
1206 .filter(|m| m.enabled)
1207 .map(|m| {
1208 (
1209 m.id,
1210 m.value_at(sample, sample_rate, bpm, tsig_num, tsig_denom),
1211 )
1212 })
1213 .collect();
1214 Arc::new(values)
1215 }
1216
1217 fn apply_modulators(&mut self, sample: usize) -> Vec<Action> {
1218 use crate::modulator::ModulatorTarget;
1219 let values = self.compute_modulator_values(sample);
1220 self.modulator_values = Some(values.clone());
1221 let mut echoes = Vec::new();
1222 let mut per_track: HashMap<String, (Option<f32>, Option<f32>)> = HashMap::new();
1223 let mut clap_params: HashMap<(String, usize, u32), f64> = HashMap::new();
1224 let mut vst3_params: HashMap<(String, usize, u32), f32> = HashMap::new();
1225 #[cfg(all(unix, not(target_os = "macos")))]
1226 let mut lv2_params: HashMap<(String, usize, u32), f32> = HashMap::new();
1227 let mut midi_cc_events: HashMap<String, Vec<MidiEvent>> = HashMap::new();
1228
1229 let map_f32 = |value: f32, min: f32, max: f32| -> f32 {
1230 crate::modulator::map_value(value, min, max)
1231 };
1232 let map_f64 = |value: f32, min: f64, max: f64| -> f64 {
1233 crate::modulator::map_value_f64(value, min, max)
1234 };
1235
1236 for m in &self.modulators {
1237 if !m.enabled {
1238 continue;
1239 }
1240 let Some(&value) = values.get(&m.id) else {
1241 continue;
1242 };
1243 for target in &m.targets {
1244 match target {
1245 ModulatorTarget::TrackVolume {
1246 track_name,
1247 min,
1248 max,
1249 } => {
1250 let clamped = map_f32(value, *min, *max);
1251 per_track.entry(track_name.clone()).or_default().0 = Some(clamped);
1252 }
1253 ModulatorTarget::TrackBalance {
1254 track_name,
1255 min,
1256 max,
1257 } => {
1258 let clamped = map_f32(value, *min, *max);
1259 per_track.entry(track_name.clone()).or_default().1 = Some(clamped);
1260 }
1261 ModulatorTarget::HwOutVolume { min, max } => {
1262 let clamped = map_f32(value, *min, *max);
1263 if (self.hw_out_level_db - clamped).abs() > f32::EPSILON {
1264 self.hw_out_level_db = clamped;
1265 echoes
1266 .push(Action::TrackAutomationLevel("hw:out".to_string(), clamped));
1267 }
1268 }
1269 ModulatorTarget::HwOutBalance { min, max } => {
1270 let next = map_f32(value, *min, *max).clamp(-1.0, 1.0);
1271 if (self.hw_out_balance - next).abs() > f32::EPSILON {
1272 self.hw_out_balance = next;
1273 echoes.push(Action::TrackAutomationBalance("hw:out".to_string(), next));
1274 }
1275 }
1276 ModulatorTarget::ClapParameter {
1277 track_name,
1278 instance_id,
1279 param_id,
1280 min,
1281 max,
1282 } => {
1283 let param_value = map_f64(value, *min, *max);
1284 clap_params
1285 .insert((track_name.clone(), *instance_id, *param_id), param_value);
1286 }
1287 ModulatorTarget::Vst3Parameter {
1288 track_name,
1289 instance_id,
1290 param_id,
1291 min,
1292 max,
1293 } => {
1294 let param_value = map_f32(value, *min, *max);
1295 vst3_params
1296 .insert((track_name.clone(), *instance_id, *param_id), param_value);
1297 }
1298 #[cfg(all(unix, not(target_os = "macos")))]
1299 ModulatorTarget::Lv2Parameter {
1300 track_name,
1301 instance_id,
1302 index,
1303 min,
1304 max,
1305 } => {
1306 let param_value = map_f32(value, *min, *max);
1307 lv2_params.insert((track_name.clone(), *instance_id, *index), param_value);
1308 }
1309 ModulatorTarget::MidiCc {
1310 track_name,
1311 channel,
1312 cc,
1313 } => {
1314 let cc_value = (value * 127.0).round() as u8;
1315 midi_cc_events
1316 .entry(track_name.clone())
1317 .or_default()
1318 .push(MidiEvent::new(
1319 0,
1320 vec![0xB0 | (*channel).min(15), (*cc).min(127), cc_value],
1321 ));
1322 }
1323 }
1324 }
1325 }
1326 for (track_name, (level, balance)) in per_track {
1327 if let Some(level) = level
1328 && let Some(track) = self.state.lock().tracks.get(&track_name).cloned()
1329 {
1330 let t = track.lock();
1331 if (t.level() - level).abs() > f32::EPSILON {
1332 t.set_level(level);
1333 echoes.push(Action::TrackAutomationLevel(track_name.clone(), level));
1334 }
1335 }
1336 if let Some(balance) = balance
1337 && let Some(track) = self.state.lock().tracks.get(&track_name).cloned()
1338 {
1339 let t = track.lock();
1340 let next = balance.clamp(-1.0, 1.0);
1341 if (t.balance - next).abs() > f32::EPSILON {
1342 t.set_balance(next);
1343 echoes.push(Action::TrackAutomationBalance(track_name.clone(), next));
1344 }
1345 }
1346 }
1347
1348 for (track_name, events) in midi_cc_events {
1349 if let Some(track) = self.state.lock().tracks.get(&track_name).cloned() {
1350 track.lock().pending_modulator_midi_events.extend(events);
1351 }
1352 }
1353
1354 let state = self.state.lock();
1355 for ((track_name, instance_id, param_id), value) in clap_params {
1356 if let Some(track) = state.tracks.get(&track_name).cloned()
1357 && track
1358 .lock()
1359 .set_clap_parameter(instance_id, param_id, value)
1360 .is_ok()
1361 {
1362 echoes.push(Action::TrackSetClapParameter {
1363 track_name,
1364 instance_id,
1365 param_id,
1366 value,
1367 });
1368 }
1369 }
1370 for ((track_name, instance_id, param_id), value) in vst3_params {
1371 if let Some(track) = state.tracks.get(&track_name).cloned()
1372 && track
1373 .lock()
1374 .set_vst3_parameter(instance_id, param_id, value)
1375 .is_ok()
1376 {
1377 echoes.push(Action::TrackSetVst3Parameter {
1378 track_name,
1379 instance_id,
1380 param_id,
1381 value,
1382 });
1383 }
1384 }
1385 #[cfg(all(unix, not(target_os = "macos")))]
1386 for ((track_name, instance_id, index), value) in lv2_params {
1387 if let Some(track) = state.tracks.get(&track_name).cloned()
1388 && track
1389 .lock()
1390 .set_lv2_control_value(instance_id, index as usize, f64::from(value))
1391 .is_ok()
1392 {
1393 echoes.push(Action::TrackSetLv2ControlValue {
1394 track_name,
1395 instance_id,
1396 index,
1397 value,
1398 });
1399 }
1400 }
1401
1402 echoes
1403 }
1404
1405 fn session_end_sample(&self) -> usize {
1406 self.state
1407 .lock()
1408 .tracks
1409 .values()
1410 .map(|track| {
1411 let track = track.lock();
1412 let audio_end = track
1413 .audio
1414 .clips
1415 .iter()
1416 .map(|clip| clip.end)
1417 .max()
1418 .unwrap_or(0);
1419 let midi_end = track
1420 .midi
1421 .clips
1422 .iter()
1423 .map(|clip| clip.end)
1424 .max()
1425 .unwrap_or(0);
1426 audio_end.max(midi_end)
1427 })
1428 .max()
1429 .unwrap_or(0)
1430 }
1431
1432 async fn ensure_metronome_track(&mut self) {
1433 if self.state.lock().tracks.contains_key(Self::METRONOME_TRACK) {
1434 return;
1435 }
1436 let (cycle_samples, sample_rate_hz, output_channels): (usize, f64, usize) =
1437 if let Some(hw) = &self.hw_driver {
1438 let hw = hw.lock();
1439 (
1440 hw.cycle_samples(),
1441 hw.sample_rate() as f64,
1442 hw.output_channels(),
1443 )
1444 } else {
1445 #[cfg(unix)]
1446 {
1447 if let Some(jack) = &self.jack_runtime {
1448 let jack = jack.lock();
1449 (
1450 jack.buffer_size,
1451 jack.sample_rate as f64,
1452 jack.audio_outs().len(),
1453 )
1454 } else {
1455 return;
1456 }
1457 }
1458 #[cfg(not(unix))]
1459 {
1460 return;
1461 }
1462 };
1463 if output_channels == 0 {
1464 return;
1465 }
1466 self.state.lock().tracks.insert(
1467 Self::METRONOME_TRACK.to_string(),
1468 Arc::new(UnsafeMutex::new(Box::new(Track::new(
1469 Self::METRONOME_TRACK.to_string(),
1470 0,
1471 1,
1472 0,
1473 0,
1474 cycle_samples.max(1),
1475 sample_rate_hz.max(1.0),
1476 )))),
1477 );
1478 if let Some(track) = self.state.lock().tracks.get(Self::METRONOME_TRACK).cloned() {
1479 track.lock().set_level(Self::METRONOME_DEFAULT_LEVEL_DB);
1480 track.lock().set_metronome_enabled(self.metronome_enabled);
1481 }
1482 self.notify_clients(Ok(Action::AddTrack {
1483 name: Self::METRONOME_TRACK.to_string(),
1484 audio_ins: 0,
1485 midi_ins: 0,
1486 audio_outs: 1,
1487 midi_outs: 0,
1488 folder: false,
1489 }))
1490 .await;
1491 self.notify_clients(Ok(Action::TrackLevel(
1492 Self::METRONOME_TRACK.to_string(),
1493 Self::METRONOME_DEFAULT_LEVEL_DB,
1494 )))
1495 .await;
1496 }
1497
1498 fn open_hw_driver(
1499 device: &str,
1500 _input_device: Option<&str>,
1501 sample_rate_hz: i32,
1502 bits: i32,
1503 hw_opts: HwOptions,
1504 ) -> Result<HwDriver, String> {
1505 #[cfg(any(target_os = "windows", target_os = "freebsd", target_os = "linux"))]
1506 {
1507 HwDriver::new_with_options(device, _input_device, sample_rate_hz, bits, hw_opts)
1508 .map_err(|e| e.to_string())
1509 }
1510 #[cfg(target_os = "openbsd")]
1511 {
1512 HwDriver::new_with_options(device, sample_rate_hz, bits, hw_opts)
1513 .map_err(|e| e.to_string())
1514 }
1515 }
1516
1517 fn hw_profile_backend_label(_device: &str) -> &'static str {
1518 #[cfg(target_os = "windows")]
1519 let label = "WASAPI";
1520 #[cfg(target_os = "linux")]
1521 let label = "ALSA";
1522 #[cfg(target_os = "freebsd")]
1523 let label = "OSS";
1524 #[cfg(target_os = "openbsd")]
1525 let label = "sndio";
1526 #[cfg(target_os = "macos")]
1527 let label = "CoreAudio";
1528 label
1529 }
1530
1531 #[cfg(target_os = "freebsd")]
1532 fn maybe_start_freebsd_sync_group(&self) {
1533 if let Some(oss) = &self.hw_driver {
1534 let in_fd = oss.lock().input_fd();
1535 let out_fd = oss.lock().output_fd();
1536 let mut group = 0;
1537 let in_group = hw::add_to_sync_group(in_fd, group, true);
1538 if in_group > 0 {
1539 group = in_group;
1540 }
1541 let out_group = hw::add_to_sync_group(out_fd, group, false);
1542 if out_group > 0 {
1543 group = out_group;
1544 }
1545 let sync_started = if group > 0 {
1546 hw::start_sync_group(in_fd, group).is_ok()
1547 } else {
1548 false
1549 };
1550 if !sync_started {
1551 let _ = oss.lock().start_input_trigger();
1552 let _ = oss.lock().start_output_trigger();
1553 }
1554 }
1555 }
1556
1557 #[cfg(not(target_os = "freebsd"))]
1558 fn maybe_start_freebsd_sync_group(&self) {}
1559
1560 async fn open_discovered_midi_hw_devices(&mut self) {
1561 for device in Self::discover_midi_hw_devices() {
1562 let (opened_in, opened_out) = {
1563 let midi_hub = self.midi_hub.lock();
1564 let opened_in = midi_hub.open_input(&device).is_ok();
1565 let opened_out = midi_hub.open_output(&device).is_ok();
1566 (opened_in, opened_out)
1567 };
1568
1569 if opened_in {
1570 self.notify_clients(Ok(Action::OpenMidiInputDevice(device.clone())))
1571 .await;
1572 }
1573 if opened_out {
1574 self.notify_clients(Ok(Action::OpenMidiOutputDevice(device.clone())))
1575 .await;
1576 }
1577 }
1578 }
1579
1580 #[cfg(unix)]
1581 async fn maybe_open_jack_runtime(&mut self, request: AudioOpenRequest<'_>) -> Option<()> {
1582 if !request.device.eq_ignore_ascii_case("jack") {
1583 return None;
1584 }
1585 match JackRuntime::new(
1586 "maolan",
1587 crate::hw::jack::Config::default(),
1588 self.tx.clone(),
1589 ) {
1590 Ok(runtime) => {
1591 let input_channels = runtime.input_channels();
1592 let output_channels = runtime.output_channels();
1593 let midi_inputs = runtime.midi_input_devices();
1594 let midi_outputs = runtime.midi_output_devices();
1595 let rate = runtime.sample_rate;
1596 if let Some(worker) = self.hw_worker.take() {
1597 if let Some(hw) = &self.hw_driver {
1598 hw.lock().request_stop();
1599 }
1600 let _ = worker.tx.send(Message::Request(Action::Quit)).await;
1601 let _ = worker.handle.await;
1602 }
1603 self.hw_driver = None;
1604 self.jack_runtime = Some(Arc::new(UnsafeMutex::new(runtime)));
1605 self.publish_hw_infos(input_channels, output_channels, rate)
1606 .await;
1607 for device in midi_inputs {
1608 self.notify_clients(Ok(Action::OpenMidiInputDevice(device)))
1609 .await;
1610 }
1611 for device in midi_outputs {
1612 self.notify_clients(Ok(Action::OpenMidiOutputDevice(device)))
1613 .await;
1614 }
1615 self.notify_clients(Ok(Action::OpenAudioDevice {
1616 device: request.device.to_string(),
1617 input_device: request.input_device.map(ToOwned::to_owned),
1618 sample_rate_hz: request.sample_rate_hz,
1619 bits: request.bits,
1620 exclusive: request.exclusive,
1621 period_frames: request.period_frames,
1622 nperiods: request.nperiods,
1623 sync_mode: request.sync_mode,
1624 actual_period_frames: request.period_frames,
1625 input_channels,
1626 output_channels,
1627 bytes_per_frame: 0,
1628 }))
1629 .await;
1630 self.awaiting_hwfinished = true;
1631 }
1632 Err(e) => {
1633 error!("Failed to open JACK runtime: {e}");
1634 self.notify_clients(Err(e)).await;
1635 }
1636 }
1637 Some(())
1638 }
1639
1640 fn hw_driver_input_audio_port(&self, from_port: usize) -> Option<Arc<AudioIO>> {
1641 self.hw_driver
1642 .as_ref()
1643 .and_then(|h| h.lock().input_port(from_port))
1644 }
1645
1646 fn hw_driver_output_audio_port(&self, to_port: usize) -> Option<Arc<AudioIO>> {
1647 self.hw_driver
1648 .as_ref()
1649 .and_then(|h| h.lock().output_port(to_port))
1650 }
1651
1652 #[cfg(unix)]
1653 fn jack_input_audio_port(&self, from_port: usize) -> Option<Arc<AudioIO>> {
1654 self.jack_runtime
1655 .as_ref()
1656 .and_then(|j| j.lock().input_audio_port(from_port))
1657 }
1658
1659 #[cfg(not(unix))]
1660 fn jack_input_audio_port(&self, _from_port: usize) -> Option<Arc<AudioIO>> {
1661 None
1662 }
1663
1664 #[cfg(unix)]
1665 fn jack_output_audio_port(&self, to_port: usize) -> Option<Arc<AudioIO>> {
1666 self.jack_runtime
1667 .as_ref()
1668 .and_then(|j| j.lock().output_audio_port(to_port))
1669 }
1670
1671 #[cfg(not(unix))]
1672 fn jack_output_audio_port(&self, _to_port: usize) -> Option<Arc<AudioIO>> {
1673 None
1674 }
1675
1676 fn normalize_transport_sample(&self, sample: usize) -> usize {
1677 if self.loop_enabled
1678 && let Some((loop_start, loop_end)) = self.loop_range_samples
1679 && loop_end > loop_start
1680 && sample >= loop_end
1681 {
1682 let loop_len = loop_end - loop_start;
1683 return loop_start + (sample - loop_start) % loop_len;
1684 }
1685 sample
1686 }
1687
1688 fn scheduled_loop_wrap_for_next_cycle(&self) -> Option<(usize, usize, usize)> {
1689 if !self.playing || !self.loop_enabled {
1690 return None;
1691 }
1692 let (loop_start, loop_end) = self.loop_range_samples?;
1693 if loop_end <= loop_start || self.transport_sample >= loop_end {
1694 return None;
1695 }
1696 let cycle_samples = self.current_cycle_samples();
1697 if cycle_samples == 0 {
1698 return None;
1699 }
1700 let next = self.transport_sample.saturating_add(cycle_samples);
1701 if next < loop_end {
1702 return None;
1703 }
1704 let after_frames = loop_end.saturating_sub(self.transport_sample);
1705 Some((
1706 after_frames,
1707 loop_start,
1708 self.normalize_transport_sample(next),
1709 ))
1710 }
1711
1712 #[cfg(unix)]
1713 fn jack_transport_sync_decision(
1714 current_playing: bool,
1715 current_sample: usize,
1716 jack_playing: bool,
1717 normalized_frame: usize,
1718 cycle_samples: usize,
1719 ) -> JackTransportSyncDecision {
1720 let play_sync = match (current_playing, jack_playing) {
1721 (false, true) => Some(JackTransportPlaySync::Start),
1722 (true, false) => Some(JackTransportPlaySync::Stop),
1723 _ => None,
1724 };
1725 let position_drift = normalized_frame.abs_diff(current_sample);
1726 let position_changed = normalized_frame != current_sample;
1727 let should_sync_position = position_changed
1728 && (!jack_playing || play_sync.is_some() || position_drift > cycle_samples.max(1));
1729
1730 JackTransportSyncDecision {
1731 play_sync,
1732 position_sync: should_sync_position.then_some(normalized_frame),
1733 }
1734 }
1735
1736 #[cfg(unix)]
1737 async fn sync_from_jack_transport(&mut self) {
1738 let Some(jack) = self.jack_runtime.clone() else {
1739 return;
1740 };
1741 let Ok((jack_state, jack_frame)) = jack.lock().transport_state_and_frame() else {
1742 return;
1743 };
1744
1745 let jack_playing = matches!(
1746 jack_state,
1747 jack::TransportState::Rolling | jack::TransportState::Starting
1748 );
1749 let normalized_frame = self.normalize_transport_sample(jack_frame);
1750 let decision = Self::jack_transport_sync_decision(
1751 self.playing,
1752 self.transport_sample,
1753 jack_playing,
1754 normalized_frame,
1755 self.current_cycle_samples(),
1756 );
1757
1758 if let Some(play_sync) = decision.play_sync {
1759 self.playing = matches!(play_sync, JackTransportPlaySync::Start);
1760 self.transport_running = self.playing;
1761 if matches!(play_sync, JackTransportPlaySync::Start) {
1762 self.transport_restart_pending = false;
1763 self.transport_panic_flush_pending = false;
1764 self.invalidate_track_cycle_state();
1765 self.notify_clients(Ok(Action::Play)).await;
1766 } else {
1767 self.transport_panic_flush_pending = false;
1768 self.transport_restart_pending = false;
1769 let panic_events = self.note_off_events_for_all_active_tracks();
1770 self.pending_hw_midi_out_events_by_device
1771 .extend(panic_events);
1772 self.flush_recordings().await;
1773 self.notify_clients(Ok(Action::Stop)).await;
1774 }
1775 }
1776
1777 if let Some(sample) = decision.position_sync {
1778 self.transport_sample = sample;
1779 self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
1780 .await;
1781 }
1782 }
1783
1784 fn cycle_segments(&self, frames: usize) -> Vec<(usize, usize, usize)> {
1785 if frames == 0 {
1786 return vec![];
1787 }
1788 if !self.loop_enabled {
1789 return vec![(
1790 self.transport_sample,
1791 self.transport_sample.saturating_add(frames),
1792 0,
1793 )];
1794 }
1795 let Some((loop_start, loop_end)) = self.loop_range_samples else {
1796 return vec![(
1797 self.transport_sample,
1798 self.transport_sample.saturating_add(frames),
1799 0,
1800 )];
1801 };
1802 if loop_end <= loop_start {
1803 return vec![(
1804 self.transport_sample,
1805 self.transport_sample.saturating_add(frames),
1806 0,
1807 )];
1808 }
1809 let mut segments = Vec::new();
1810 let mut remaining = frames;
1811 let mut out_offset = 0usize;
1812 let mut current = self.transport_sample;
1813 while remaining > 0 {
1814 let take = loop_end.saturating_sub(current).min(remaining);
1815 if take == 0 {
1816 current = loop_start;
1817 continue;
1818 }
1819 segments.push((current, current.saturating_add(take), out_offset));
1820 out_offset = out_offset.saturating_add(take);
1821 remaining -= take;
1822 current = if remaining > 0 {
1823 loop_start
1824 } else {
1825 current.saturating_add(take)
1826 };
1827 }
1828 segments
1829 }
1830
1831 fn recording_segments_for_cycle(&self, frames: usize) -> Vec<(usize, usize, usize)> {
1832 let segments = self.cycle_segments(frames);
1833 let comp = self.hw_input_latency_frames;
1834 let segments: Vec<_> = if comp > 0 {
1835 segments
1836 .into_iter()
1837 .map(|(start, end, offset)| {
1838 (start.saturating_sub(comp), end.saturating_sub(comp), offset)
1839 })
1840 .collect()
1841 } else {
1842 segments
1843 };
1844 if !self.punch_enabled {
1845 return segments;
1846 }
1847 let Some((punch_start, punch_end)) = self.punch_range_samples else {
1848 return vec![];
1849 };
1850 if punch_end <= punch_start {
1851 return vec![];
1852 }
1853 let mut clipped = Vec::new();
1854 for (segment_start, segment_end, frame_offset) in segments {
1855 let start = segment_start.max(punch_start);
1856 let end = segment_end.min(punch_end);
1857 if end <= start {
1858 continue;
1859 }
1860 let clipped_offset = frame_offset.saturating_add(start.saturating_sub(segment_start));
1861 clipped.push((start, end, clipped_offset));
1862 }
1863 clipped
1864 }
1865
1866 fn hw_device_info<D: HwDevice>(d: &D) -> HwDeviceInfo {
1867 (
1868 d.input_channels(),
1869 d.output_channels(),
1870 d.sample_rate() as usize,
1871 d.latency_ranges(),
1872 )
1873 }
1874
1875 async fn publish_hw_infos(
1876 &mut self,
1877 input_channels: usize,
1878 output_channels: usize,
1879 rate: usize,
1880 ) {
1881 self.notify_clients(Ok(Action::HWInfo {
1882 channels: input_channels,
1883 rate,
1884 input: true,
1885 }))
1886 .await;
1887 self.notify_clients(Ok(Action::HWInfo {
1888 channels: output_channels,
1889 rate,
1890 input: false,
1891 }))
1892 .await;
1893 }
1894
1895 #[cfg(unix)]
1896 fn jack_runtime_is_some(&self) -> bool {
1897 self.jack_runtime.is_some()
1898 }
1899
1900 #[cfg(not(unix))]
1901 fn jack_runtime_is_some(&self) -> bool {
1902 false
1903 }
1904
1905 fn can_schedule_hw_cycle(&self) -> bool {
1906 self.playing && (self.hw_worker.is_some() || self.jack_runtime_is_some())
1907 }
1908
1909 async fn ensure_hw_worker_running(&mut self) {
1910 if self.hw_worker.is_some() || self.hw_driver.is_none() {
1911 return;
1912 }
1913 let (tx, rx) = channel::<Message>(32);
1914 let hw = self.hw_driver.clone().unwrap();
1915 let midi_hub = self.midi_hub.clone();
1916 let tx_engine = self.tx.clone();
1917 let handler = tokio::spawn(async move {
1918 let worker = HwWorker::new(hw, midi_hub, rx, tx_engine);
1919 worker.work().await;
1920 });
1921 self.hw_worker = Some(WorkerData::new(tx, handler));
1922 }
1923
1924 fn build_hw_options(
1925 exclusive: bool,
1926 period_frames: usize,
1927 nperiods: usize,
1928 sync_mode: bool,
1929 ) -> HwOptions {
1930 HwOptions {
1931 exclusive,
1932 period_frames: period_frames.max(1).next_power_of_two(),
1933 nperiods: nperiods.max(1),
1934 sync_mode,
1935 ..Default::default()
1936 }
1937 }
1938
1939 async fn open_non_jack_audio_device(
1940 &mut self,
1941 device: &str,
1942 input_device: Option<&str>,
1943 sample_rate_hz: i32,
1944 bits: i32,
1945 hw_opts: HwOptions,
1946 ) -> Result<(), String> {
1947 let hw_profile_enabled = config::env_flag(config::HW_PROFILE_ENV);
1948 let d = Self::open_hw_driver(device, input_device, sample_rate_hz, bits, hw_opts)?;
1949 let (in_channels, out_channels, rate, (in_lat, out_lat)) = Self::hw_device_info(&d);
1950 if hw_profile_enabled {
1951 let label = Self::hw_profile_backend_label(device);
1952 error!(
1953 "{} config: exclusive={}, period={}, nperiods={}, ignore_hwbuf={}, sync_mode={}, in_latency_extra={}, out_latency_extra={}, input_range={:?}, output_range={:?}",
1954 label,
1955 hw_opts.exclusive,
1956 hw_opts.period_frames,
1957 hw_opts.nperiods,
1958 hw_opts.ignore_hwbuf,
1959 hw_opts.sync_mode,
1960 hw_opts.input_latency_frames,
1961 hw_opts.output_latency_frames,
1962 in_lat,
1963 out_lat
1964 );
1965 }
1966 self.hw_input_latency_frames = in_lat.0;
1967 self.hw_output_latency_frames = out_lat.0;
1968 #[cfg(unix)]
1969 {
1970 self.jack_runtime = None;
1971 }
1972 self.hw_driver = Some(Arc::new(UnsafeMutex::new(d)));
1973 self.publish_hw_infos(in_channels, out_channels, rate).await;
1974 Ok(())
1975 }
1976
1977 async fn finalize_open_audio_device(&mut self) {
1978 self.maybe_start_freebsd_sync_group();
1979 if self.metronome_enabled {
1980 self.ensure_metronome_track().await;
1981 }
1982 if self.hw_worker.is_none() && self.hw_driver.is_some() {
1983 self.ensure_hw_worker_running().await;
1984 self.request_hw_cycle().await;
1985 }
1986 self.open_discovered_midi_hw_devices().await;
1987 }
1988
1989 fn hw_input_audio_port(&self, from_port: usize) -> Option<Arc<AudioIO>> {
1990 self.hw_driver_input_audio_port(from_port)
1991 .or_else(|| self.jack_input_audio_port(from_port))
1992 }
1993
1994 fn hw_output_audio_port(&self, to_port: usize) -> Option<Arc<AudioIO>> {
1995 self.hw_driver_output_audio_port(to_port)
1996 .or_else(|| self.jack_output_audio_port(to_port))
1997 }
1998
1999 fn all_hw_output_audio_ports(&self) -> Vec<Arc<AudioIO>> {
2000 if let Some(driver) = &self.hw_driver {
2001 let count = driver.lock().output_channels();
2002 return (0..count)
2003 .filter_map(|idx| self.hw_driver_output_audio_port(idx))
2004 .collect();
2005 }
2006 #[cfg(unix)]
2007 if let Some(jack) = &self.jack_runtime {
2008 return jack.lock().audio_outs();
2009 }
2010 Vec::new()
2011 }
2012
2013 fn all_hw_input_audio_ports(&self) -> Vec<Arc<AudioIO>> {
2014 if let Some(driver) = &self.hw_driver {
2015 let count = driver.lock().input_channels();
2016 return (0..count)
2017 .filter_map(|idx| self.hw_driver_input_audio_port(idx))
2018 .collect();
2019 }
2020 #[cfg(unix)]
2021 if let Some(jack) = &self.jack_runtime {
2022 return jack.lock().audio_ins();
2023 }
2024 Vec::new()
2025 }
2026
2027 #[cfg(unix)]
2028 fn audio_ports_connected(source: &Arc<AudioIO>, target: &Arc<AudioIO>) -> bool {
2029 source
2030 .connections
2031 .lock()
2032 .iter()
2033 .any(|conn| Arc::ptr_eq(conn, target))
2034 }
2035
2036 fn resolve_audio_route_ports(
2037 &self,
2038 from_track: &str,
2039 from_port: usize,
2040 to_track: &str,
2041 to_port: usize,
2042 ) -> (Option<Arc<AudioIO>>, Option<Arc<AudioIO>>) {
2043 let state = self.state.lock();
2044 let from_is_child_of_to = state
2045 .tracks
2046 .get(from_track)
2047 .and_then(|t| t.lock().parent_track.as_deref())
2048 == Some(to_track);
2049 let to_is_child_of_from = state
2050 .tracks
2051 .get(to_track)
2052 .and_then(|t| t.lock().parent_track.as_deref())
2053 == Some(from_track);
2054
2055 let from_audio_io = if from_track == "hw:in" {
2056 self.hw_input_audio_port(from_port)
2057 } else {
2058 state.tracks.get(from_track).and_then(|t| {
2059 let t = t.lock();
2060 if t.is_folder {
2061 if to_is_child_of_from {
2062 t.audio.ins.get(from_port).cloned()
2064 } else {
2065 t.audio.outs.get(from_port).cloned()
2067 }
2068 } else {
2069 t.audio.outs.get(from_port).cloned()
2070 }
2071 })
2072 };
2073 let to_audio_io = if to_track == "hw:out" {
2074 self.hw_output_audio_port(to_port)
2075 } else {
2076 state.tracks.get(to_track).and_then(|t| {
2077 let t = t.lock();
2078 if t.is_folder {
2079 if from_is_child_of_to {
2080 t.audio.outs.get(to_port).cloned()
2082 } else {
2083 t.audio.ins.get(to_port).cloned()
2085 }
2086 } else {
2087 t.audio.ins.get(to_port).cloned()
2088 }
2089 })
2090 };
2091 (from_audio_io, to_audio_io)
2092 }
2093
2094 async fn disconnect_audio_route_and_notify(&mut self, action: Action) -> Result<(), String> {
2095 let Action::Disconnect {
2096 from_track,
2097 from_port,
2098 to_track,
2099 to_port,
2100 kind,
2101 } = &action
2102 else {
2103 return Err("disconnect_audio_route_and_notify requires Disconnect action".to_string());
2104 };
2105 if *kind != Kind::Audio {
2106 return Err("disconnect_audio_route_and_notify only supports audio routes".to_string());
2107 }
2108 let (from_audio_io, to_audio_io) =
2109 self.resolve_audio_route_ports(from_track, *from_port, to_track, *to_port);
2110 match (from_audio_io, to_audio_io) {
2111 (Some(source), Some(target)) => {
2112 crate::audio::io::AudioIO::disconnect(&source, &target)
2113 .map_err(|e| format!("Disconnect failed: {e}"))?;
2114 self.notify_clients(Ok(action)).await;
2115 Ok(())
2116 }
2117 _ => Err(format!(
2118 "Disconnect failed: Port not found ({} -> {})",
2119 from_track, to_track
2120 )),
2121 }
2122 }
2123
2124 #[cfg(unix)]
2125 fn disconnect_actions_for_removed_hw_input(
2126 &self,
2127 removed_port: usize,
2128 removed_io: &Arc<AudioIO>,
2129 ) -> Vec<Action> {
2130 let mut actions = Vec::new();
2131 {
2132 let state = self.state.lock();
2133 for (track_name, track) in &state.tracks {
2134 let track = track.lock();
2135 for (to_port, target) in track.audio.ins.iter().enumerate() {
2136 if Self::audio_ports_connected(removed_io, target) {
2137 actions.push(Action::Disconnect {
2138 from_track: "hw:in".to_string(),
2139 from_port: removed_port,
2140 to_track: track_name.clone(),
2141 to_port,
2142 kind: Kind::Audio,
2143 });
2144 }
2145 }
2146 }
2147 }
2148 for (to_port, target) in self.all_hw_output_audio_ports().into_iter().enumerate() {
2149 if Self::audio_ports_connected(removed_io, &target) {
2150 actions.push(Action::Disconnect {
2151 from_track: "hw:in".to_string(),
2152 from_port: removed_port,
2153 to_track: "hw:out".to_string(),
2154 to_port,
2155 kind: Kind::Audio,
2156 });
2157 }
2158 }
2159 actions
2160 }
2161
2162 #[cfg(unix)]
2163 fn disconnect_actions_for_removed_hw_output(
2164 &self,
2165 removed_port: usize,
2166 removed_io: &Arc<AudioIO>,
2167 ) -> Vec<Action> {
2168 let mut actions = Vec::new();
2169 {
2170 let state = self.state.lock();
2171 for (track_name, track) in &state.tracks {
2172 let track = track.lock();
2173 for (from_port, source) in track.audio.outs.iter().enumerate() {
2174 if Self::audio_ports_connected(source, removed_io) {
2175 actions.push(Action::Disconnect {
2176 from_track: track_name.clone(),
2177 from_port,
2178 to_track: "hw:out".to_string(),
2179 to_port: removed_port,
2180 kind: Kind::Audio,
2181 });
2182 }
2183 }
2184 }
2185 }
2186 #[cfg(unix)]
2187 if let Some(jack) = &self.jack_runtime {
2188 for (from_port, source) in jack.lock().audio_ins().into_iter().enumerate() {
2189 if Self::audio_ports_connected(&source, removed_io) {
2190 actions.push(Action::Disconnect {
2191 from_track: "hw:in".to_string(),
2192 from_port,
2193 to_track: "hw:out".to_string(),
2194 to_port: removed_port,
2195 kind: Kind::Audio,
2196 });
2197 }
2198 }
2199 }
2200 actions
2201 }
2202
2203 #[cfg(unix)]
2204 fn reindex_notifications_for_removed_hw_input(&self, removed_port: usize) -> Vec<Action> {
2205 let mut actions = Vec::new();
2206 #[cfg(unix)]
2207 if let Some(jack) = &self.jack_runtime {
2208 let jack = jack.lock();
2209 for from_port in (removed_port + 1)..jack.input_channels() {
2210 let Some(source) = jack.input_audio_port(from_port) else {
2211 continue;
2212 };
2213 {
2214 let state = self.state.lock();
2215 for (track_name, track) in &state.tracks {
2216 let track = track.lock();
2217 for (to_port, target) in track.audio.ins.iter().enumerate() {
2218 if Self::audio_ports_connected(&source, target) {
2219 actions.push(Action::Disconnect {
2220 from_track: "hw:in".to_string(),
2221 from_port,
2222 to_track: track_name.clone(),
2223 to_port,
2224 kind: Kind::Audio,
2225 });
2226 actions.push(Action::Connect {
2227 from_track: "hw:in".to_string(),
2228 from_port: from_port - 1,
2229 to_track: track_name.clone(),
2230 to_port,
2231 kind: Kind::Audio,
2232 });
2233 }
2234 }
2235 }
2236 }
2237 for (to_port, target) in self.all_hw_output_audio_ports().into_iter().enumerate() {
2238 if Self::audio_ports_connected(&source, &target) {
2239 actions.push(Action::Disconnect {
2240 from_track: "hw:in".to_string(),
2241 from_port,
2242 to_track: "hw:out".to_string(),
2243 to_port,
2244 kind: Kind::Audio,
2245 });
2246 actions.push(Action::Connect {
2247 from_track: "hw:in".to_string(),
2248 from_port: from_port - 1,
2249 to_track: "hw:out".to_string(),
2250 to_port,
2251 kind: Kind::Audio,
2252 });
2253 }
2254 }
2255 }
2256 }
2257 actions
2258 }
2259
2260 #[cfg(unix)]
2261 fn reindex_notifications_for_removed_hw_output(&self, removed_port: usize) -> Vec<Action> {
2262 let mut actions = Vec::new();
2263 #[cfg(unix)]
2264 if let Some(jack) = &self.jack_runtime {
2265 let jack = jack.lock();
2266 for to_port in (removed_port + 1)..jack.output_channels() {
2267 let Some(target) = jack.output_audio_port(to_port) else {
2268 continue;
2269 };
2270 {
2271 let state = self.state.lock();
2272 for (track_name, track) in &state.tracks {
2273 let track = track.lock();
2274 for (from_port, source) in track.audio.outs.iter().enumerate() {
2275 if Self::audio_ports_connected(source, &target) {
2276 actions.push(Action::Disconnect {
2277 from_track: track_name.clone(),
2278 from_port,
2279 to_track: "hw:out".to_string(),
2280 to_port,
2281 kind: Kind::Audio,
2282 });
2283 actions.push(Action::Connect {
2284 from_track: track_name.clone(),
2285 from_port,
2286 to_track: "hw:out".to_string(),
2287 to_port: to_port - 1,
2288 kind: Kind::Audio,
2289 });
2290 }
2291 }
2292 }
2293 }
2294 for (from_port, source) in jack.audio_ins().into_iter().enumerate() {
2295 if Self::audio_ports_connected(&source, &target) {
2296 actions.push(Action::Disconnect {
2297 from_track: "hw:in".to_string(),
2298 from_port,
2299 to_track: "hw:out".to_string(),
2300 to_port,
2301 kind: Kind::Audio,
2302 });
2303 actions.push(Action::Connect {
2304 from_track: "hw:in".to_string(),
2305 from_port,
2306 to_track: "hw:out".to_string(),
2307 to_port: to_port - 1,
2308 kind: Kind::Audio,
2309 });
2310 }
2311 }
2312 }
2313 }
2314 actions
2315 }
2316
2317 fn midi_hw_in_device(track: &str) -> Option<&str> {
2318 track.strip_prefix("midi:hw:in:")
2319 }
2320
2321 fn midi_hw_out_device(track: &str) -> Option<&str> {
2322 track.strip_prefix("midi:hw:out:")
2323 }
2324
2325 fn midi_binding_matches(
2326 a: &crate::message::MidiLearnBinding,
2327 b: &crate::message::MidiLearnBinding,
2328 ) -> bool {
2329 if a.channel != b.channel || a.cc != b.cc {
2330 return false;
2331 }
2332 match (&a.device, &b.device) {
2333 (Some(ad), Some(bd)) => ad == bd,
2334 _ => true,
2335 }
2336 }
2337
2338 fn midi_learn_slot_conflicts(
2339 &self,
2340 binding: &crate::message::MidiLearnBinding,
2341 ignore: Option<MidiLearnSlot>,
2342 ) -> Vec<String> {
2343 let mut conflicts = Vec::<String>::new();
2344 let state = self.state.lock();
2345 let mut push_conflict = |slot: MidiLearnSlot, label: String| {
2346 if ignore.as_ref().is_some_and(|i| i == &slot) {
2347 return;
2348 }
2349 conflicts.push(label);
2350 };
2351 let check_global =
2352 |current: &Option<crate::message::MidiLearnBinding>,
2353 target: crate::message::GlobalMidiLearnTarget,
2354 label: &str,
2355 push_conflict: &mut dyn FnMut(MidiLearnSlot, String)| {
2356 if let Some(existing) = current
2357 && Self::midi_binding_matches(binding, existing)
2358 {
2359 push_conflict(MidiLearnSlot::Global(target), format!("Global {label}"));
2360 }
2361 };
2362 check_global(
2363 &self.global_midi_learn_play_pause,
2364 crate::message::GlobalMidiLearnTarget::PlayPause,
2365 "PlayPause",
2366 &mut push_conflict,
2367 );
2368 check_global(
2369 &self.global_midi_learn_stop,
2370 crate::message::GlobalMidiLearnTarget::Stop,
2371 "Stop",
2372 &mut push_conflict,
2373 );
2374 check_global(
2375 &self.global_midi_learn_record_toggle,
2376 crate::message::GlobalMidiLearnTarget::RecordToggle,
2377 "RecordToggle",
2378 &mut push_conflict,
2379 );
2380 for (track_name, track) in state.tracks.iter() {
2381 let t = track.lock();
2382 let mut check_track = |current: &Option<crate::message::MidiLearnBinding>,
2383 target: crate::message::TrackMidiLearnTarget,
2384 label: &str| {
2385 if let Some(existing) = current
2386 && Self::midi_binding_matches(binding, existing)
2387 {
2388 push_conflict(
2389 MidiLearnSlot::Track(track_name.clone(), target),
2390 format!("{track_name} {label}"),
2391 );
2392 }
2393 };
2394 check_track(
2395 &t.midi_learn_volume,
2396 crate::message::TrackMidiLearnTarget::Volume,
2397 "Volume",
2398 );
2399 check_track(
2400 &t.midi_learn_balance,
2401 crate::message::TrackMidiLearnTarget::Balance,
2402 "Balance",
2403 );
2404 check_track(
2405 &t.midi_learn_mute,
2406 crate::message::TrackMidiLearnTarget::Mute,
2407 "Mute",
2408 );
2409 check_track(
2410 &t.midi_learn_solo,
2411 crate::message::TrackMidiLearnTarget::Solo,
2412 "Solo",
2413 );
2414 check_track(
2415 &t.midi_learn_arm,
2416 crate::message::TrackMidiLearnTarget::Arm,
2417 "Arm",
2418 );
2419 check_track(
2420 &t.midi_learn_input_monitor,
2421 crate::message::TrackMidiLearnTarget::InputMonitor,
2422 "InputMonitor",
2423 );
2424 check_track(
2425 &t.midi_learn_disk_monitor,
2426 crate::message::TrackMidiLearnTarget::DiskMonitor,
2427 "DiskMonitor",
2428 );
2429 }
2430 for (key, existing) in &self.session_midi_learn_slots {
2431 if Self::midi_binding_matches(binding, existing) {
2432 push_conflict(
2433 MidiLearnSlot::Session(crate::message::SessionMidiLearnTarget::Slot {
2434 track_name: key.0.clone(),
2435 scene_index: key.1,
2436 }),
2437 format!("{} Slot {}", key.0, key.1 + 1),
2438 );
2439 }
2440 }
2441 for (scene_index, existing) in &self.session_midi_learn_scenes {
2442 if Self::midi_binding_matches(binding, existing) {
2443 push_conflict(
2444 MidiLearnSlot::Session(crate::message::SessionMidiLearnTarget::Scene(
2445 *scene_index,
2446 )),
2447 format!("Scene {}", scene_index + 1),
2448 );
2449 }
2450 }
2451 for (track_name, existing) in &self.session_midi_learn_stop_track {
2452 if Self::midi_binding_matches(binding, existing) {
2453 push_conflict(
2454 MidiLearnSlot::Session(crate::message::SessionMidiLearnTarget::StopTrack(
2455 track_name.clone(),
2456 )),
2457 format!("{track_name} Stop"),
2458 );
2459 }
2460 }
2461 if let Some(existing) = self.session_midi_learn_stop_all.as_ref()
2462 && Self::midi_binding_matches(binding, existing)
2463 {
2464 push_conflict(
2465 MidiLearnSlot::Session(crate::message::SessionMidiLearnTarget::StopAll),
2466 "Stop All Clips".to_string(),
2467 );
2468 }
2469 conflicts
2470 }
2471
2472 async fn handle_incoming_hw_cc(&mut self, device: &str, channel: u8, cc: u8, value: u8) {
2473 let gate_key = (device.to_string(), channel, cc);
2474 let high = value >= 64;
2475 let prev_high = self.midi_cc_gate.get(&gate_key).copied().unwrap_or(false);
2476 self.midi_cc_gate.insert(gate_key, high);
2477 let rising = high && !prev_high;
2478
2479 if let Some((track_name, target, armed_device)) = self.pending_midi_learn.clone() {
2480 let binding = crate::message::MidiLearnBinding {
2481 device: armed_device.or(Some(device.to_string())),
2482 channel,
2483 cc,
2484 };
2485 let conflicts = self.midi_learn_slot_conflicts(
2486 &binding,
2487 Some(MidiLearnSlot::Track(track_name.clone(), target)),
2488 );
2489 if !conflicts.is_empty() {
2490 self.pending_midi_learn = None;
2491 self.notify_clients(Err(format!(
2492 "MIDI learn conflict for '{}' {:?}: {}",
2493 track_name,
2494 target,
2495 conflicts.join(", ")
2496 )))
2497 .await;
2498 return;
2499 }
2500 if let Some(track) = self.state.lock().tracks.get(&track_name) {
2501 match target {
2502 crate::message::TrackMidiLearnTarget::Volume => {
2503 track.lock().midi_learn_volume = Some(binding.clone());
2504 }
2505 crate::message::TrackMidiLearnTarget::Balance => {
2506 track.lock().midi_learn_balance = Some(binding.clone());
2507 }
2508 crate::message::TrackMidiLearnTarget::Mute => {
2509 track.lock().midi_learn_mute = Some(binding.clone());
2510 }
2511 crate::message::TrackMidiLearnTarget::Solo => {
2512 track.lock().midi_learn_solo = Some(binding.clone());
2513 }
2514 crate::message::TrackMidiLearnTarget::Arm => {
2515 track.lock().midi_learn_arm = Some(binding.clone());
2516 }
2517 crate::message::TrackMidiLearnTarget::InputMonitor => {
2518 track.lock().midi_learn_input_monitor = Some(binding.clone());
2519 }
2520 crate::message::TrackMidiLearnTarget::DiskMonitor => {
2521 track.lock().midi_learn_disk_monitor = Some(binding.clone());
2522 }
2523 }
2524 self.pending_midi_learn = None;
2525 self.notify_clients(Ok(Action::TrackSetMidiLearnBinding {
2526 track_name: track_name.clone(),
2527 target,
2528 binding: Some(binding),
2529 }))
2530 .await;
2531 } else {
2532 self.pending_midi_learn = None;
2533 }
2534 }
2535 if let Some(target) = self.pending_global_midi_learn.take() {
2536 let binding = crate::message::MidiLearnBinding {
2537 device: Some(device.to_string()),
2538 channel,
2539 cc,
2540 };
2541 let conflicts =
2542 self.midi_learn_slot_conflicts(&binding, Some(MidiLearnSlot::Global(target)));
2543 if !conflicts.is_empty() {
2544 self.notify_clients(Err(format!(
2545 "Global MIDI learn conflict for {:?}: {}",
2546 target,
2547 conflicts.join(", ")
2548 )))
2549 .await;
2550 return;
2551 }
2552 match target {
2553 crate::message::GlobalMidiLearnTarget::PlayPause => {
2554 self.global_midi_learn_play_pause = Some(binding.clone());
2555 }
2556 crate::message::GlobalMidiLearnTarget::Stop => {
2557 self.global_midi_learn_stop = Some(binding.clone());
2558 }
2559 crate::message::GlobalMidiLearnTarget::RecordToggle => {
2560 self.global_midi_learn_record_toggle = Some(binding.clone());
2561 }
2562 }
2563 self.notify_clients(Ok(Action::SetGlobalMidiLearnBinding {
2564 target,
2565 binding: Some(binding),
2566 }))
2567 .await;
2568 }
2569 if let Some(target) = self.pending_session_midi_learn.take() {
2570 let binding = crate::message::MidiLearnBinding {
2571 device: Some(device.to_string()),
2572 channel,
2573 cc,
2574 };
2575 let conflicts = self
2576 .midi_learn_slot_conflicts(&binding, Some(MidiLearnSlot::Session(target.clone())));
2577 if !conflicts.is_empty() {
2578 self.notify_clients(Err(format!(
2579 "Session MIDI learn conflict for {:?}: {}",
2580 target,
2581 conflicts.join(", ")
2582 )))
2583 .await;
2584 return;
2585 }
2586 match target {
2587 crate::message::SessionMidiLearnTarget::Slot {
2588 ref track_name,
2589 scene_index,
2590 } => {
2591 self.session_midi_learn_slots
2592 .insert((track_name.clone(), scene_index), binding.clone());
2593 }
2594 crate::message::SessionMidiLearnTarget::Scene(scene_index) => {
2595 self.session_midi_learn_scenes
2596 .insert(scene_index, binding.clone());
2597 }
2598 crate::message::SessionMidiLearnTarget::StopTrack(ref track_name) => {
2599 self.session_midi_learn_stop_track
2600 .insert(track_name.clone(), binding.clone());
2601 }
2602 crate::message::SessionMidiLearnTarget::StopAll => {
2603 self.session_midi_learn_stop_all = Some(binding.clone());
2604 }
2605 }
2606 self.notify_clients(Ok(Action::SetSessionMidiLearnBinding {
2607 target,
2608 binding: Some(binding),
2609 }))
2610 .await;
2611 }
2612
2613 let mut mapped_actions = Vec::<Action>::new();
2614 for (track_name, track) in self.state.lock().tracks.iter() {
2615 let t = track.lock();
2616 if let Some(binding) = t.midi_learn_volume.as_ref() {
2617 let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2618 if device_matches && binding.channel == channel && binding.cc == cc {
2619 let level = -90.0 + (value as f32 / 127.0) * 110.0;
2620 mapped_actions.push(Action::TrackLevel(track_name.clone(), level));
2621 }
2622 }
2623 if let Some(binding) = t.midi_learn_balance.as_ref() {
2624 let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2625 if device_matches && binding.channel == channel && binding.cc == cc {
2626 let balance = (value as f32 / 127.0) * 2.0 - 1.0;
2627 mapped_actions.push(Action::TrackBalance(track_name.clone(), balance));
2628 }
2629 }
2630 if let Some(binding) = t.midi_learn_mute.as_ref() {
2631 let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2632 if device_matches && binding.channel == channel && binding.cc == cc {
2633 let wanted = value >= 64;
2634 if t.muted != wanted {
2635 mapped_actions.push(Action::TrackToggleMute(track_name.clone()));
2636 }
2637 }
2638 }
2639 if let Some(binding) = t.midi_learn_solo.as_ref() {
2640 let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2641 if device_matches && binding.channel == channel && binding.cc == cc {
2642 let wanted = value >= 64;
2643 if t.soloed != wanted {
2644 mapped_actions.push(Action::TrackToggleSolo(track_name.clone()));
2645 }
2646 }
2647 }
2648 if let Some(binding) = t.midi_learn_arm.as_ref() {
2649 let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2650 if device_matches && binding.channel == channel && binding.cc == cc {
2651 let wanted = value >= 64;
2652 if t.armed != wanted {
2653 mapped_actions.push(Action::TrackToggleArm(track_name.clone()));
2654 }
2655 }
2656 }
2657 if let Some(binding) = t.midi_learn_input_monitor.as_ref() {
2658 let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2659 if device_matches && binding.channel == channel && binding.cc == cc {
2660 let wanted = value >= 64;
2661 if t.input_monitor.first() != Some(&wanted) {
2662 mapped_actions.push(Action::TrackToggleInputMonitor {
2663 track_name: track_name.clone(),
2664 lane: 0,
2665 });
2666 }
2667 }
2668 }
2669 if let Some(binding) = t.midi_learn_disk_monitor.as_ref() {
2670 let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2671 if device_matches && binding.channel == channel && binding.cc == cc {
2672 let wanted = value >= 64;
2673 if t.disk_monitor.first() != Some(&wanted) {
2674 mapped_actions.push(Action::TrackToggleDiskMonitor {
2675 track_name: track_name.clone(),
2676 lane: 0,
2677 });
2678 }
2679 }
2680 }
2681 }
2682 let device_matches =
2683 |binding: &crate::message::MidiLearnBinding| binding.device.as_deref() == Some(device);
2684 let mut mapped_global_actions = Vec::<Action>::new();
2685 if let Some(binding) = self.global_midi_learn_play_pause.as_ref()
2686 && device_matches(binding)
2687 && binding.channel == channel
2688 && binding.cc == cc
2689 && rising
2690 {
2691 mapped_global_actions.push(if self.playing {
2692 Action::Stop
2693 } else {
2694 Action::Play
2695 });
2696 }
2697 if let Some(binding) = self.global_midi_learn_stop.as_ref()
2698 && device_matches(binding)
2699 && binding.channel == channel
2700 && binding.cc == cc
2701 && rising
2702 && self.playing
2703 {
2704 mapped_global_actions.push(Action::Stop);
2705 }
2706 if let Some(binding) = self.global_midi_learn_record_toggle.as_ref()
2707 && device_matches(binding)
2708 && binding.channel == channel
2709 && binding.cc == cc
2710 && rising
2711 {
2712 mapped_global_actions.push(Action::SetRecordEnabled(!self.record_enabled));
2713 }
2714 if rising {
2715 for (key, binding) in &self.session_midi_learn_slots {
2716 let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2717 if device_matches && binding.channel == channel && binding.cc == cc {
2718 mapped_global_actions.push(Action::SessionMidiLearnTriggered {
2719 target: crate::message::SessionMidiLearnTarget::Slot {
2720 track_name: key.0.clone(),
2721 scene_index: key.1,
2722 },
2723 });
2724 }
2725 }
2726 for (scene_index, binding) in &self.session_midi_learn_scenes {
2727 let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2728 if device_matches && binding.channel == channel && binding.cc == cc {
2729 mapped_global_actions.push(Action::SessionMidiLearnTriggered {
2730 target: crate::message::SessionMidiLearnTarget::Scene(*scene_index),
2731 });
2732 }
2733 }
2734 for (track_name, binding) in &self.session_midi_learn_stop_track {
2735 let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2736 if device_matches && binding.channel == channel && binding.cc == cc {
2737 mapped_global_actions.push(Action::SessionMidiLearnTriggered {
2738 target: crate::message::SessionMidiLearnTarget::StopTrack(
2739 track_name.clone(),
2740 ),
2741 });
2742 }
2743 }
2744 if let Some(binding) = self.session_midi_learn_stop_all.as_ref() {
2745 let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2746 if device_matches && binding.channel == channel && binding.cc == cc {
2747 mapped_global_actions.push(Action::SessionMidiLearnTriggered {
2748 target: crate::message::SessionMidiLearnTarget::StopAll,
2749 });
2750 }
2751 }
2752 }
2753 for action in mapped_actions {
2754 match action {
2755 Action::TrackLevel(ref track_name, level) => {
2756 if let Some(track) = self.state.lock().tracks.get(track_name) {
2757 track.lock().set_level(level);
2758 self.notify_clients(Ok(Action::TrackLevel(track_name.clone(), level)))
2759 .await;
2760 }
2761 }
2762 Action::TrackBalance(ref track_name, balance) => {
2763 if let Some(track) = self.state.lock().tracks.get(track_name) {
2764 track.lock().set_balance(balance);
2765 self.notify_clients(Ok(Action::TrackBalance(track_name.clone(), balance)))
2766 .await;
2767 }
2768 }
2769 Action::TrackToggleMute(ref track_name) => {
2770 if let Some(track) = self.state.lock().tracks.get(track_name) {
2771 track.lock().mute();
2772 self.notify_clients(Ok(Action::TrackToggleMute(track_name.clone())))
2773 .await;
2774 }
2775 }
2776 Action::TrackTogglePhase(ref track_name) => {
2777 if let Some(track) = self.state.lock().tracks.get(track_name) {
2778 track.lock().invert_phase();
2779 self.notify_clients(Ok(Action::TrackTogglePhase(track_name.clone())))
2780 .await;
2781 }
2782 }
2783 Action::TrackToggleSolo(ref track_name) => {
2784 if let Some(track) = self.state.lock().tracks.get(track_name) {
2785 track.lock().solo();
2786 self.notify_clients(Ok(Action::TrackToggleSolo(track_name.clone())))
2787 .await;
2788 }
2789 }
2790 Action::TrackToggleMaster(ref track_name) => {
2791 if let Some(track) = self.state.lock().tracks.get(track_name) {
2792 let can_toggle = {
2793 let t = track.lock();
2794 t.is_master || !t.is_folder
2795 };
2796 if can_toggle {
2797 track.lock().toggle_master();
2798 self.notify_clients(Ok(Action::TrackToggleMaster(track_name.clone())))
2799 .await;
2800 }
2801 }
2802 }
2803 Action::TrackToggleArm(ref track_name) => {
2804 if let Some(track) = self.state.lock().tracks.get(track_name) {
2805 track.lock().arm();
2806 self.notify_clients(Ok(Action::TrackToggleArm(track_name.clone())))
2807 .await;
2808 }
2809 }
2810 Action::TrackToggleInputMonitor {
2811 ref track_name,
2812 lane,
2813 } => {
2814 if let Some(track) = self.state.lock().tracks.get(track_name) {
2815 track.lock().toggle_input_monitor(lane);
2816 self.notify_clients(Ok(Action::TrackToggleInputMonitor {
2817 track_name: track_name.clone(),
2818 lane,
2819 }))
2820 .await;
2821 }
2822 }
2823 Action::TrackToggleDiskMonitor {
2824 ref track_name,
2825 lane,
2826 } => {
2827 if let Some(track) = self.state.lock().tracks.get(track_name) {
2828 track.lock().toggle_disk_monitor(lane);
2829 self.notify_clients(Ok(Action::TrackToggleDiskMonitor {
2830 track_name: track_name.clone(),
2831 lane,
2832 }))
2833 .await;
2834 }
2835 }
2836 Action::TrackToggleMidiInputMonitor {
2837 ref track_name,
2838 lane,
2839 } => {
2840 if let Some(track) = self.state.lock().tracks.get(track_name) {
2841 track.lock().toggle_midi_input_monitor(lane);
2842 self.notify_clients(Ok(Action::TrackToggleMidiInputMonitor {
2843 track_name: track_name.clone(),
2844 lane,
2845 }))
2846 .await;
2847 }
2848 }
2849 Action::TrackToggleMidiDiskMonitor {
2850 ref track_name,
2851 lane,
2852 } => {
2853 if let Some(track) = self.state.lock().tracks.get(track_name) {
2854 track.lock().toggle_midi_disk_monitor(lane);
2855 self.notify_clients(Ok(Action::TrackToggleMidiDiskMonitor {
2856 track_name: track_name.clone(),
2857 lane,
2858 }))
2859 .await;
2860 }
2861 }
2862 _ => {}
2863 }
2864 }
2865 for action in mapped_global_actions {
2866 self.handle_request_inner(action, false).await;
2867 }
2868 }
2869
2870 fn upstream_audio_track_names(
2871 &self,
2872 seeds: &std::collections::HashSet<String>,
2873 ) -> std::collections::HashSet<String> {
2874 let state = self.state.lock();
2875 let mut output_to_track: std::collections::HashMap<
2876 *const crate::audio::io::AudioIO,
2877 String,
2878 > = std::collections::HashMap::new();
2879 for (name, track) in &state.tracks {
2880 let t = track.lock();
2881 for out in &t.audio.outs {
2882 output_to_track.insert(std::sync::Arc::as_ptr(out), name.clone());
2883 }
2884 }
2885 let mut upstream = std::collections::HashSet::new();
2886 let mut to_process: Vec<String> = seeds.iter().cloned().collect();
2887 let mut processed = std::collections::HashSet::new();
2888 while let Some(target_name) = to_process.pop() {
2889 if !processed.insert(target_name.clone()) {
2890 continue;
2891 }
2892 if let Some(target_track) = state.tracks.get(&target_name) {
2893 let tt = target_track.lock();
2894 for input in &tt.audio.ins {
2895 for conn in input.connections.lock().iter() {
2896 let conn_ptr = std::sync::Arc::as_ptr(conn);
2897 if let Some(source_name) = output_to_track.get(&conn_ptr)
2898 && source_name != &target_name
2899 && !seeds.contains(source_name)
2900 {
2901 upstream.insert(source_name.clone());
2902 to_process.push(source_name.clone());
2903 }
2904 }
2905 }
2906 }
2907 }
2908 upstream
2909 }
2910
2911 fn is_track_in_soloed_folder(
2912 &self,
2913 track: &Track,
2914 tracks: &std::collections::HashMap<String, Arc<UnsafeMutex<Box<Track>>>>,
2915 ) -> bool {
2916 let mut current = track.parent_track.as_deref();
2917 while let Some(parent_name) = current {
2918 if let Some(parent) = tracks.get(parent_name) {
2919 let p = parent.lock();
2920 if p.soloed {
2921 return true;
2922 }
2923 current = p.parent_track.as_deref();
2924 } else {
2925 break;
2926 }
2927 }
2928 false
2929 }
2930
2931 fn folder_has_soloed_descendant(
2932 &self,
2933 folder_name: &str,
2934 tracks: &std::collections::HashMap<String, Arc<UnsafeMutex<Box<Track>>>>,
2935 ) -> bool {
2936 for track in tracks.values() {
2937 let t = track.lock();
2938 if !t.soloed {
2939 continue;
2940 }
2941 let mut current = t.parent_track.as_deref();
2942 while let Some(parent_name) = current {
2943 if parent_name == folder_name {
2944 return true;
2945 }
2946 if let Some(parent) = tracks.get(parent_name) {
2947 current = parent.lock().parent_track.as_deref();
2948 } else {
2949 break;
2950 }
2951 }
2952 }
2953 false
2954 }
2955
2956 fn refresh_realtime_infection(&self) {
2957 let state = self.state.lock();
2958 let live_seeds: std::collections::HashSet<String> = state
2959 .tracks
2960 .iter()
2961 .filter_map(|(name, track)| {
2962 let t = track.lock();
2963 if t.armed && t.input_monitor.iter().any(|&m| m) {
2964 Some(name.clone())
2965 } else {
2966 None
2967 }
2968 })
2969 .collect();
2970 let mut output_owner: std::collections::HashMap<*const crate::audio::io::AudioIO, String> =
2971 std::collections::HashMap::new();
2972 for (name, track) in state.tracks.iter() {
2973 let t = track.lock();
2974 for out in &t.audio.outs {
2975 output_owner.insert(std::sync::Arc::as_ptr(out), name.clone());
2976 }
2977 }
2978
2979 let mut infected = live_seeds.clone();
2980 let mut mixed_nodes = std::collections::HashSet::new();
2981 loop {
2982 let mut changed = false;
2983 for (name, track) in state.tracks.iter() {
2984 let t = track.lock();
2985 let mut upstream_owners = std::collections::HashSet::new();
2986 for input in &t.audio.ins {
2987 for conn in input.connections.lock().iter() {
2988 if let Some(owner) = output_owner.get(&std::sync::Arc::as_ptr(conn)) {
2989 upstream_owners.insert(owner.clone());
2990 }
2991 }
2992 }
2993 if upstream_owners.is_empty() {
2994 continue;
2995 }
2996 let has_realtime = upstream_owners
2997 .iter()
2998 .any(|owner| infected.contains(owner) || live_seeds.contains(owner));
2999 let has_playback = upstream_owners
3000 .iter()
3001 .any(|owner| !infected.contains(owner) && !live_seeds.contains(owner));
3002 if has_realtime && has_playback {
3003 mixed_nodes.insert(name.clone());
3004 }
3005 if has_realtime && infected.insert(name.clone()) {
3006 changed = true;
3007 }
3008 }
3009 if !changed {
3010 break;
3011 }
3012 }
3013
3014 for (name, track) in state.tracks.iter() {
3015 let forced = infected.contains(name) && !live_seeds.contains(name);
3016 let t = track.lock();
3017 t.set_shared_realtime_mixed(mixed_nodes.contains(name));
3018 t.set_force_realtime_domain(forced);
3019 }
3020 }
3021
3022 fn apply_mute_solo_policy(&mut self) {
3023 let mut newly_disabled_tracks = Vec::new();
3024 {
3025 let tracks = &self.state.lock().tracks;
3026 let soloed: std::collections::HashSet<String> = tracks
3027 .iter()
3028 .filter_map(|(name, t)| {
3029 if t.lock().soloed {
3030 Some(name.clone())
3031 } else {
3032 None
3033 }
3034 })
3035 .collect();
3036 let any_soloed = !soloed.is_empty();
3037 let upstream = if any_soloed {
3038 self.upstream_audio_track_names(&soloed)
3039 } else {
3040 std::collections::HashSet::new()
3041 };
3042 for track in tracks.values() {
3043 let t = track.lock();
3044 let was_enabled = t.output_enabled;
3045 let in_soloed_folder = self.is_track_in_soloed_folder(t, tracks);
3046 let folder_with_soloed_child =
3047 t.is_folder && self.folder_has_soloed_descendant(&t.name, tracks);
3048 let enabled = if t.is_master {
3049 !t.muted
3050 } else if any_soloed {
3051 (t.soloed
3052 || upstream.contains(&t.name)
3053 || in_soloed_folder
3054 || folder_with_soloed_child)
3055 && !t.muted
3056 } else {
3057 !t.muted
3058 };
3059 t.set_output_enabled(enabled);
3060 if was_enabled && !enabled {
3061 newly_disabled_tracks.push(t.name.clone());
3062 }
3063 }
3064 }
3065 let mut note_off_events = Vec::new();
3066 for track_name in newly_disabled_tracks {
3067 note_off_events.extend(self.note_off_events_for_track(&track_name));
3068 }
3069 if !note_off_events.is_empty() {
3070 self.pending_hw_midi_out_events_by_device
3071 .extend(note_off_events);
3072 }
3073 }
3074
3075 fn sanitize_file_stem(name: &str) -> String {
3076 let mut out = String::with_capacity(name.len());
3077 for c in name.chars() {
3078 if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
3079 out.push(c);
3080 } else {
3081 out.push('_');
3082 }
3083 }
3084 if out.is_empty() {
3085 "track".to_string()
3086 } else {
3087 out
3088 }
3089 }
3090
3091 fn next_recording_file_name(track_name: &str) -> String {
3092 let ts = SystemTime::now()
3093 .duration_since(UNIX_EPOCH)
3094 .map(|d| d.as_secs())
3095 .unwrap_or(0);
3096 format!("{}_{}.wav", Self::sanitize_file_stem(track_name), ts)
3097 }
3098
3099 fn next_midi_recording_file_name(track_name: &str) -> String {
3100 let ts = SystemTime::now()
3101 .duration_since(UNIX_EPOCH)
3102 .map(|d| d.as_secs())
3103 .unwrap_or(0);
3104 format!("{}_{}.mid", Self::sanitize_file_stem(track_name), ts)
3105 }
3106
3107 fn append_recorded_cycle(&mut self) {
3108 if !self.playing || !self.record_enabled {
3109 return;
3110 }
3111 for (name, track_handle) in &self.state.lock().tracks {
3112 let track = track_handle.lock();
3113 if !track.armed {
3114 continue;
3115 }
3116 let audio_channels = track.record_tap_outs.len();
3117 let audio_frames = track
3118 .record_tap_outs
3119 .first()
3120 .map(|ch| ch.len())
3121 .unwrap_or(0);
3122 let frames = audio_frames.max(self.current_cycle_samples());
3123 if frames == 0 {
3124 continue;
3125 }
3126 let segments = self.recording_segments_for_cycle(frames);
3127 for (segment_start, segment_end, frame_offset) in segments {
3128 let segment_len = segment_end.saturating_sub(segment_start);
3129 if segment_len == 0 {
3130 continue;
3131 }
3132
3133 if audio_channels > 0 && audio_frames > 0 {
3134 let audio_entry =
3135 self.audio_recordings
3136 .entry(name.clone())
3137 .or_insert_with(|| RecordingSession {
3138 start_sample: segment_start,
3139 samples: Vec::with_capacity(segment_len * audio_channels * 2),
3140 channels: audio_channels,
3141 file_name: Self::next_recording_file_name(name),
3142 stripe_peaks: vec![Vec::new(); audio_channels],
3143 current_stripe_frames: 0,
3144 });
3145 if audio_entry.channels != audio_channels {
3146 continue;
3147 }
3148 if let Some(entry) = self.audio_recordings.get_mut(name.as_str()) {
3149 let from = frame_offset.min(audio_frames);
3150 let to = frame_offset.saturating_add(segment_len).min(audio_frames);
3151 for frame in from..to {
3152 let is_new_stripe =
3153 entry.current_stripe_frames % RECORDING_STRIPE_FRAMES == 0;
3154 for ch in 0..audio_channels {
3155 let sample = track.record_tap_outs[ch][frame].clamp(-1.0, 1.0);
3156 if is_new_stripe {
3157 entry.stripe_peaks[ch].push([sample, sample]);
3158 } else {
3159 let idx = entry.stripe_peaks[ch].len() - 1;
3160 entry.stripe_peaks[ch][idx][0] =
3161 entry.stripe_peaks[ch][idx][0].min(sample);
3162 entry.stripe_peaks[ch][idx][1] =
3163 entry.stripe_peaks[ch][idx][1].max(sample);
3164 }
3165 entry.samples.push(track.record_tap_outs[ch][frame]);
3166 }
3167 entry.current_stripe_frames += 1;
3168 }
3169 }
3170 }
3171
3172 let entry = self.midi_recordings.entry(name.clone()).or_insert_with(|| {
3173 MidiRecordingSession {
3174 start_sample: segment_start,
3175 events: Vec::new(),
3176 file_name: Self::next_midi_recording_file_name(name),
3177 }
3178 });
3179 let from = frame_offset;
3180 let to = frame_offset.saturating_add(segment_len);
3181 for event in &track.record_tap_midi_in {
3182 let frame = event.frame as usize;
3183 if frame < from || frame >= to {
3184 continue;
3185 }
3186 let abs_sample = segment_start as u64 + (frame - from) as u64;
3187 entry.events.push((abs_sample, event.data.clone()));
3188 }
3189
3190 if self.punch_enabled
3191 && let Some((_, punch_end)) = self.punch_range_samples
3192 && segment_end == punch_end
3193 {
3194 if let Some(done) = self.audio_recordings.remove(name.as_str()) {
3195 self.completed_audio_recordings.push((name.clone(), done));
3196 }
3197 if let Some(done) = self.midi_recordings.remove(name.as_str()) {
3198 self.completed_midi_recordings.push((name.clone(), done));
3199 }
3200 } else if self.loop_enabled
3201 && let Some((_, loop_end)) = self.loop_range_samples
3202 && segment_end == loop_end
3203 {
3204 if let Some(done) = self.audio_recordings.remove(name.as_str()) {
3205 self.completed_audio_recordings.push((name.clone(), done));
3206 }
3207 if let Some(done) = self.midi_recordings.remove(name.as_str()) {
3208 self.completed_midi_recordings.push((name.clone(), done));
3209 }
3210 }
3211 }
3212 }
3213 }
3214
3215 async fn flush_completed_recordings(&mut self) {
3216 if self.completed_audio_recordings.is_empty() && self.completed_midi_recordings.is_empty() {
3217 return;
3218 }
3219 let Some(audio_dir) = self.session_audio_dir() else {
3220 self.completed_audio_recordings.clear();
3221 self.completed_midi_recordings.clear();
3222 return;
3223 };
3224 let Some(midi_dir) = self.session_midi_dir() else {
3225 self.completed_audio_recordings.clear();
3226 self.completed_midi_recordings.clear();
3227 return;
3228 };
3229 if std::fs::create_dir_all(&audio_dir).is_err()
3230 || std::fs::create_dir_all(&midi_dir).is_err()
3231 {
3232 self.completed_audio_recordings.clear();
3233 self.completed_midi_recordings.clear();
3234 return;
3235 }
3236 let rate = self
3237 .hw_driver
3238 .as_ref()
3239 .map(|o| o.lock().sample_rate())
3240 .unwrap_or(48_000);
3241 let completed_audio = std::mem::take(&mut self.completed_audio_recordings);
3242 for (track_name, rec) in completed_audio {
3243 self.flush_recording_entry(&audio_dir, rate, track_name, rec)
3244 .await;
3245 }
3246 let completed_midi = std::mem::take(&mut self.completed_midi_recordings);
3247 for (track_name, rec) in completed_midi {
3248 self.flush_midi_recording_entry(&midi_dir, rate as u32, track_name, rec)
3249 .await;
3250 }
3251 }
3252
3253 async fn flush_recordings(&mut self) {
3254 let Some(audio_dir) = self.session_audio_dir() else {
3255 if !self.audio_recordings.is_empty()
3256 || !self.midi_recordings.is_empty()
3257 || !self.completed_audio_recordings.is_empty()
3258 || !self.completed_midi_recordings.is_empty()
3259 {
3260 self.notify_clients(Err("Recording stopped: session path is not set".to_string()))
3261 .await;
3262 }
3263 self.audio_recordings.clear();
3264 self.midi_recordings.clear();
3265 self.completed_audio_recordings.clear();
3266 self.completed_midi_recordings.clear();
3267 return;
3268 };
3269 if std::fs::create_dir_all(&audio_dir).is_err() {
3270 self.notify_clients(Err(format!(
3271 "Recording stopped: failed to create audio directory {}",
3272 audio_dir.display()
3273 )))
3274 .await;
3275 self.audio_recordings.clear();
3276 self.midi_recordings.clear();
3277 self.completed_audio_recordings.clear();
3278 self.completed_midi_recordings.clear();
3279 return;
3280 }
3281 let Some(midi_dir) = self.session_midi_dir() else {
3282 self.audio_recordings.clear();
3283 self.midi_recordings.clear();
3284 self.completed_audio_recordings.clear();
3285 self.completed_midi_recordings.clear();
3286 return;
3287 };
3288 if std::fs::create_dir_all(&midi_dir).is_err() {
3289 self.audio_recordings.clear();
3290 self.midi_recordings.clear();
3291 self.completed_audio_recordings.clear();
3292 self.completed_midi_recordings.clear();
3293 return;
3294 }
3295 let rate = self
3296 .hw_driver
3297 .as_ref()
3298 .map(|o| o.lock().sample_rate())
3299 .unwrap_or(48_000);
3300 let completed_audio = std::mem::take(&mut self.completed_audio_recordings);
3301 for (track_name, rec) in completed_audio {
3302 self.flush_recording_entry(&audio_dir, rate, track_name, rec)
3303 .await;
3304 }
3305 let completed_midi = std::mem::take(&mut self.completed_midi_recordings);
3306 for (track_name, rec) in completed_midi {
3307 self.flush_midi_recording_entry(&midi_dir, rate as u32, track_name, rec)
3308 .await;
3309 }
3310 let recordings = std::mem::take(&mut self.audio_recordings);
3311 for (track_name, rec) in recordings {
3312 self.flush_recording_entry(&audio_dir, rate, track_name, rec)
3313 .await;
3314 }
3315 let midi_recordings = std::mem::take(&mut self.midi_recordings);
3316 for (track_name, rec) in midi_recordings {
3317 self.flush_midi_recording_entry(&midi_dir, rate as u32, track_name, rec)
3318 .await;
3319 }
3320 }
3321
3322 fn compute_peaks_from_stripes(
3323 stripe_peaks: &[Vec<[f32; 2]>],
3324 total_frames: usize,
3325 channels: usize,
3326 ) -> serde_json::Value {
3327 const MAX_PEAK_BINS: usize = 32_768;
3328 if total_frames == 0 || stripe_peaks.is_empty() {
3329 return serde_json::json!({"peaks": []});
3330 }
3331 let target_bins = total_frames.clamp(1024, MAX_PEAK_BINS);
3332 let mut peaks = vec![vec![[0.0_f32, 0.0_f32]; target_bins]; channels];
3333 for (ch, channel_peaks) in peaks.iter_mut().enumerate() {
3334 let mut touched = vec![false; target_bins];
3335 let empty = Vec::new();
3336 let channel_stripes = stripe_peaks.get(ch).unwrap_or(&empty);
3337 for (stripe_idx, stripe) in channel_stripes.iter().enumerate() {
3338 let stripe_start = stripe_idx * RECORDING_STRIPE_FRAMES;
3339 let stripe_end = ((stripe_idx + 1) * RECORDING_STRIPE_FRAMES).min(total_frames);
3340 let start_bin = (stripe_start * target_bins) / total_frames.max(1);
3341 let end_bin = ((stripe_end.saturating_sub(1)) * target_bins / total_frames.max(1))
3342 .min(target_bins - 1);
3343 for bin in start_bin..=end_bin {
3344 if !touched[bin] {
3345 channel_peaks[bin] = *stripe;
3346 touched[bin] = true;
3347 } else {
3348 channel_peaks[bin][0] = channel_peaks[bin][0].min(stripe[0]);
3349 channel_peaks[bin][1] = channel_peaks[bin][1].max(stripe[1]);
3350 }
3351 }
3352 }
3353 }
3354 serde_json::json!({
3355 "peaks": peaks.iter().map(|ch| {
3356 ch.iter().map(|pair| serde_json::json!([pair[0], pair[1]])).collect::<Vec<_>>()
3357 }).collect::<Vec<_>>()
3358 })
3359 }
3360
3361 async fn flush_recording_entry(
3362 &mut self,
3363 audio_dir: &Path,
3364 rate: i32,
3365 track_name: String,
3366 rec: RecordingSession,
3367 ) {
3368 if rec.samples.is_empty() || rec.channels == 0 {
3369 return;
3370 }
3371
3372 let trim_frames = self.hw_output_latency_frames;
3373 let trim_samples = trim_frames * rec.channels;
3374 let samples = if trim_samples > 0 && rec.samples.len() > trim_samples {
3375 &rec.samples[trim_samples..]
3376 } else {
3377 &rec.samples[..]
3378 };
3379 if samples.is_empty() {
3380 return;
3381 }
3382 let file_path = audio_dir.join(&rec.file_name);
3383 let write_result =
3384 crate::audio_codec::write_wav_f32(&file_path, samples, rec.channels, rate as u32);
3385 if let Err(e) = write_result {
3386 tracing::error!("flush_recording_entry: WAV write failed: {}", e);
3387 self.notify_clients(Err(format!(
3388 "Failed to write recording {}: {}",
3389 file_path.display(),
3390 e
3391 )))
3392 .await;
3393 return;
3394 }
3395
3396 let total_frames = rec.current_stripe_frames;
3397 let peaks_json =
3398 Self::compute_peaks_from_stripes(&rec.stripe_peaks, total_frames, rec.channels);
3399 let peaks_file_name = format!("{}.json", rec.file_name);
3400 let peaks_rel = format!("peaks/{}", peaks_file_name);
3401 let peaks_path = self.session_peaks_dir().map(|d| d.join(&peaks_file_name));
3402 if let Some(peaks_dir) = self.session_peaks_dir() {
3403 let _ = std::fs::create_dir_all(&peaks_dir);
3404 }
3405 if let Some(ref path) = peaks_path
3406 && let Err(e) = std::fs::write(
3407 path,
3408 serde_json::to_string_pretty(&peaks_json).unwrap_or_default(),
3409 )
3410 {
3411 tracing::warn!("Failed to write peaks file {}: {}", path.display(), e);
3412 }
3413 let length = samples.len() / rec.channels;
3414 let start_sample = rec.start_sample.saturating_add(trim_frames);
3415 let clip_rel_name = format!("audio/{}", rec.file_name);
3416 let mut clip = AudioClip::new(
3417 clip_rel_name.clone(),
3418 start_sample,
3419 start_sample.saturating_add(length.max(1)),
3420 );
3421 let (audio_ins, audio_outs) = if let Some(track) = self.state.lock().tracks.get(&track_name)
3422 {
3423 let track = track.lock();
3424 let audio_ins = track.audio.ins.len();
3425 let audio_outs = track.audio.outs.len();
3426 track.audio.clips.push(clip.clone());
3427 (audio_ins, audio_outs)
3428 } else {
3429 tracing::warn!(
3430 "flush_recording_entry: track '{}' not found in engine state",
3431 track_name
3432 );
3433 (0, 0)
3434 };
3435 let clip_id = crate::message::generate_clip_id();
3436 clip.id.clone_from(&clip_id);
3437 self.notify_clients(Ok(Action::AddClip {
3438 clip_id,
3439 name: clip_rel_name,
3440 track_name: track_name.clone(),
3441 start: start_sample,
3442 length,
3443 offset: 0,
3444 input_channel: 0,
3445 muted: false,
3446 peaks_file: peaks_path.is_some().then_some(peaks_rel),
3447 kind: Kind::Audio,
3448 fade_enabled: clip.fade_enabled,
3449 fade_in_samples: clip.fade_in_samples,
3450 fade_out_samples: clip.fade_out_samples,
3451 source_name: None,
3452 source_offset: None,
3453 source_length: None,
3454 preview_name: None,
3455 pitch_correction_points: vec![],
3456 pitch_correction_frame_likeness: None,
3457 pitch_correction_inertia_ms: None,
3458 pitch_correction_formant_compensation: None,
3459 plugin_graph_json: Some(Self::default_clip_plugin_graph_json(audio_ins, audio_outs)),
3460 }))
3461 .await;
3462 if let Some(track) = self.state.lock().tracks.get(&track_name).cloned() {
3463 tokio::task::spawn_blocking(move || {
3464 track.lock().preload_clips();
3465 tracing::debug!("Preloaded clips for track '{}' after recording", track_name);
3466 });
3467 }
3468 }
3469
3470 async fn flush_track_recording(&mut self, track_name: &str) {
3471 let Some(audio_dir) = self.session_audio_dir() else {
3472 self.audio_recordings.remove(track_name);
3473 self.midi_recordings.remove(track_name);
3474 self.completed_audio_recordings
3475 .retain(|(name, _)| name != track_name);
3476 self.completed_midi_recordings
3477 .retain(|(name, _)| name != track_name);
3478 return;
3479 };
3480 let Some(midi_dir) = self.session_midi_dir() else {
3481 self.audio_recordings.remove(track_name);
3482 self.midi_recordings.remove(track_name);
3483 self.completed_audio_recordings
3484 .retain(|(name, _)| name != track_name);
3485 self.completed_midi_recordings
3486 .retain(|(name, _)| name != track_name);
3487 return;
3488 };
3489 if std::fs::create_dir_all(&audio_dir).is_err()
3490 || std::fs::create_dir_all(&midi_dir).is_err()
3491 {
3492 return;
3493 }
3494 let rate = self
3495 .hw_driver
3496 .as_ref()
3497 .map(|o| o.lock().sample_rate())
3498 .unwrap_or(48_000);
3499 let mut i = 0;
3500 while i < self.completed_audio_recordings.len() {
3501 if self.completed_audio_recordings[i].0 == track_name {
3502 let (name, rec) = self.completed_audio_recordings.remove(i);
3503 self.flush_recording_entry(&audio_dir, rate, name, rec)
3504 .await;
3505 } else {
3506 i += 1;
3507 }
3508 }
3509 let mut j = 0;
3510 while j < self.completed_midi_recordings.len() {
3511 if self.completed_midi_recordings[j].0 == track_name {
3512 let (name, rec) = self.completed_midi_recordings.remove(j);
3513 self.flush_midi_recording_entry(&midi_dir, rate as u32, name, rec)
3514 .await;
3515 } else {
3516 j += 1;
3517 }
3518 }
3519
3520 let Some(rec) = self.audio_recordings.remove(track_name) else {
3521 if let Some(mrec) = self.midi_recordings.remove(track_name) {
3522 self.flush_midi_recording_entry(
3523 &midi_dir,
3524 rate as u32,
3525 track_name.to_string(),
3526 mrec,
3527 )
3528 .await;
3529 }
3530 return;
3531 };
3532 self.flush_recording_entry(&audio_dir, rate, track_name.to_string(), rec)
3533 .await;
3534 if let Some(mrec) = self.midi_recordings.remove(track_name) {
3535 self.flush_midi_recording_entry(&midi_dir, rate as u32, track_name.to_string(), mrec)
3536 .await;
3537 }
3538 }
3539
3540 async fn flush_midi_recording_entry(
3541 &mut self,
3542 midi_dir: &Path,
3543 sample_rate: u32,
3544 track_name: String,
3545 mut rec: MidiRecordingSession,
3546 ) {
3547 if rec.events.is_empty() {
3548 return;
3549 }
3550 rec.events.sort_by_key(|(sample, _)| *sample);
3551 let clip_rel_name = format!("midi/{}", rec.file_name);
3552 let clip_len_samples = rec
3553 .events
3554 .last()
3555 .map(|(s, _)| s.saturating_sub(rec.start_sample as u64) as usize + 1)
3556 .unwrap_or(1);
3557
3558 for (sample, _) in &mut rec.events {
3559 *sample = sample.saturating_sub(rec.start_sample as u64);
3560 }
3561 let path = midi_dir.join(&rec.file_name);
3562 if let Err(e) = Self::write_midi_file(&path, sample_rate, &rec.events) {
3563 self.notify_clients(Err(format!(
3564 "Failed to write MIDI recording {}: {}",
3565 path.display(),
3566 e
3567 )))
3568 .await;
3569 return;
3570 }
3571 let mut clip = MIDIClip::new(
3572 clip_rel_name.clone(),
3573 rec.start_sample,
3574 rec.start_sample.saturating_add(clip_len_samples.max(1)),
3575 );
3576 clip.offset = 0;
3577 let clip_id = crate::message::generate_clip_id();
3578 clip.id.clone_from(&clip_id);
3579 if let Some(track) = self.state.lock().tracks.get(&track_name) {
3580 track.lock().midi.clips.push(clip);
3581 }
3582 self.notify_clients(Ok(Action::AddClip {
3583 clip_id,
3584 name: clip_rel_name,
3585 track_name: track_name.clone(),
3586 start: rec.start_sample,
3587 length: clip_len_samples,
3588 offset: 0,
3589 input_channel: 0,
3590 muted: false,
3591 peaks_file: None,
3592 kind: Kind::MIDI,
3593 fade_enabled: true,
3594 fade_in_samples: 240,
3595 fade_out_samples: 240,
3596 source_name: None,
3597 source_offset: None,
3598 source_length: None,
3599 preview_name: None,
3600 pitch_correction_points: vec![],
3601 pitch_correction_frame_likeness: None,
3602 pitch_correction_inertia_ms: None,
3603 pitch_correction_formant_compensation: None,
3604 plugin_graph_json: None,
3605 }))
3606 .await;
3607 if let Some(track) = self.state.lock().tracks.get(&track_name).cloned() {
3608 tokio::task::spawn_blocking(move || {
3609 track.lock().preload_clips();
3610 tracing::debug!(
3611 "Preloaded clips for track '{}' after MIDI recording",
3612 track_name
3613 );
3614 });
3615 }
3616 }
3617
3618 fn write_midi_file(
3619 path: &Path,
3620 sample_rate: u32,
3621 events: &[(u64, Vec<u8>)],
3622 ) -> Result<(), String> {
3623 let ppq: u16 = 480;
3624 let ticks_per_second: u64 = 960;
3625 let arena = Arena::new();
3626 let mut track_events: Vec<TrackEvent<'_>> = vec![TrackEvent {
3627 delta: u28::new(0),
3628 kind: TrackEventKind::Meta(MetaMessage::Tempo(u24::new(500_000))),
3629 }];
3630 let mut prev_ticks = 0_u64;
3631 for (sample, data) in events {
3632 let ticks = sample.saturating_mul(ticks_per_second) / sample_rate.max(1) as u64;
3633 let delta = ticks.saturating_sub(prev_ticks).min(u32::MAX as u64) as u32;
3634 prev_ticks = ticks;
3635 let Ok(live) = LiveEvent::parse(data) else {
3636 continue;
3637 };
3638 let kind = live.as_track_event(&arena);
3639 track_events.push(TrackEvent {
3640 delta: u28::new(delta),
3641 kind,
3642 });
3643 }
3644 track_events.push(TrackEvent {
3645 delta: u28::new(0),
3646 kind: TrackEventKind::Meta(MetaMessage::EndOfTrack),
3647 });
3648
3649 let smf = Smf {
3650 header: Header::new(Format::SingleTrack, Timing::Metrical(u15::new(ppq))),
3651 tracks: vec![track_events],
3652 };
3653 let mut file = File::create(path).map_err(|e| e.to_string())?;
3654 smf.write_std(&mut file).map_err(|e| e.to_string())
3655 }
3656
3657 pub async fn init(&mut self) {
3658 let max_threads = num_cpus::get();
3659 for id in 0..max_threads {
3660 let (tx, rx) = channel::<Message>(32);
3661 let tx_thread = self.tx.clone();
3662 let handler = tokio::spawn(async move {
3663 let wrk = Worker::new(id, rx, tx_thread, 8);
3664 wrk.await.work().await;
3665 });
3666 self.workers.push(WorkerData::new(tx.clone(), handler));
3667 }
3668 }
3669
3670 async fn notify_clients(&mut self, action: Result<Action, String>) {
3671 self.clients.retain(|client| !client.is_closed());
3672 for client in self.clients.iter() {
3673 if client
3674 .send(Message::Response(action.clone()))
3675 .await
3676 .is_err()
3677 {}
3678 }
3679 if let Some(reply_to) = self.osc_reply_target {
3680 match &action {
3681 Err(reason) => {
3682 self.send_osc_reply(reply_to, &build_error_packet(reason));
3683 }
3684 Ok(Action::TrackList(names)) => {
3685 let args: Vec<OscArg> = names
3686 .iter()
3687 .map(|name| OscArg::String(name.clone()))
3688 .collect();
3689 self.send_osc_reply(
3690 reply_to,
3691 &build_osc_packet("/response/tracks", &"s".repeat(names.len()), &args),
3692 );
3693 }
3694 Ok(Action::TransportState {
3695 sample,
3696 tempo_bpm,
3697 playing,
3698 paused: _,
3699 tsig_num,
3700 tsig_denom,
3701 }) => {
3702 self.send_osc_reply(
3703 reply_to,
3704 &build_osc_packet(
3705 "/response/transport",
3706 "idffii",
3707 &[
3708 OscArg::Int(*sample as i32),
3709 OscArg::Int(if *playing { 1 } else { 0 }),
3710 OscArg::Float(*tempo_bpm as f32),
3711 OscArg::Float(0.0), OscArg::Int(*tsig_num as i32),
3713 OscArg::Int(*tsig_denom as i32),
3714 ],
3715 ),
3716 );
3717 }
3718 Ok(Action::MeterSnapshot {
3719 hw_out_db,
3720 track_meters,
3721 }) => {
3722 let mut args: Vec<OscArg> = Vec::new();
3723 args.push(OscArg::Int(hw_out_db.len() as i32));
3724 for db in hw_out_db.iter() {
3725 args.push(OscArg::Float(*db));
3726 }
3727 args.push(OscArg::Int(track_meters.len() as i32));
3728 for (name, channels) in track_meters.iter() {
3729 args.push(OscArg::String(name.clone()));
3730 args.push(OscArg::Int(channels.len() as i32));
3731 for db in channels.iter() {
3732 args.push(OscArg::Float(*db));
3733 }
3734 }
3735 let types = args
3736 .iter()
3737 .map(|a| match a {
3738 OscArg::String(_) => 's',
3739 OscArg::Int(_) => 'i',
3740 OscArg::Float(_) => 'f',
3741 })
3742 .collect::<String>();
3743 self.send_osc_reply(
3744 reply_to,
3745 &build_osc_packet("/response/meters", &types, &args),
3746 );
3747 }
3748 Ok(Action::TrackPluginGraph {
3749 track_name,
3750 plugins,
3751 connections: _,
3752 connectable_connections: _,
3753 }) => {
3754 let mut args: Vec<OscArg> = vec![OscArg::String(track_name.clone())];
3755 args.push(OscArg::Int(plugins.len() as i32));
3756 for plugin in plugins.iter() {
3757 args.push(OscArg::Int(plugin.instance_id as i32));
3758 args.push(OscArg::String(plugin.format.clone()));
3759 args.push(OscArg::String(plugin.uri.clone()));
3760 args.push(OscArg::String(plugin.name.clone()));
3761 args.push(OscArg::Int(plugin.bypassed as i32));
3762 }
3763 let types = args
3764 .iter()
3765 .map(|a| match a {
3766 OscArg::String(_) => 's',
3767 OscArg::Int(_) => 'i',
3768 OscArg::Float(_) => 'f',
3769 })
3770 .collect::<String>();
3771 self.send_osc_reply(
3772 reply_to,
3773 &build_osc_packet("/response/plugins", &types, &args),
3774 );
3775 }
3776 Ok(Action::ClapPlugins(plugins)) => {
3777 let args: Vec<OscArg> = plugins
3778 .iter()
3779 .map(|p| OscArg::String(format!("{}|{}", p.path, p.name)))
3780 .collect();
3781 let types = "s".repeat(args.len());
3782 self.send_osc_reply(
3783 reply_to,
3784 &build_osc_packet("/response/clap_plugins", &types, &args),
3785 );
3786 }
3787 Ok(Action::Vst3Plugins(plugins)) => {
3788 let args: Vec<OscArg> = plugins
3789 .iter()
3790 .map(|p| OscArg::String(format!("{}|{}", p.id, p.name)))
3791 .collect();
3792 let types = "s".repeat(args.len());
3793 self.send_osc_reply(
3794 reply_to,
3795 &build_osc_packet("/response/vst3_plugins", &types, &args),
3796 );
3797 }
3798 #[cfg(all(unix, not(target_os = "macos")))]
3799 Ok(Action::Lv2Plugins(plugins)) => {
3800 let args: Vec<OscArg> = plugins
3801 .iter()
3802 .map(|p| OscArg::String(format!("{}|{}", p.uri, p.name)))
3803 .collect();
3804 let types = "s".repeat(args.len());
3805 self.send_osc_reply(
3806 reply_to,
3807 &build_osc_packet("/response/lv2_plugins", &types, &args),
3808 );
3809 }
3810 Ok(Action::ClapPluginsUnavailable { error })
3811 | Ok(Action::Vst3PluginsUnavailable { error }) => {
3812 self.send_osc_reply(reply_to, &build_error_packet(error));
3813 }
3814 #[cfg(all(unix, not(target_os = "macos")))]
3815 Ok(Action::Lv2PluginsUnavailable { error }) => {
3816 self.send_osc_reply(reply_to, &build_error_packet(error));
3817 }
3818 Ok(Action::TrackClapParameters {
3819 track_name,
3820 instance_id,
3821 parameters,
3822 }) => {
3823 let json = serde_json::json!(
3824 parameters
3825 .iter()
3826 .map(|p| serde_json::json!({
3827 "id": p.id,
3828 "name": p.name,
3829 "module": p.module,
3830 "min_value": p.min_value,
3831 "max_value": p.max_value,
3832 "default_value": p.default_value,
3833 }))
3834 .collect::<Vec<_>>()
3835 )
3836 .to_string();
3837 self.send_osc_reply(
3838 reply_to,
3839 &build_osc_packet(
3840 "/response/plugin_parameters",
3841 "siss",
3842 &[
3843 OscArg::String(track_name.clone()),
3844 OscArg::Int(*instance_id as i32),
3845 OscArg::String("clap".to_string()),
3846 OscArg::String(json),
3847 ],
3848 ),
3849 );
3850 }
3851 Ok(Action::TrackVst3Parameters {
3852 track_name,
3853 instance_id,
3854 parameters,
3855 }) => {
3856 let json = serde_json::to_string(parameters).unwrap_or_default();
3857 self.send_osc_reply(
3858 reply_to,
3859 &build_osc_packet(
3860 "/response/plugin_parameters",
3861 "siss",
3862 &[
3863 OscArg::String(track_name.clone()),
3864 OscArg::Int(*instance_id as i32),
3865 OscArg::String("vst3".to_string()),
3866 OscArg::String(json),
3867 ],
3868 ),
3869 );
3870 }
3871 #[cfg(all(unix, not(target_os = "macos")))]
3872 Ok(Action::TrackLv2PluginControls {
3873 track_name,
3874 instance_id,
3875 controls,
3876 instance_access_handle: _,
3877 }) => {
3878 let json = serde_json::json!(
3879 controls
3880 .iter()
3881 .map(|c| serde_json::json!({
3882 "index": c.index,
3883 "name": c.name,
3884 "min": c.min,
3885 "max": c.max,
3886 "value": c.value,
3887 }))
3888 .collect::<Vec<_>>()
3889 )
3890 .to_string();
3891 self.send_osc_reply(
3892 reply_to,
3893 &build_osc_packet(
3894 "/response/plugin_parameters",
3895 "siss",
3896 &[
3897 OscArg::String(track_name.clone()),
3898 OscArg::Int(*instance_id as i32),
3899 OscArg::String("lv2".to_string()),
3900 OscArg::String(json),
3901 ],
3902 ),
3903 );
3904 }
3905 _ => {}
3906 }
3907 }
3908 }
3909
3910 fn send_osc_reply(&mut self, reply_to: SocketAddr, packet: &[u8]) {
3911 if self.osc_reply_socket.is_none() {
3912 self.osc_reply_socket = UdpSocket::bind("0.0.0.0:0").ok();
3913 }
3914 if let Some(socket) = self.osc_reply_socket.as_ref() {
3915 let _ = socket.send_to(packet, reply_to);
3916 }
3917 }
3918
3919 async fn dispatch_request(&mut self, a: Action) {
3920 match a {
3921 Action::TrackOfflineBounceCancel { track_name } => {
3922 if let Some(job) = self.offline_bounce_jobs.get(&track_name) {
3923 job.cancel.store(true, Ordering::Relaxed);
3924 }
3925 }
3926 Action::TrackOfflineBounceCancelAll => {
3927 for job in self.offline_bounce_jobs.values() {
3928 job.cancel.store(true, Ordering::Relaxed);
3929 }
3930 }
3931 _ if !self.offline_bounce_jobs.is_empty() => {
3932 self.pending_requests.push_back(a);
3933 }
3934 Action::OpenAudioDevice { .. }
3935 | Action::OpenMidiInputDevice(_)
3936 | Action::OpenMidiOutputDevice(_)
3937 | Action::RequestMeterSnapshot
3938 | Action::RequestTrackList
3939 | Action::RequestTransportState
3940 | Action::Quit
3941 | Action::Log { .. }
3942 | Action::Play
3943 | Action::Pause
3944 | Action::Stop
3945 | Action::TransportPosition(_)
3946 | Action::JumpToEnd
3947 | Action::SetLoopEnabled(_)
3948 | Action::SetLoopRange(_)
3949 | Action::SetPunchEnabled(_)
3950 | Action::SetPunchRange(_)
3951 | Action::SetMetronomeEnabled(_)
3952 | Action::SetTempo(_)
3953 | Action::SetTimeSignature { .. }
3954 | Action::SetTempoMap { .. }
3955 | Action::SetOscEnabled(_)
3956 | Action::SetClipPlaybackEnabled(_)
3957 | Action::SetRecordEnabled(_)
3958 | Action::SetStepRecording(_)
3959 | Action::StepRecordMidiNote { .. }
3960 | Action::SetSessionPath(_)
3961 | Action::ClearHistory
3962 | Action::BeginSessionRestore
3963 | Action::PianoKey { .. }
3964 | Action::ModifyMidiNotes { .. }
3965 | Action::ModifyMidiControllers { .. }
3966 | Action::DeleteMidiControllers { .. }
3967 | Action::InsertMidiControllers { .. }
3968 | Action::DeleteMidiNotes { .. }
3969 | Action::InsertMidiNotes { .. }
3970 | Action::SetMidiSysExEvents { .. }
3971 | Action::Session(_) => {
3972 self.handle_request(a).await;
3973 }
3974 #[cfg(all(unix, not(target_os = "macos")))]
3975 Action::ListLv2Plugins => {
3976 self.handle_request(a).await;
3977 }
3978 Action::ListVst3Plugins => {
3979 self.handle_request(a).await;
3980 }
3981 Action::ListClapPlugins => {
3982 self.handle_request(a).await;
3983 }
3984 Action::ListClapPluginsWithCapabilities => {
3985 self.handle_request(a).await;
3986 }
3987 _ => {
3988 self.pending_requests.push_back(a);
3989 if self.can_schedule_hw_cycle() {
3990 self.request_hw_cycle().await;
3991 } else {
3992 while let Some(next) = self.pending_requests.pop_front() {
3993 self.handle_request(next).await;
3994 }
3995 }
3996 }
3997 };
3998 self.publish_clap_state_dirty().await;
3999 }
4000
4001 fn spawn_plugin_host_stderr_reader(&self, stderr: std::process::ChildStderr, source: String) {
4002 let tx = self.tx.clone();
4003 std::thread::spawn(move || {
4004 use std::io::{BufRead, BufReader};
4005 let reader = BufReader::new(stderr);
4006 for line in reader.lines() {
4007 if let Ok(line) = line
4008 && !line.is_empty()
4009 {
4010 let _ = tx.blocking_send(Message::Request(Action::Log {
4011 source: source.clone(),
4012 message: line,
4013 }));
4014 }
4015 }
4016 });
4017 }
4018
4019 fn set_osc_enabled_with<F>(&mut self, enabled: bool, start_server: F) -> Result<(), String>
4020 where
4021 F: FnOnce(Sender<Message>) -> Result<OscServer, String>,
4022 {
4023 if enabled {
4024 if self.osc_server.is_none() {
4025 self.osc_server = Some(start_server(self.tx.clone())?);
4026 }
4027 } else if let Some(mut server) = self.osc_server.take() {
4028 server.stop();
4029 }
4030 Ok(())
4031 }
4032
4033 fn track_handle_by_name(&self, track_name: &str) -> Option<Arc<UnsafeMutex<Box<Track>>>> {
4034 self.state.lock().tracks.get(track_name).cloned()
4035 }
4036
4037 fn track_handle_or_err(
4038 &self,
4039 track_name: &str,
4040 ) -> Result<Arc<UnsafeMutex<Box<Track>>>, String> {
4041 self.track_handle_by_name(track_name)
4042 .ok_or_else(|| format!("Track not found: {track_name}"))
4043 }
4044
4045 fn parse_automation_lanes(value: &serde_json::Value) -> Vec<OfflineAutomationLane> {
4046 parse_automation_lanes(value)
4047 }
4048
4049 fn add_clip_to_track(&self, request: ClipAddRequest<'_>) {
4050 if let Some(track) = self.state.lock().tracks.get(request.track_name) {
4051 let track = track.lock();
4052 if track.is_master || track.is_folder {
4053 return;
4054 }
4055 match request.kind {
4056 Kind::Audio => {
4057 let mut clip = AudioClip::new(
4058 request.name.to_string(),
4059 request.start,
4060 request.start.saturating_add(request.length.max(1)),
4061 );
4062 clip.id = request.clip_id.to_string();
4063 clip.offset = request.offset;
4064 let max_lane = track.audio.ins.len().saturating_sub(1);
4065 clip.input_channel = request.input_channel.min(max_lane);
4066 clip.muted = request.muted;
4067 clip.peaks_file = request.peaks_file;
4068 clip.fade_enabled = request.fade_enabled;
4069 clip.fade_in_samples = request.fade_in_samples;
4070 clip.fade_out_samples = request.fade_out_samples;
4071 clip.pitch_correction_preview_name = request.preview_name;
4072 clip.pitch_correction_source_name = request.source_name;
4073 clip.pitch_correction_source_offset = request.source_offset;
4074 clip.pitch_correction_source_length = request.source_length;
4075 clip.pitch_correction_points = request.pitch_correction_points;
4076 clip.pitch_correction_frame_likeness = request.pitch_correction_frame_likeness;
4077 clip.pitch_correction_inertia_ms = request.pitch_correction_inertia_ms;
4078 clip.pitch_correction_formant_compensation =
4079 request.pitch_correction_formant_compensation;
4080 clip.plugin_graph_json = request.plugin_graph_json;
4081 track.audio.clips.push(clip);
4082 #[cfg(unix)]
4083 track.clip_pitch_shifters.clear();
4084 }
4085 Kind::MIDI => {
4086 let mut clip = MIDIClip::new(
4087 request.name.to_string(),
4088 request.start,
4089 request.start.saturating_add(request.length.max(1)),
4090 );
4091 clip.id = request.clip_id.to_string();
4092 clip.offset = request.offset;
4093 let max_lane = track.midi.ins.len().saturating_sub(1);
4094 clip.input_channel = request.input_channel.min(max_lane);
4095 clip.muted = request.muted;
4096 track.midi.clips.push(clip);
4097 }
4098 }
4099 }
4100 }
4101
4102 fn audio_clip_from_data(data: &crate::message::AudioClipData) -> AudioClip {
4103 let mut clip = AudioClip::new(
4104 data.name.clone(),
4105 data.start,
4106 data.start.saturating_add(data.length.max(1)),
4107 );
4108 clip.id = data.id.clone();
4109 clip.offset = data.offset;
4110 clip.input_channel = data.input_channel;
4111 clip.muted = data.muted;
4112 clip.peaks_file = data.peaks_file.clone();
4113 clip.fade_enabled = data.fade_enabled;
4114 clip.fade_in_samples = data.fade_in_samples;
4115 clip.fade_out_samples = data.fade_out_samples;
4116 clip.pitch_correction_preview_name = data.preview_name.clone();
4117 clip.pitch_correction_source_name = data.source_name.clone();
4118 clip.pitch_correction_source_offset = data.source_offset;
4119 clip.pitch_correction_source_length = data.source_length;
4120 clip.pitch_correction_points = data.pitch_correction_points.clone();
4121 clip.pitch_correction_frame_likeness = data.pitch_correction_frame_likeness;
4122 clip.pitch_correction_inertia_ms = data.pitch_correction_inertia_ms;
4123 clip.pitch_correction_formant_compensation = data.pitch_correction_formant_compensation;
4124 clip.plugin_graph_json = data.plugin_graph_json.clone();
4125 clip.grouped_clips = data
4126 .grouped_clips
4127 .iter()
4128 .map(Self::audio_clip_from_data)
4129 .collect();
4130 for child in &mut clip.grouped_clips {
4131 child.fade_enabled = false;
4132 child.fade_in_samples = 0;
4133 child.fade_out_samples = 0;
4134 }
4135 clip
4136 }
4137
4138 fn midi_clip_from_data(data: &crate::message::MidiClipData) -> MIDIClip {
4139 let mut clip = MIDIClip::new(
4140 data.name.clone(),
4141 data.start,
4142 data.start.saturating_add(data.length.max(1)),
4143 );
4144 clip.id = data.id.clone();
4145 clip.offset = data.offset;
4146 clip.input_channel = data.input_channel;
4147 clip.muted = data.muted;
4148 clip.grouped_clips = data
4149 .grouped_clips
4150 .iter()
4151 .map(Self::midi_clip_from_data)
4152 .collect();
4153 clip
4154 }
4155
4156 fn add_grouped_clip_to_track(
4157 &self,
4158 track_name: &str,
4159 kind: Kind,
4160 audio_clip: Option<crate::message::AudioClipData>,
4161 midi_clip: Option<crate::message::MidiClipData>,
4162 ) {
4163 if let Some(track) = self.state.lock().tracks.get(track_name) {
4164 let track = track.lock();
4165 if track.is_master {
4166 return;
4167 }
4168 match kind {
4169 Kind::Audio => {
4170 if let Some(mut clip) = audio_clip.map(|clip| Self::audio_clip_from_data(&clip))
4171 {
4172 let max_lane = track.audio.ins.len().saturating_sub(1);
4173 clip.input_channel = clip.input_channel.min(max_lane);
4174 track.audio.clips.push(clip);
4175 #[cfg(unix)]
4176 track.clip_pitch_shifters.clear();
4177 }
4178 }
4179 Kind::MIDI => {
4180 if let Some(mut clip) = midi_clip.map(|clip| Self::midi_clip_from_data(&clip)) {
4181 let max_lane = track.midi.ins.len().saturating_sub(1);
4182 clip.input_channel = clip.input_channel.min(max_lane);
4183 track.midi.clips.push(clip);
4184 }
4185 }
4186 }
4187 }
4188 }
4189
4190 fn remove_clips_from_track(&self, track_name: &str, kind: Kind, clip_indices: &[usize]) {
4191 if let Some(track) = self.state.lock().tracks.get(track_name) {
4192 let track = track.lock();
4193 let mut indices = clip_indices.to_vec();
4194 indices.sort_unstable();
4195 indices.dedup();
4196 match kind {
4197 Kind::Audio => {
4198 for idx in indices.into_iter().rev() {
4199 if idx < track.audio.clips.len() {
4200 track.audio.clips.remove(idx);
4201 }
4202 }
4203 #[cfg(unix)]
4204 track.clip_pitch_shifters.clear();
4205 }
4206 Kind::MIDI => {
4207 for idx in indices.into_iter().rev() {
4208 if idx < track.midi.clips.len() {
4209 track.midi.clips.remove(idx);
4210 }
4211 }
4212 }
4213 }
4214 }
4215 }
4216
4217 fn rename_clip_references(
4218 &self,
4219 track_name: &str,
4220 kind: Kind,
4221 clip_index: usize,
4222 new_name: &str,
4223 ) {
4224 let Some(track) = self.state.lock().tracks.get(track_name) else {
4225 return;
4226 };
4227 let track = track.lock();
4228 let old_name = match kind {
4229 Kind::Audio => {
4230 if clip_index >= track.audio.clips.len() {
4231 return;
4232 }
4233 track.audio.clips[clip_index].name.clone()
4234 }
4235 Kind::MIDI => {
4236 if clip_index >= track.midi.clips.len() {
4237 return;
4238 }
4239 track.midi.clips[clip_index].name.clone()
4240 }
4241 };
4242
4243 let new_file_name = match kind {
4244 Kind::Audio => format!("audio/{}.wav", new_name),
4245 Kind::MIDI => {
4246 let ext = std::path::Path::new(&old_name)
4247 .extension()
4248 .and_then(|e| e.to_str())
4249 .map(|s| s.to_ascii_lowercase())
4250 .filter(|e| e == "mid" || e == "midi")
4251 .unwrap_or_else(|| "mid".to_string());
4252 format!("midi/{}.{}", new_name, ext)
4253 }
4254 };
4255 let _ = track;
4256
4257 for (_, other_track) in self.state.lock().tracks.iter() {
4258 let other_track = other_track.lock();
4259 match kind {
4260 Kind::Audio => {
4261 for clip in &mut other_track.audio.clips {
4262 if clip.name == old_name {
4263 clip.name = new_file_name.clone();
4264 }
4265 if clip.pitch_correction_source_name.as_deref() == Some(old_name.as_str()) {
4266 clip.pitch_correction_source_name = Some(new_file_name.clone());
4267 }
4268 }
4269 }
4270 Kind::MIDI => {
4271 for clip in &mut other_track.midi.clips {
4272 if clip.name == old_name {
4273 clip.name = new_file_name.clone();
4274 }
4275 }
4276 }
4277 }
4278 }
4279 }
4280
4281 fn set_clip_fade(
4282 &self,
4283 track_name: &str,
4284 clip_index: usize,
4285 kind: Kind,
4286 fade_enabled: bool,
4287 fade_in_samples: usize,
4288 fade_out_samples: usize,
4289 ) {
4290 let Some(track) = self.state.lock().tracks.get(track_name) else {
4291 return;
4292 };
4293 let track = track.lock();
4294 match kind {
4295 Kind::Audio => {
4296 if let Some(clip) = track.audio.clips.get_mut(clip_index) {
4297 clip.fade_enabled = fade_enabled;
4298 clip.fade_in_samples = fade_in_samples;
4299 clip.fade_out_samples = fade_out_samples;
4300 }
4301 }
4302 Kind::MIDI => {}
4303 }
4304 }
4305
4306 fn set_clip_bounds(
4307 &self,
4308 track_name: &str,
4309 clip_index: usize,
4310 kind: Kind,
4311 start: usize,
4312 length: usize,
4313 offset: usize,
4314 ) {
4315 let Some(track) = self.state.lock().tracks.get(track_name) else {
4316 return;
4317 };
4318 let track = track.lock();
4319 match kind {
4320 Kind::Audio => {
4321 if let Some(clip) = track.audio.clips.get_mut(clip_index) {
4322 clip.start = start;
4323 clip.end = start.saturating_add(length.max(1));
4324 clip.offset = offset;
4325 clip.pitch_correction_preview_name = None;
4326 clip.pitch_correction_source_name = None;
4327 clip.pitch_correction_source_offset = None;
4328 clip.pitch_correction_source_length = None;
4329 clip.pitch_correction_points.clear();
4330 clip.pitch_correction_frame_likeness = None;
4331 clip.pitch_correction_inertia_ms = None;
4332 clip.pitch_correction_formant_compensation = None;
4333 }
4334 #[cfg(unix)]
4335 track.clip_pitch_shifters.clear();
4336 }
4337 Kind::MIDI => {
4338 if let Some(clip) = track.midi.clips.get_mut(clip_index) {
4339 clip.start = start;
4340 clip.end = start.saturating_add(length.max(1));
4341 clip.offset = offset;
4342 }
4343 }
4344 }
4345 }
4346
4347 fn set_clip_source_name(&self, track_name: &str, clip_index: usize, kind: Kind, name: String) {
4348 let Some(track) = self.state.lock().tracks.get(track_name) else {
4349 return;
4350 };
4351 let track = track.lock();
4352 match kind {
4353 Kind::Audio => {
4354 if let Some(clip) = track.audio.clips.get_mut(clip_index) {
4355 clip.name = name;
4356 }
4357 #[cfg(unix)]
4358 track.clip_pitch_shifters.clear();
4359 }
4360 Kind::MIDI => {
4361 if let Some(clip) = track.midi.clips.get_mut(clip_index) {
4362 clip.name = name;
4363 }
4364 }
4365 }
4366 }
4367
4368 fn set_clip_muted(&self, track_name: &str, clip_index: usize, kind: Kind, muted: bool) {
4369 let Some(track) = self.state.lock().tracks.get(track_name) else {
4370 return;
4371 };
4372 let track = track.lock();
4373 match kind {
4374 Kind::Audio => {
4375 if let Some(clip) = track.audio.clips.get_mut(clip_index) {
4376 clip.muted = muted;
4377 }
4378 }
4379 Kind::MIDI => {
4380 if let Some(clip) = track.midi.clips.get_mut(clip_index) {
4381 clip.muted = muted;
4382 }
4383 }
4384 }
4385 }
4386
4387 #[allow(clippy::too_many_arguments)]
4388 fn set_clip_pitch_correction(
4389 &self,
4390 track_name: &str,
4391 clip_index: usize,
4392 preview_name: Option<String>,
4393 source_name: Option<String>,
4394 source_offset: Option<usize>,
4395 source_length: Option<usize>,
4396 pitch_correction_points: Vec<crate::message::PitchCorrectionPointData>,
4397 pitch_correction_frame_likeness: Option<f32>,
4398 pitch_correction_inertia_ms: Option<u16>,
4399 pitch_correction_formant_compensation: Option<bool>,
4400 ) {
4401 if let Some(track) = self.state.lock().tracks.get(track_name) {
4402 let track = track.lock();
4403 if let Some(clip) = track.audio.clips.get_mut(clip_index) {
4404 clip.pitch_correction_preview_name = preview_name;
4405 clip.pitch_correction_source_name = source_name;
4406 clip.pitch_correction_source_offset = source_offset;
4407 clip.pitch_correction_source_length = source_length;
4408 clip.pitch_correction_points = pitch_correction_points;
4409 clip.pitch_correction_frame_likeness = pitch_correction_frame_likeness;
4410 clip.pitch_correction_inertia_ms = pitch_correction_inertia_ms;
4411 clip.pitch_correction_formant_compensation = pitch_correction_formant_compensation;
4412 }
4413 #[cfg(unix)]
4414 track.clip_pitch_shifters.clear();
4415 }
4416 }
4417
4418 async fn request_hw_cycle(&mut self) {
4419 if self.awaiting_hwfinished {
4420 tracing::debug!("request_hw_cycle skipped (already awaiting)");
4421 return;
4422 }
4423 tracing::debug!("request_hw_cycle sending TracksFinished");
4424 self.apply_hw_out_gain_and_meter().await;
4425 if let Some((after_frames, loop_start, cycle_end_sample)) =
4426 self.scheduled_loop_wrap_for_next_cycle()
4427 {
4428 self.notified_loop_wrap_sample = Some(cycle_end_sample);
4429 self.notify_clients(Ok(Action::TransportPositionAt {
4430 sample: loop_start,
4431 after_frames,
4432 }))
4433 .await;
4434 } else {
4435 self.notified_loop_wrap_sample = None;
4436 }
4437 if let Some(worker) = &self.hw_worker {
4438 if !self.pending_hw_midi_out_events_by_device.is_empty() {
4439 let out_events = std::mem::take(&mut self.pending_hw_midi_out_events_by_device);
4440 if let Err(e) = worker.tx.send(Message::HWMidiOutEvents(out_events)).await {
4441 error!("Error sending HWMidiOutEvents {e}");
4442 }
4443 }
4444 match worker.tx.send(Message::TracksFinished).await {
4445 Ok(_) => {
4446 self.awaiting_hwfinished = true;
4447 }
4448 Err(e) => {
4449 error!("Error sending TracksFinished {e}");
4450 }
4451 }
4452 }
4453 }
4454
4455 async fn clear_hw_midi_output_state(&mut self, send_panic: bool) {
4456 self.pending_hw_midi_out_events.clear();
4457 self.pending_hw_midi_out_events_by_device.clear();
4458 {
4459 let state = self.state.lock();
4460 for track in state.tracks.values() {
4461 track.lock().take_hw_midi_out_events();
4462 }
4463 }
4464
4465 let panic_events = if send_panic {
4466 self.note_off_events_for_all_active_tracks()
4467 } else {
4468 vec![]
4469 };
4470
4471 if let Some(worker) = &self.hw_worker {
4472 if let Err(e) = worker.tx.send(Message::ClearHWMidiOutEvents).await {
4473 error!("Error clearing pending HWMidiOutEvents {e}");
4474 }
4475 if !panic_events.is_empty()
4476 && let Err(e) = worker.tx.send(Message::HWMidiOutEvents(panic_events)).await
4477 {
4478 error!("Error sending transport restart MIDI panic events {e}");
4479 }
4480 } else if !panic_events.is_empty() {
4481 self.pending_hw_midi_out_events_by_device
4482 .extend(panic_events);
4483 }
4484 }
4485
4486 fn invalidate_track_cycle_state(&mut self) {
4487 self.track_process_epoch = self.track_process_epoch.saturating_add(1);
4488 self.task_processing_started_at.clear();
4489 self.cycle_tasks.clear();
4490 self.cycle_task_deps.clear();
4491 self.cycle_tasks_running.clear();
4492 self.cycle_tasks_finished.clear();
4493 let state = self.state.lock();
4494 for track in state.tracks.values() {
4495 let t = track.lock();
4496 t.audio.finished = false;
4497 t.audio.processing = false;
4498 }
4499 }
4500
4501 fn force_stalled_task_completions(&mut self) {
4502 let now = Instant::now();
4503 let running: Vec<ProcessTask> = self.cycle_tasks_running.clone();
4504 for task in running {
4505 let key = Self::task_key(&task);
4506 let Some(started) = self.task_processing_started_at.get(&key).copied() else {
4507 continue;
4508 };
4509 if now.duration_since(started) < Self::TRACK_PROCESS_TIMEOUT {
4510 continue;
4511 }
4512 if Self::task_running_finished_contains(&self.cycle_tasks_finished, &task) {
4513 self.task_processing_started_at.remove(&key);
4514 continue;
4515 }
4516 let track = match &task {
4517 ProcessTask::Track(t)
4518 | ProcessTask::FolderInput(t)
4519 | ProcessTask::FolderOutput(t) => t.clone(),
4520 ProcessTask::Plugin { track, .. } => track.clone(),
4521 };
4522 {
4523 let t = track.lock();
4524 if t.audio.finished || !t.audio.processing {
4525 self.task_processing_started_at.remove(&key);
4526 continue;
4527 }
4528 for out in &t.audio.outs {
4529 out.buffer.lock().fill(0.0);
4530 *out.finished.lock() = true;
4531 }
4532 t.audio.processing = false;
4533 t.audio.finished = true;
4534 }
4535 self.cycle_tasks_running
4536 .retain(|t| Self::task_key(t) != key);
4537 self.cycle_tasks_finished.push(task.clone());
4538 self.task_processing_started_at.remove(&key);
4539 tracing::warn!(
4540 "Task '{}' exceeded process timeout ({} ms); forcing silent completion for cycle",
4541 Self::task_track_name(&task),
4542 Self::TRACK_PROCESS_TIMEOUT.as_millis()
4543 );
4544 }
4545 }
4546
4547 fn should_publish_hw_out_meters(&mut self) -> bool {
4548 let now = Instant::now();
4549 match self.last_hw_out_meter_publish {
4550 Some(last) if now.duration_since(last) < Self::METER_PUBLISH_INTERVAL => false,
4551 _ => {
4552 self.last_hw_out_meter_publish = Some(now);
4553 true
4554 }
4555 }
4556 }
4557
4558 fn should_publish_track_meters(&mut self) -> bool {
4559 let now = Instant::now();
4560 match self.last_track_meter_publish {
4561 Some(last) if now.duration_since(last) < Self::METER_PUBLISH_INTERVAL => false,
4562 _ => {
4563 self.last_track_meter_publish = Some(now);
4564 true
4565 }
4566 }
4567 }
4568
4569 fn should_publish_hw_out_linear(&mut self, peaks_linear: &[f32]) -> bool {
4570 #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
4571 {
4572 self.hw_out_meter_publish_phase = !self.hw_out_meter_publish_phase;
4573 if !self.hw_out_meter_publish_phase {
4574 return false;
4575 }
4576 let changed = if self.last_hw_out_meter_linear.len() != peaks_linear.len() {
4577 true
4578 } else {
4579 self.last_hw_out_meter_linear
4580 .iter()
4581 .zip(peaks_linear.iter())
4582 .any(|(prev, next)| (prev - next).abs() >= Self::HW_OUT_METER_LINEAR_EPSILON)
4583 };
4584 if !changed {
4585 return false;
4586 }
4587 self.last_hw_out_meter_linear.clear();
4588 self.last_hw_out_meter_linear
4589 .extend_from_slice(peaks_linear);
4590 true
4591 }
4592 #[cfg(not(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd")))]
4593 {
4594 let _ = peaks_linear;
4595 false
4596 }
4597 }
4598
4599 async fn maybe_notify_hw_out_meter(&mut self, _meter_db: Vec<f32>) {
4600 {}
4601 }
4602
4603 fn collect_changed_track_meters(
4604 &mut self,
4605 _tracks: &[(String, Arc<UnsafeMutex<Box<Track>>>)],
4606 ) -> Vec<(String, Vec<f32>)> {
4607 Vec::new()
4608 }
4609
4610 async fn apply_hw_out_gain_and_meter(&mut self) {
4611 let gain = if self.hw_out_muted {
4612 0.0
4613 } else {
4614 10.0_f32.powf(self.hw_out_level_db / 20.0)
4615 };
4616 let should_notify_interval = self.should_publish_hw_out_meters();
4617 if let Some(oss) = self.hw_driver.clone() {
4618 let hw = oss.lock();
4619 hw.set_output_gain_balance(gain, self.hw_out_balance);
4620 if !should_notify_interval {
4621 return;
4622 }
4623 } else {
4624 #[cfg(unix)]
4625 {
4626 if let Some(jack) = self.jack_runtime.clone() {
4627 jack.lock().set_output_gain_linear(gain);
4628 jack.lock().set_output_balance(self.hw_out_balance);
4629 if !should_notify_interval {
4630 return;
4631 }
4632 } else {
4633 return;
4634 }
4635 }
4636 #[cfg(not(unix))]
4637 {
4638 return;
4639 }
4640 }
4641 let peaks_linear = if let Some(oss) = self.hw_driver.clone() {
4642 oss.lock().output_meter_linear(gain, self.hw_out_balance)
4643 } else {
4644 #[cfg(unix)]
4645 {
4646 if let Some(jack) = self.jack_runtime.clone() {
4647 let outs = jack.lock().audio_outs();
4648 let out_count = outs.len();
4649 let b = if out_count == 2 {
4650 self.hw_out_balance.clamp(-1.0, 1.0)
4651 } else {
4652 0.0
4653 };
4654 let mut meters_linear = Vec::with_capacity(out_count);
4655 for (channel_idx, channel) in outs.iter().enumerate() {
4656 let balance_gain = if out_count == 2 {
4657 if channel_idx == 0 {
4658 (1.0 - b).clamp(0.0, 1.0)
4659 } else {
4660 (1.0 + b).clamp(0.0, 1.0)
4661 }
4662 } else {
4663 1.0
4664 };
4665 let buf = channel.buffer.lock();
4666 let peak = crate::simd::peak_abs(buf) * gain * balance_gain;
4667 meters_linear.push(peak);
4668 }
4669 meters_linear
4670 } else {
4671 return;
4672 }
4673 }
4674 #[cfg(not(unix))]
4675 {
4676 return;
4677 }
4678 };
4679 if self.hw_out_peak_hold_linear.len() != peaks_linear.len() {
4680 self.hw_out_peak_hold_linear.resize(peaks_linear.len(), 0.0);
4681 }
4682 let mut held_peaks = Vec::with_capacity(peaks_linear.len());
4683 for (idx, peak_now) in peaks_linear.iter().copied().enumerate() {
4684 let held = self.hw_out_peak_hold_linear[idx] * 0.92;
4685 let next = peak_now.max(held);
4686 self.hw_out_peak_hold_linear[idx] = next;
4687 held_peaks.push(next);
4688 }
4689 let should_notify =
4690 should_notify_interval && self.should_publish_hw_out_linear(&held_peaks);
4691 let meter_db: Vec<f32> = held_peaks
4692 .into_iter()
4693 .map(Self::meter_linear_to_db)
4694 .collect();
4695 self.latest_hw_out_meter_db = Arc::new(meter_db.clone());
4696 if should_notify {
4697 self.maybe_notify_hw_out_meter(meter_db).await;
4698 }
4699 }
4700
4701 fn preload_track_clips_spawn(&self) {
4702 let tracks: Vec<_> = self.state.lock().tracks.values().cloned().collect();
4703 for track in tracks {
4704 tokio::task::spawn_blocking(move || {
4705 track.lock().preload_clips();
4706 });
4707 }
4708 }
4709
4710 async fn preload_track_clips(&self) {
4711 let tracks: Vec<_> = self.state.lock().tracks.values().cloned().collect();
4712 if tracks.is_empty() {
4713 return;
4714 }
4715 let mut handles = Vec::with_capacity(tracks.len());
4716 for track in tracks {
4717 handles.push(tokio::task::spawn_blocking(move || {
4718 track.lock().preload_clips();
4719 }));
4720 }
4721 for handle in handles {
4722 if let Err(e) = handle.await {
4723 tracing::warn!("Clip preload task panicked: {e}");
4724 }
4725 }
4726 }
4727
4728 fn build_task_graph(
4729 &self,
4730 ) -> (
4731 Vec<ProcessTask>,
4732 std::collections::HashMap<String, Vec<String>>,
4733 ) {
4734 let state = self.state.lock();
4735 let ordered: Vec<(String, Arc<UnsafeMutex<Box<Track>>>)> = state
4736 .tracks
4737 .iter()
4738 .map(|(name, track)| (name.clone(), track.clone()))
4739 .collect();
4740 let mut tasks = Vec::new();
4741 let mut deps = std::collections::HashMap::new();
4742
4743 for (_name, track) in &ordered {
4744 let t = track.lock();
4745 if t.parent_track.is_some() {
4746 continue;
4747 }
4748 self.append_track_tasks(track.clone(), None, &mut tasks, &mut deps);
4749 }
4750
4751 (tasks, deps)
4752 }
4753
4754 fn append_track_tasks(
4755 &self,
4756 track: Arc<UnsafeMutex<Box<Track>>>,
4757 predecessor: Option<String>,
4758 tasks: &mut Vec<ProcessTask>,
4759 deps: &mut std::collections::HashMap<String, Vec<String>>,
4760 ) -> (String, String) {
4761 use crate::message::ConnectableRef;
4762 let t = track.lock();
4763 if t.is_folder {
4764 let folder_input = ProcessTask::FolderInput(track.clone());
4765 let folder_input_key = Self::task_key(&folder_input);
4766 tasks.push(folder_input.clone());
4767 let folder_input_deps: Vec<_> = predecessor.into_iter().collect();
4768 deps.insert(folder_input_key.clone(), folder_input_deps);
4769
4770 let mut source_keys: std::collections::HashMap<ConnectableRef, String> =
4771 std::collections::HashMap::new();
4772 let mut target_keys: std::collections::HashMap<ConnectableRef, String> =
4773 std::collections::HashMap::new();
4774 source_keys.insert(ConnectableRef::TrackInput, folder_input_key.clone());
4775 target_keys.insert(ConnectableRef::TrackInput, folder_input_key.clone());
4776
4777 let mut plugin_keys: Vec<String> = Vec::new();
4778 for idx in 0..t.clap_plugins.len() {
4779 let plugin_task = ProcessTask::Plugin {
4780 track: track.clone(),
4781 kind: PluginKind::Clap,
4782 index: idx,
4783 };
4784 let plugin_key = Self::task_key(&plugin_task);
4785 let id = t.clap_plugins[idx].id;
4786 source_keys.insert(ConnectableRef::ClapPlugin(id), plugin_key.clone());
4787 target_keys.insert(ConnectableRef::ClapPlugin(id), plugin_key.clone());
4788 tasks.push(plugin_task);
4789 deps.insert(plugin_key.clone(), vec![folder_input_key.clone()]);
4790 plugin_keys.push(plugin_key);
4791 }
4792 for idx in 0..t.vst3_plugins.len() {
4793 let plugin_task = ProcessTask::Plugin {
4794 track: track.clone(),
4795 kind: PluginKind::Vst3,
4796 index: idx,
4797 };
4798 let plugin_key = Self::task_key(&plugin_task);
4799 let id = t.vst3_plugins[idx].id;
4800 source_keys.insert(ConnectableRef::Vst3Plugin(id), plugin_key.clone());
4801 target_keys.insert(ConnectableRef::Vst3Plugin(id), plugin_key.clone());
4802 tasks.push(plugin_task);
4803 deps.insert(plugin_key.clone(), vec![folder_input_key.clone()]);
4804 plugin_keys.push(plugin_key);
4805 }
4806 #[cfg(all(unix, not(target_os = "macos")))]
4807 for idx in 0..t.lv2_plugins.len() {
4808 let plugin_task = ProcessTask::Plugin {
4809 track: track.clone(),
4810 kind: PluginKind::Lv2,
4811 index: idx,
4812 };
4813 let plugin_key = Self::task_key(&plugin_task);
4814 let id = t.lv2_plugins[idx].id;
4815 source_keys.insert(ConnectableRef::Lv2Plugin(id), plugin_key.clone());
4816 target_keys.insert(ConnectableRef::Lv2Plugin(id), plugin_key.clone());
4817 tasks.push(plugin_task);
4818 deps.insert(plugin_key.clone(), vec![folder_input_key.clone()]);
4819 plugin_keys.push(plugin_key);
4820 }
4821
4822 let mut child_keys = Vec::new();
4823 for child_track in &t.child_tracks {
4824 let (child_first, child_last) = self.append_track_tasks(
4825 child_track.clone(),
4826 Some(folder_input_key.clone()),
4827 tasks,
4828 deps,
4829 );
4830 let child_name = child_track.lock().name.clone();
4831 source_keys.insert(
4832 ConnectableRef::ChildTrack(child_name.clone()),
4833 child_last.clone(),
4834 );
4835 target_keys.insert(ConnectableRef::ChildTrack(child_name), child_first.clone());
4836 child_keys.push((child_first, child_last.clone()));
4837 }
4838
4839 let folder_output = ProcessTask::FolderOutput(track.clone());
4840 let folder_output_key = Self::task_key(&folder_output);
4841 source_keys.insert(ConnectableRef::TrackOutput, folder_output_key.clone());
4842 target_keys.insert(ConnectableRef::TrackOutput, folder_output_key.clone());
4843 tasks.push(folder_output.clone());
4844 let mut folder_output_deps = vec![folder_input_key.clone()];
4845 folder_output_deps.extend(plugin_keys);
4846 folder_output_deps.extend(child_keys.iter().map(|(_, last)| last.clone()));
4847 deps.insert(folder_output_key.clone(), folder_output_deps);
4848
4849 for conn in t.connectable_connections() {
4852 let Some(source_key) = source_keys.get(&conn.from) else {
4853 continue;
4854 };
4855 let Some(target_key) = target_keys.get(&conn.to) else {
4856 continue;
4857 };
4858 if source_key == target_key {
4859 continue;
4860 }
4861 let entry = deps.entry(target_key.clone()).or_default();
4862 if !entry.contains(source_key) {
4863 entry.push(source_key.clone());
4864 }
4865 }
4866
4867 (folder_input_key, folder_output_key)
4868 } else {
4869 let task = ProcessTask::Track(track.clone());
4870 let task_key = Self::task_key(&task);
4871 tasks.push(task.clone());
4872 deps.insert(
4873 task_key.clone(),
4874 predecessor.into_iter().collect::<Vec<_>>(),
4875 );
4876 (task_key.clone(), task_key)
4877 }
4878 }
4879
4880 fn task_track_name(task: &ProcessTask) -> String {
4881 match task {
4882 ProcessTask::Track(t) | ProcessTask::FolderInput(t) | ProcessTask::FolderOutput(t) => {
4883 t.lock().name.clone()
4884 }
4885 ProcessTask::Plugin { track, .. } => track.lock().name.clone(),
4886 }
4887 }
4888
4889 fn task_key(task: &ProcessTask) -> String {
4890 match task {
4891 ProcessTask::Track(t) => format!("Track:{:p}", std::sync::Arc::as_ptr(t)),
4892 ProcessTask::FolderInput(t) => {
4893 format!("FolderInput:{:p}", std::sync::Arc::as_ptr(t))
4894 }
4895 ProcessTask::FolderOutput(t) => {
4896 format!("FolderOutput:{:p}", std::sync::Arc::as_ptr(t))
4897 }
4898 ProcessTask::Plugin { track, kind, index } => format!(
4899 "Plugin:{:?}:{:p}:{}",
4900 kind,
4901 std::sync::Arc::as_ptr(track),
4902 index
4903 ),
4904 }
4905 }
4906
4907 fn task_running_finished_contains(haystack: &[ProcessTask], needle: &ProcessTask) -> bool {
4908 let needle_key = Self::task_key(needle);
4909 haystack.iter().any(|t| Self::task_key(t) == needle_key)
4910 }
4911
4912 fn task_ready(&self, task: &ProcessTask) -> bool {
4913 match task {
4914 ProcessTask::Track(t) | ProcessTask::FolderInput(t) => {
4915 let track = t.lock();
4916 let ready = track.audio.ready();
4917 if !ready {
4918 let task_kind = match task {
4919 ProcessTask::Track(_) => "Track",
4920 ProcessTask::FolderInput(_) => "FolderInput",
4921 _ => "?",
4922 };
4923 let mut input_status = Vec::new();
4924 for (idx, input) in track.audio.ins.iter().enumerate() {
4925 let finished = *input.finished.lock();
4926 let conn_count = input.connection_count.load(Ordering::Relaxed);
4927 let mut pending = Vec::new();
4928 for conn in input.connections.lock().iter() {
4929 pending.push(*conn.finished.lock());
4930 }
4931 input_status.push(format!(
4932 "in{}: finished={} conns={} pending_finished={:?}",
4933 idx, finished, conn_count, pending
4934 ));
4935 }
4936 tracing::info!(
4937 "task not ready for '{}' ({}): {}",
4938 track.name,
4939 task_kind,
4940 input_status.join(", ")
4941 );
4942 }
4943 ready
4944 }
4945 ProcessTask::Plugin { .. } | ProcessTask::FolderOutput(_) => true,
4946 }
4947 }
4948
4949 fn task_dependencies_satisfied(&self, task: &ProcessTask) -> bool {
4950 let key = Self::task_key(task);
4951 let Some(deps) = self.cycle_task_deps.get(&key) else {
4952 return true;
4953 };
4954 let finished_keys: std::collections::HashSet<String> = self
4955 .cycle_tasks_finished
4956 .iter()
4957 .map(Self::task_key)
4958 .collect();
4959 deps.iter().all(|d| finished_keys.contains(d))
4960 }
4961
4962 fn prepare_task_track(&self, task: &ProcessTask) {
4963 let track = match task {
4964 ProcessTask::Track(t) | ProcessTask::FolderInput(t) | ProcessTask::FolderOutput(t) => t,
4965 ProcessTask::Plugin { track, .. } => track,
4966 };
4967 let t = track.lock();
4968 let transport_sample = if self.session_clip_playback_enabled && self.playing {
4969 self.session_transport_sample
4970 } else {
4971 self.transport_sample
4972 };
4973 t.set_transport_sample(transport_sample);
4974 t.set_loop_config(self.loop_enabled, self.loop_range_samples);
4975 t.set_transport_timing(self.tempo_bpm, self.tsig_num, self.tsig_denom);
4976 t.process_epoch = self.track_process_epoch;
4977 t.set_clip_playback_enabled(self.clip_playback_enabled && self.playing);
4978 t.set_session_clip_playback_enabled(self.session_clip_playback_enabled && self.playing);
4979 t.set_record_tap_enabled(self.playing && self.record_enabled);
4980 t.audio.processing = true;
4981 }
4982
4983 async fn send_tasks(&mut self) -> bool {
4984 if !self.playing {
4985 return false;
4986 }
4987 self.refresh_realtime_infection();
4988 self.force_stalled_task_completions();
4989
4990 if self.cycle_tasks.is_empty() {
4991 let (tasks, deps) = self.build_task_graph();
4992 let task_names: Vec<String> = tasks.iter().map(Self::task_track_name).collect();
4993 tracing::debug!(
4994 "send_tasks rebuilt graph: {} tasks ({:?})",
4995 tasks.len(),
4996 task_names
4997 );
4998 self.cycle_tasks = tasks;
4999 self.cycle_task_deps = deps;
5000 self.cycle_tasks_running.clear();
5001 self.cycle_tasks_finished.clear();
5002 }
5003
5004 let mut finished = true;
5005 let mut dispatched = 0;
5006 loop {
5007 let next_task = {
5008 let mut next = None;
5009 tracing::debug!(
5010 "selecting next: cycle={} running={} finished={}",
5011 self.cycle_tasks.len(),
5012 self.cycle_tasks_running.len(),
5013 self.cycle_tasks_finished.len()
5014 );
5015 for task in &self.cycle_tasks {
5016 let in_running =
5017 Self::task_running_finished_contains(&self.cycle_tasks_running, task);
5018 let in_finished =
5019 Self::task_running_finished_contains(&self.cycle_tasks_finished, task);
5020 tracing::debug!(
5021 "checking task {} in_running={} in_finished={}",
5022 Self::task_track_name(task),
5023 in_running,
5024 in_finished
5025 );
5026 if in_finished || in_running {
5027 continue;
5028 }
5029 finished = false;
5030 if !self.task_dependencies_satisfied(task) {
5031 continue;
5032 }
5033 if !self.task_ready(task) {
5034 continue;
5035 }
5036 next = Some(task.clone());
5037 break;
5038 }
5039 next
5040 };
5041
5042 let Some(task) = next_task else {
5043 if !finished && dispatched == 0 {
5044 tracing::info!(
5045 "send_tasks returning finished={} (dispatched {})",
5046 finished,
5047 dispatched
5048 );
5049 } else {
5050 tracing::debug!(
5051 "send_tasks returning finished={} (dispatched {})",
5052 finished,
5053 dispatched
5054 );
5055 }
5056 return finished;
5057 };
5058 let Some(worker_index) = self.take_ready_worker_index() else {
5059 self.force_stalled_task_completions();
5060 tracing::debug!(
5061 "send_tasks returning false (no ready worker; dispatched {})",
5062 dispatched
5063 );
5064 return false;
5065 };
5066
5067 if Self::task_running_finished_contains(&self.cycle_tasks_finished, &task)
5068 || Self::task_running_finished_contains(&self.cycle_tasks_running, &task)
5069 {
5070 continue;
5071 }
5072 dispatched += 1;
5073 let task_key = Self::task_key(&task);
5074 tracing::info!(
5075 "send_tasks dispatching {} (running={} finished={})",
5076 Self::task_track_name(&task),
5077 self.cycle_tasks_running.len(),
5078 self.cycle_tasks_finished.len()
5079 );
5080 self.prepare_task_track(&task);
5081 self.cycle_tasks_running.push(task.clone());
5082 tracing::debug!(
5083 "inserted task {} -> running_size={}",
5084 Self::task_track_name(&task),
5085 self.cycle_tasks_running.len()
5086 );
5087 self.task_processing_started_at
5088 .insert(task_key.clone(), Instant::now());
5089 let worker = &self.workers[worker_index];
5090 if let Err(e) = worker.tx.send(Message::ProcessTask(task.clone())).await {
5091 self.cycle_tasks_running
5092 .retain(|t| Self::task_key(t) != task_key);
5093 self.task_processing_started_at.remove(&task_key);
5094 self.notify_clients(Err(format!("Failed to send task to worker: {}", e)))
5095 .await;
5096 }
5097 }
5098 }
5099
5100 async fn on_all_tracks_finished(&mut self) {
5101 if self.transport_restart_pending {
5102 let state = self.state.lock();
5103 for track in state.tracks.values() {
5104 track.lock().take_hw_midi_out_events();
5105 }
5106 } else if self.hw_worker.is_some() {
5107 self.active_hw_notes_cycle_start = self.active_hw_notes_by_track.clone();
5108 let mut out_events = self.collect_hw_midi_output_events_by_device();
5109 if self.loop_enabled
5110 && let Some((_, loop_end)) = self.loop_range_samples
5111 {
5112 let cycle_end = self
5113 .transport_sample
5114 .saturating_add(self.current_cycle_samples());
5115 if self.transport_sample < loop_end && cycle_end >= loop_end {
5116 let wrap_frame = loop_end
5117 .saturating_sub(self.transport_sample)
5118 .min(self.current_cycle_samples())
5119 as u32;
5120 out_events.extend(self.note_off_events_for_active_snapshot(
5121 &self.active_hw_notes_cycle_start,
5122 wrap_frame,
5123 ));
5124 out_events.sort_by(|a, b| {
5125 a.event
5126 .frame
5127 .cmp(&b.event.frame)
5128 .then_with(|| a.device.cmp(&b.device))
5129 });
5130 }
5131 }
5132 self.pending_hw_midi_out_events_by_device.extend(out_events);
5133 } else {
5134 self.pending_hw_midi_out_events = self.collect_hw_midi_output_events();
5135 }
5136 self.request_hw_cycle().await;
5137 }
5138
5139 fn take_ready_worker_index(&mut self) -> Option<usize> {
5140 while !self.ready_workers.is_empty() {
5141 let worker_index = self.ready_workers.remove(0);
5142 if worker_index < self.workers.len() {
5143 return Some(worker_index);
5144 }
5145 }
5146 None
5147 }
5148
5149 fn push_ready_worker(&mut self, worker_index: usize) {
5150 self.ready_workers.push(worker_index);
5151 }
5152
5153 async fn publish_track_meters(&mut self) {
5154 if !self.should_publish_track_meters() {
5155 return;
5156 }
5157 let tracks: Vec<(String, Arc<UnsafeMutex<Box<Track>>>)> = self
5158 .state
5159 .lock()
5160 .tracks
5161 .iter()
5162 .map(|(name, track)| (name.clone(), track.clone()))
5163 .collect();
5164 let mut snapshot = Vec::with_capacity(tracks.len());
5165 for (name, track) in &tracks {
5166 let linear = self
5167 .track_meter_linear_by_track
5168 .get(name)
5169 .cloned()
5170 .unwrap_or_else(|| track.lock().output_meter_linear());
5171 let output_db = linear
5172 .iter()
5173 .copied()
5174 .map(Self::meter_linear_to_db)
5175 .collect::<Vec<_>>();
5176 snapshot.push((name.clone(), output_db));
5177 }
5178 self.latest_track_meter_snapshot = Arc::new(snapshot);
5179 let meters = self.collect_changed_track_meters(&tracks);
5180 for (track_name, output_db) in meters {
5181 self.notify_clients(Ok(Action::TrackMeters {
5182 track_name,
5183 output_db,
5184 }))
5185 .await;
5186 }
5187 }
5188
5189 async fn publish_session_runtime_reports(&mut self) {
5190 if self
5191 .last_session_report_publish
5192 .is_some_and(|t| t.elapsed() < Self::SESSION_RUNTIME_REPORT_INTERVAL)
5193 {
5194 return;
5195 }
5196
5197 let mut current = HashMap::<(String, usize), (SessionSlotState, usize, usize)>::new();
5198 {
5199 let state = self.state.lock();
5200 for (track_name, track) in &state.tracks {
5201 let track = track.lock();
5202 for launch in &track.pending_session_launches {
5203 current.insert(
5204 (track_name.clone(), launch.scene_index),
5205 (SessionSlotState::Queued, 0, 0),
5206 );
5207 }
5208 for clip in &track.playing_session_clips {
5209 current.insert(
5210 (track_name.clone(), clip.scene_index),
5211 (
5212 SessionSlotState::Playing,
5213 clip.play_position_samples,
5214 clip.elapsed_samples,
5215 ),
5216 );
5217 }
5218 }
5219 }
5220
5221 let previous_keys: Vec<(String, usize)> =
5222 self.session_report_state.keys().cloned().collect();
5223 for key in previous_keys {
5224 if current.contains_key(&key) {
5225 continue;
5226 }
5227 let (track_name, scene_index) = key;
5228 self.notify_clients(Ok(Action::SessionRuntimeReport {
5229 track_name,
5230 scene_index,
5231 state: SessionSlotState::Stopped,
5232 play_position_samples: 0,
5233 elapsed_samples: 0,
5234 }))
5235 .await;
5236 }
5237
5238 for ((track_name, scene_index), (state, play_position_samples, elapsed_samples)) in ¤t
5239 {
5240 self.notify_clients(Ok(Action::SessionRuntimeReport {
5241 track_name: track_name.clone(),
5242 scene_index: *scene_index,
5243 state: *state,
5244 play_position_samples: *play_position_samples,
5245 elapsed_samples: *elapsed_samples,
5246 }))
5247 .await;
5248 }
5249
5250 self.session_report_state = current.into_iter().map(|(k, (s, _, _))| (k, s)).collect();
5251 self.last_session_report_publish = Some(Instant::now());
5252 }
5253
5254 async fn publish_clap_state_dirty(&mut self) {
5255 let tracks: Vec<(String, Arc<UnsafeMutex<Box<Track>>>)> = self
5256 .state
5257 .lock()
5258 .tracks
5259 .iter()
5260 .map(|(name, track)| (name.clone(), track.clone()))
5261 .collect();
5262 for (track_name, track) in &tracks {
5263 let dirty = track.lock().take_dirty_clap_instances();
5264 for instance_id in dirty {
5265 self.notify_clients(Ok(Action::TrackClapStateDirty {
5266 track_name: track_name.clone(),
5267 instance_id,
5268 }))
5269 .await;
5270 }
5271 }
5272 }
5273
5274 fn reset_meters_after_stop(&mut self) {
5275 self.last_hw_out_meter_publish = None;
5276 self.last_track_meter_publish = None;
5277 self.hw_out_peak_hold_linear.fill(0.0);
5278 #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
5279 {
5280 self.last_hw_out_meter_linear.clear();
5281 }
5282 let hw_channels = self.latest_hw_out_meter_db.len();
5283 self.latest_hw_out_meter_db = Arc::new(vec![-90.0; hw_channels]);
5284
5285 let tracks: Vec<(String, Arc<UnsafeMutex<Box<Track>>>)> = self
5286 .state
5287 .lock()
5288 .tracks
5289 .iter()
5290 .map(|(name, track)| (name.clone(), track.clone()))
5291 .collect();
5292 self.track_meter_linear_by_track.clear();
5293 let mut snapshot = Vec::with_capacity(tracks.len());
5294 for (name, track) in tracks {
5295 let t = track.lock();
5296 t.clear_output_meters();
5297 let width = t.output_meter_linear().len();
5298 let zero_linear = vec![0.0; width];
5299 self.track_meter_linear_by_track
5300 .insert(name.clone(), zero_linear);
5301 snapshot.push((name, vec![-90.0; width]));
5302 }
5303 self.latest_track_meter_snapshot = Arc::new(snapshot);
5304 }
5305
5306 pub fn check_if_leads_to_kind(
5307 &self,
5308 kind: Kind,
5309 current_track_name: &str,
5310 target_track_name: &str,
5311 ) -> bool {
5312 routing::would_create_cycle(
5313 &target_track_name.to_string(),
5314 ¤t_track_name.to_string(),
5315 |track_name| self.connected_neighbors(kind, track_name),
5316 )
5317 }
5318
5319 fn connected_neighbors(&self, kind: Kind, current_track_name: &str) -> Vec<String> {
5320 let state = self.state.lock();
5321 let mut found_neighbors = Vec::new();
5322
5323 if let Some(current_track_handle) = state.tracks.get(current_track_name) {
5324 let current_track = current_track_handle.lock();
5325
5326 match kind {
5327 Kind::Audio => {
5328 for out_port in ¤t_track.audio.outs {
5329 let conns = out_port.connections.lock();
5330 for conn in conns.iter() {
5331 for (name, next_track_handle) in &state.tracks {
5332 let next_track = next_track_handle.lock();
5333 let is_connected =
5334 next_track.audio.ins.iter().any(|ins_port| {
5335 Arc::ptr_eq(&ins_port.buffer, &conn.buffer)
5336 });
5337
5338 if is_connected {
5339 found_neighbors.push(name.clone());
5340 }
5341 }
5342 }
5343 }
5344 }
5345 Kind::MIDI => {
5346 for out_port in ¤t_track.midi.outs {
5347 let conns = out_port.lock().connections.clone();
5348 for conn in conns.iter() {
5349 for (name, next_track_handle) in &state.tracks {
5350 let next_track = next_track_handle.lock();
5351 let is_connected = next_track
5352 .midi
5353 .ins
5354 .iter()
5355 .any(|ins_port| Arc::ptr_eq(ins_port, conn));
5356
5357 if is_connected {
5358 found_neighbors.push(name.clone());
5359 }
5360 }
5361 }
5362 }
5363 }
5364 }
5365 }
5366 found_neighbors
5367 }
5368
5369 async fn handle_request(&mut self, a: Action) {
5370 match a {
5371 Action::Log { source, message } => {
5372 self.notify_clients(Ok(Action::Log { source, message }))
5373 .await;
5374 }
5375 Action::Undo => {
5376 let actions = match self.history.undo() {
5377 Some(actions) => actions,
5378 None => {
5379 self.notify_clients(Ok(Action::Undo)).await;
5380 self.notify_clients(Ok(Action::HistoryState {
5381 dirty: self.history.is_dirty(),
5382 }))
5383 .await;
5384 return;
5385 }
5386 };
5387
5388 let was_suspended = self.history_suspended;
5389 self.history_suspended = true;
5390 for action in actions {
5391 self.handle_request_inner(action, false).await;
5392 }
5393 self.history_suspended = was_suspended;
5394 self.notify_clients(Ok(Action::Undo)).await;
5395 self.notify_clients(Ok(Action::HistoryState {
5396 dirty: self.history.is_dirty(),
5397 }))
5398 .await;
5399 }
5400 Action::Redo => {
5401 let actions = match self.history.redo() {
5402 Some(actions) => actions,
5403 None => {
5404 self.notify_clients(Ok(Action::Redo)).await;
5405 self.notify_clients(Ok(Action::HistoryState {
5406 dirty: self.history.is_dirty(),
5407 }))
5408 .await;
5409 return;
5410 }
5411 };
5412
5413 let was_suspended = self.history_suspended;
5414 self.history_suspended = true;
5415 for action in actions {
5416 self.handle_request_inner(action, false).await;
5417 }
5418 self.history_suspended = was_suspended;
5419 self.notify_clients(Ok(Action::Redo)).await;
5420 self.notify_clients(Ok(Action::HistoryState {
5421 dirty: self.history.is_dirty(),
5422 }))
5423 .await;
5424 }
5425 Action::ApplyGroupedActions(actions) => {
5426 self.handle_request_inner(Action::BeginHistoryGroup, true)
5427 .await;
5428 for action in actions {
5429 self.handle_request_inner(action, true).await;
5430 }
5431 self.handle_request_inner(Action::EndHistoryGroup, true)
5432 .await;
5433 }
5434 Action::Session(_) => {
5435 self.handle_request_inner(a, false).await;
5436 }
5437 other => {
5438 self.handle_request_inner(other, true).await;
5439 }
5440 }
5441 }
5442
5443 async fn handle_session_action(&mut self, action: SessionAction) {
5444 let sample_rate = self.sample_rate();
5445 let bpm = self.tempo_bpm;
5446 let tsig_num = self.tsig_num;
5447 let tsig_denom = self.tsig_denom;
5448 let session_active = self.session_clip_playback_enabled && self.playing;
5449 let quantize_reference_sample = if session_active {
5450 self.session_transport_sample
5451 } else {
5452 self.transport_sample
5453 };
5454 let quantize = |sample: usize, quantization: LaunchQuantization| -> usize {
5455 if !self.transport_running && !session_active {
5456 return sample;
5457 }
5458 Track::quantize_sample_to_boundary(
5459 sample,
5460 quantization,
5461 bpm,
5462 tsig_num,
5463 tsig_denom,
5464 sample_rate,
5465 )
5466 };
5467
5468 match action {
5469 SessionAction::LaunchClip {
5470 track_name,
5471 scene_index,
5472 clip_id,
5473 launch_quantization,
5474 loop_enabled,
5475 loop_start_samples,
5476 loop_end_samples,
5477 } => {
5478 let Some(track) = self.track_handle_by_name(&track_name) else {
5479 tracing::warn!("Session launch for unknown track '{}'", track_name);
5480 return;
5481 };
5482 let track = track.lock();
5483 let clip_id = if clip_id.is_empty() {
5484 track
5485 .session_slots
5486 .get(&scene_index)
5487 .map(|slot| slot.clip_id.clone())
5488 .unwrap_or_default()
5489 } else {
5490 clip_id
5491 };
5492 let kind = if track.audio.clips.iter().any(|c| c.id == clip_id) {
5493 Kind::Audio
5494 } else if track.midi.clips.iter().any(|c| c.id == clip_id) {
5495 Kind::MIDI
5496 } else {
5497 tracing::warn!(
5498 "Session launch for unknown clip '{}' on track '{}'",
5499 clip_id,
5500 track_name
5501 );
5502 return;
5503 };
5504 let launch_at_sample = quantize(quantize_reference_sample, launch_quantization);
5505 tracing::info!(
5506 "Session launch track={} scene={} clip_id={} kind={:?} launch_at={} transport_running={}",
5507 track_name,
5508 scene_index,
5509 clip_id,
5510 kind,
5511 launch_at_sample,
5512 self.transport_running
5513 );
5514 track.schedule_session_launch(crate::track::PendingSessionLaunch {
5515 scene_index,
5516 clip_id,
5517 kind,
5518 launch_at_sample,
5519 loop_enabled,
5520 loop_start_samples,
5521 loop_end_samples,
5522 });
5523 }
5524 SessionAction::StopClip {
5525 track_name,
5526 scene_index,
5527 launch_quantization,
5528 } => {
5529 let Some(track) = self.track_handle_by_name(&track_name) else {
5530 return;
5531 };
5532 let stop_at_sample = quantize(quantize_reference_sample, launch_quantization);
5533 track
5534 .lock()
5535 .schedule_session_stop(scene_index, stop_at_sample);
5536 }
5537 SessionAction::LaunchScene {
5538 scene_index,
5539 launch_quantization,
5540 } => {
5541 let launch_at_sample = quantize(quantize_reference_sample, launch_quantization);
5542 let tracks: Vec<_> = self.state.lock().tracks.values().cloned().collect();
5543 for track in tracks {
5544 let track_lock = track.lock();
5545 let Some(slot) = track_lock.session_slots.get(&scene_index) else {
5546 continue;
5547 };
5548 if !slot.play_enabled {
5549 continue;
5550 }
5551 let clip_id = slot.clip_id.clone();
5552 let kind = if track_lock.audio.clips.iter().any(|c| c.id == clip_id) {
5553 Kind::Audio
5554 } else if track_lock.midi.clips.iter().any(|c| c.id == clip_id) {
5555 Kind::MIDI
5556 } else {
5557 continue;
5558 };
5559 track_lock.schedule_session_launch(crate::track::PendingSessionLaunch {
5560 scene_index,
5561 clip_id,
5562 kind,
5563 launch_at_sample,
5564 loop_enabled: true,
5565 loop_start_samples: 0,
5566 loop_end_samples: 0,
5567 });
5568 }
5569 }
5570 SessionAction::StopScene {
5571 scene_index,
5572 launch_quantization,
5573 } => {
5574 let stop_at_sample = quantize(quantize_reference_sample, launch_quantization);
5575 let tracks: Vec<_> = self.state.lock().tracks.values().cloned().collect();
5576 for track in tracks {
5577 track
5578 .lock()
5579 .schedule_session_stop(scene_index, stop_at_sample);
5580 }
5581 }
5582 SessionAction::StopAllClips => {
5583 let stop_at_sample = quantize(quantize_reference_sample, LaunchQuantization::Bar);
5584 let tracks: Vec<_> = self.state.lock().tracks.values().cloned().collect();
5585 for track in tracks {
5586 let track = track.lock();
5587 for clip in &mut track.playing_session_clips {
5588 if clip.stop_at_sample.is_none() {
5589 clip.stop_at_sample = Some(stop_at_sample);
5590 }
5591 }
5592 }
5593 }
5594 }
5595 }
5596
5597 fn find_audio_io_owner(
5598 &self,
5599 state: &crate::state::State,
5600 io: &std::sync::Arc<crate::audio::io::AudioIO>,
5601 ) -> Option<(String, usize)> {
5602 for (name, track) in &state.tracks {
5603 let t = track.lock();
5604 for (i, out) in t.audio.outs.iter().enumerate() {
5605 if std::sync::Arc::ptr_eq(out, io) {
5606 return Some((name.clone(), i));
5607 }
5608 }
5609 for (i, inp) in t.audio.ins.iter().enumerate() {
5610 if std::sync::Arc::ptr_eq(inp, io) {
5611 return Some((name.clone(), i));
5612 }
5613 }
5614 }
5615 None
5616 }
5617
5618 fn find_midi_io_owner(
5619 &self,
5620 state: &crate::state::State,
5621 io: &std::sync::Arc<crate::mutex::UnsafeMutex<Box<crate::midi::io::MIDIIO>>>,
5622 ) -> Option<(String, usize, bool)> {
5623 for (name, track) in &state.tracks {
5624 let t = track.lock();
5625 for (i, out) in t.midi.outs.iter().enumerate() {
5626 if std::sync::Arc::ptr_eq(out, io) {
5627 return Some((name.clone(), i, false));
5628 }
5629 }
5630 for (i, inp) in t.midi.ins.iter().enumerate() {
5631 if std::sync::Arc::ptr_eq(inp, io) {
5632 return Some((name.clone(), i, true));
5633 }
5634 }
5635 }
5636 None
5637 }
5638
5639 fn collect_descendant_track_names(&self, name: &str, out: &mut Vec<String>) {
5640 let child_arcs: Vec<Arc<UnsafeMutex<Box<Track>>>> = {
5643 let state = self.state.lock();
5644 if let Some(track) = state.tracks.get(name) {
5645 track.lock().child_tracks.clone()
5646 } else {
5647 Vec::new()
5648 }
5649 };
5650 for child in child_arcs {
5651 let child_name = { child.lock().name.clone() };
5652 self.collect_descendant_track_names(&child_name, out);
5653 out.push(child_name);
5654 }
5655 }
5656
5657 async fn remove_single_track(&mut self, name: &str) {
5658 let children: Vec<Arc<UnsafeMutex<Box<Track>>>> = {
5659 let state = self.state.lock();
5660 if let Some(removed) = state.tracks.get(name).cloned() {
5661 removed.lock().child_tracks.clone()
5662 } else {
5663 Vec::new()
5664 }
5665 };
5666 let parent_name: Option<String> = {
5667 let state = self.state.lock();
5668 state
5669 .tracks
5670 .get(name)
5671 .map(|t| t.lock().parent_track.clone())
5672 .unwrap_or(None)
5673 };
5674 if let Some(parent_name) = parent_name {
5675 let state = self.state.lock();
5676 if let Some(parent) = state.tracks.get(&parent_name).cloned() {
5677 let parent = parent.lock();
5678 parent.child_tracks.retain(|c| c.lock().name != *name);
5679 }
5680 }
5681 if let Some(removed_track) = self.state.lock().tracks.get(name).cloned() {
5682 for child in children {
5683 let removed = removed_track.lock();
5684 child.lock().disconnect_from_parent(removed);
5685 child.lock().parent_track = None;
5686 }
5687 }
5688 self.state.lock().tracks.remove(name);
5689 self.audio_recordings.remove(name);
5690 self.midi_recordings.remove(name);
5691 self.midi_hw_in_routes.retain(|r| r.to_track != *name);
5692 self.midi_hw_out_routes.retain(|r| r.from_track != *name);
5693 if self
5694 .pending_midi_learn
5695 .as_ref()
5696 .is_some_and(|(track_name, _, _)| track_name == name)
5697 {
5698 self.pending_midi_learn = None;
5699 }
5700 }
5701
5702 fn prepare_inverse_actions(
5703 &self,
5704 action_to_process: &Action,
5705 record_history: bool,
5706 suppress_timing_history: bool,
5707 ) -> Option<Vec<Action>> {
5708 let mut extra_inverse_actions: Vec<Action> = Vec::new();
5709 if record_history
5710 && !self.history_suspended
5711 && let Action::RemoveTrack(track_name) = action_to_process
5712 {
5713 for route in self
5714 .midi_hw_in_routes
5715 .iter()
5716 .filter(|route| &route.to_track == track_name)
5717 {
5718 extra_inverse_actions.push(Action::Connect {
5719 from_track: format!("midi:hw:in:{}", route.device),
5720 from_port: 0,
5721 to_track: route.to_track.clone(),
5722 to_port: route.to_port,
5723 kind: Kind::MIDI,
5724 });
5725 }
5726 for route in self
5727 .midi_hw_out_routes
5728 .iter()
5729 .filter(|route| &route.from_track == track_name)
5730 {
5731 extra_inverse_actions.push(Action::Connect {
5732 from_track: route.from_track.clone(),
5733 from_port: route.from_port,
5734 to_track: format!("midi:hw:out:{}", route.device),
5735 to_port: 0,
5736 kind: Kind::MIDI,
5737 });
5738 }
5739 }
5740 if record_history
5741 && !self.history_suspended
5742 && matches!(action_to_process, Action::ClearAllMidiLearnBindings)
5743 {
5744 if let Some(binding) = self.global_midi_learn_play_pause.clone() {
5745 extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
5746 target: crate::message::GlobalMidiLearnTarget::PlayPause,
5747 binding: Some(binding),
5748 });
5749 }
5750 if let Some(binding) = self.global_midi_learn_stop.clone() {
5751 extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
5752 target: crate::message::GlobalMidiLearnTarget::Stop,
5753 binding: Some(binding),
5754 });
5755 }
5756 if let Some(binding) = self.global_midi_learn_record_toggle.clone() {
5757 extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
5758 target: crate::message::GlobalMidiLearnTarget::RecordToggle,
5759 binding: Some(binding),
5760 });
5761 }
5762 for (key, binding) in self.session_midi_learn_slots.clone() {
5763 extra_inverse_actions.push(Action::SetSessionMidiLearnBinding {
5764 target: crate::message::SessionMidiLearnTarget::Slot {
5765 track_name: key.0,
5766 scene_index: key.1,
5767 },
5768 binding: Some(binding),
5769 });
5770 }
5771 for (scene_index, binding) in self.session_midi_learn_scenes.clone() {
5772 extra_inverse_actions.push(Action::SetSessionMidiLearnBinding {
5773 target: crate::message::SessionMidiLearnTarget::Scene(scene_index),
5774 binding: Some(binding),
5775 });
5776 }
5777 for (track_name, binding) in self.session_midi_learn_stop_track.clone() {
5778 extra_inverse_actions.push(Action::SetSessionMidiLearnBinding {
5779 target: crate::message::SessionMidiLearnTarget::StopTrack(track_name),
5780 binding: Some(binding),
5781 });
5782 }
5783 if let Some(binding) = self.session_midi_learn_stop_all.clone() {
5784 extra_inverse_actions.push(Action::SetSessionMidiLearnBinding {
5785 target: crate::message::SessionMidiLearnTarget::StopAll,
5786 binding: Some(binding),
5787 });
5788 }
5789 }
5790 let mut inverse_actions = if record_history
5791 && !suppress_timing_history
5792 && should_record(action_to_process)
5793 && !self.history_suspended
5794 {
5795 let state = self.state.lock();
5796 create_inverse_actions(action_to_process, state).map(|mut actions| {
5797 actions.extend(extra_inverse_actions);
5798 actions
5799 })
5800 } else {
5801 None
5802 };
5803 if record_history && !suppress_timing_history && !self.history_suspended {
5804 match action_to_process {
5805 Action::SetTempo(_) => {
5806 inverse_actions = Some(vec![Action::SetTempo(self.tempo_bpm)]);
5807 }
5808 Action::SetLoopEnabled(_) => {
5809 inverse_actions = Some(vec![Action::SetLoopEnabled(self.loop_enabled)]);
5810 }
5811 Action::SetLoopRange(_) => {
5812 inverse_actions = Some(vec![
5813 Action::SetLoopRange(self.loop_range_samples),
5814 Action::SetLoopEnabled(self.loop_enabled),
5815 ]);
5816 }
5817 Action::SetPunchEnabled(_) => {
5818 inverse_actions = Some(vec![Action::SetPunchEnabled(self.punch_enabled)]);
5819 }
5820 Action::SetPunchRange(_) => {
5821 inverse_actions = Some(vec![
5822 Action::SetPunchRange(self.punch_range_samples),
5823 Action::SetPunchEnabled(self.punch_enabled),
5824 ]);
5825 }
5826 Action::SetMetronomeEnabled(_) => {
5827 inverse_actions =
5828 Some(vec![Action::SetMetronomeEnabled(self.metronome_enabled)]);
5829 }
5830 Action::SetTimeSignature { .. } => {
5831 inverse_actions = Some(vec![Action::SetTimeSignature {
5832 numerator: self.tsig_num,
5833 denominator: self.tsig_denom,
5834 }]);
5835 }
5836 Action::SetTempoMap { .. } => {
5837 inverse_actions = Some(vec![Action::SetTempoMap {
5838 tempo_points: self.tempo_points.clone(),
5839 time_signature_points: self.time_signature_points.clone(),
5840 }]);
5841 }
5842 Action::SetClipPlaybackEnabled(_) => {
5843 inverse_actions = Some(vec![Action::SetClipPlaybackEnabled(
5844 self.clip_playback_enabled,
5845 )]);
5846 }
5847 Action::SetRecordEnabled(_) => {
5848 inverse_actions = Some(vec![Action::SetRecordEnabled(self.record_enabled)]);
5849 }
5850 Action::SetGlobalMidiLearnBinding { target, .. } => {
5851 let binding = match target {
5852 crate::message::GlobalMidiLearnTarget::PlayPause => {
5853 self.global_midi_learn_play_pause.clone()
5854 }
5855 crate::message::GlobalMidiLearnTarget::Stop => {
5856 self.global_midi_learn_stop.clone()
5857 }
5858 crate::message::GlobalMidiLearnTarget::RecordToggle => {
5859 self.global_midi_learn_record_toggle.clone()
5860 }
5861 };
5862 inverse_actions = Some(vec![Action::SetGlobalMidiLearnBinding {
5863 target: *target,
5864 binding,
5865 }]);
5866 }
5867 Action::SetModulators(_) => {
5868 inverse_actions = Some(vec![Action::SetModulators(self.modulators.clone())]);
5869 }
5870 Action::SetSessionMidiLearnBinding { target, .. } => {
5871 let binding = match target {
5872 crate::message::SessionMidiLearnTarget::Slot {
5873 track_name,
5874 scene_index,
5875 } => self
5876 .session_midi_learn_slots
5877 .get(&(track_name.clone(), *scene_index))
5878 .cloned(),
5879 crate::message::SessionMidiLearnTarget::Scene(scene_index) => {
5880 self.session_midi_learn_scenes.get(scene_index).cloned()
5881 }
5882 crate::message::SessionMidiLearnTarget::StopTrack(track_name) => {
5883 self.session_midi_learn_stop_track.get(track_name).cloned()
5884 }
5885 crate::message::SessionMidiLearnTarget::StopAll => {
5886 self.session_midi_learn_stop_all.clone()
5887 }
5888 };
5889 inverse_actions = Some(vec![Action::SetSessionMidiLearnBinding {
5890 target: target.clone(),
5891 binding,
5892 }]);
5893 }
5894 _ => {}
5895 }
5896 }
5897 inverse_actions
5898 }
5899
5900 async fn handle_quit(&mut self, a: Action) {
5901 self.flush_recordings().await;
5902 if let Some(worker) = self.hw_worker.take() {
5910 if let Some(hw) = &self.hw_driver {
5911 hw.lock().request_stop();
5912 }
5913 let panic_events = self.panic_events_for_all_hw_midi_outputs();
5916 if !panic_events.is_empty() {
5917 let _ = worker.tx.send(Message::HWMidiOutEvents(panic_events)).await;
5918 }
5919 if let Err(e) = worker.tx.send(Message::Request(a.clone())).await {
5922 error!("Error sending quit message to HW worker: {e}");
5923 }
5924 worker
5925 .handle
5926 .await
5927 .unwrap_or_else(|e| error!("Error waiting for HW worker to quit: {e}"));
5928 }
5929 if let Some(hw) = &self.hw_driver {
5935 hw.lock().close_fds();
5936 }
5937 self.midi_hub.lock().close_all();
5938 self.hw_driver = None;
5939 self.notify_clients(Ok(Action::Quit)).await;
5940 self.ready_workers.clear();
5941 while !self.workers.is_empty() {
5942 let worker = self.workers.remove(0);
5943 if let Err(e) = worker.tx.send(Message::Request(a.clone())).await {
5944 error!("Error sending quit message to worker: {e}");
5945 }
5946 worker
5947 .handle
5948 .await
5949 .unwrap_or_else(|e| error!("Error waiting for worker to quit: {e}"));
5950 }
5951 #[cfg(unix)]
5952 {
5953 self.jack_runtime = None;
5954 }
5955 self.osc_server = None;
5956 }
5957
5958 async fn handle_add_track(
5959 &mut self,
5960 name: String,
5961 audio_ins: usize,
5962 midi_ins: usize,
5963 audio_outs: usize,
5964 midi_outs: usize,
5965 folder: bool,
5966 ) {
5967 let tracks = &mut self.state.lock().tracks;
5968 if tracks.contains_key(&name) {
5969 self.notify_clients(Err(format!("Track {} already exists", name)))
5970 .await;
5971 return;
5972 }
5973 let maybe_hw = if let Some(oss) = &self.hw_driver {
5974 let hw = oss.lock();
5975 Some((hw.cycle_samples(), hw.sample_rate() as f64))
5976 } else {
5977 #[cfg(unix)]
5978 if let Some(jack) = &self.jack_runtime {
5979 let j = jack.lock();
5980 Some((j.buffer_size, j.sample_rate as f64))
5981 } else {
5982 None
5983 }
5984 #[cfg(not(unix))]
5985 None
5986 };
5987
5988 if let Some((chsamples, sample_rate)) = maybe_hw {
5989 let track = if folder {
5990 Track::new_folder(
5991 name.clone(),
5992 audio_ins,
5993 audio_outs,
5994 midi_ins,
5995 midi_outs,
5996 chsamples,
5997 sample_rate,
5998 )
5999 } else {
6000 Track::new(
6001 name.clone(),
6002 audio_ins,
6003 audio_outs,
6004 midi_ins,
6005 midi_outs,
6006 chsamples,
6007 sample_rate,
6008 )
6009 };
6010 tracks.insert(name.clone(), Arc::new(UnsafeMutex::new(Box::new(track))));
6011 if let Some(track) = tracks.get(&name) {
6012 let t = track.lock();
6013 t.set_clip_playback_enabled(self.clip_playback_enabled);
6014 t.set_transport_timing(self.tempo_bpm, self.tsig_num, self.tsig_denom);
6015 t.set_session_base_dir(self.session_dir.clone());
6016 }
6017 } else {
6018 self.notify_clients(Err(
6019 "Engine needs to open audio device before adding audio track".to_string(),
6020 ))
6021 .await;
6022 }
6023 }
6024
6025 async fn handle_remove_track(&mut self, name: String, record_history: bool) {
6026 let mut descendant_names = Vec::new();
6027 self.collect_descendant_track_names(&name, &mut descendant_names);
6028 let names_to_remove: Vec<String> = descendant_names
6029 .iter()
6030 .cloned()
6031 .chain(std::iter::once(name.clone()))
6032 .collect();
6033
6034 let combined_inverse = if record_history && !self.history_suspended {
6035 let state = self.state.lock();
6036 let mut inv = Vec::new();
6037 for n in &names_to_remove {
6038 if let Some(mut actions) =
6039 create_inverse_actions(&Action::RemoveTrack(n.clone()), state)
6040 {
6041 inv.append(&mut actions);
6042 }
6043 for route in self.midi_hw_in_routes.iter().filter(|r| &r.to_track == n) {
6044 inv.push(Action::Connect {
6045 from_track: format!("midi:hw:in:{}", route.device),
6046 from_port: 0,
6047 to_track: route.to_track.clone(),
6048 to_port: route.to_port,
6049 kind: Kind::MIDI,
6050 });
6051 }
6052 for route in self
6053 .midi_hw_out_routes
6054 .iter()
6055 .filter(|r| &r.from_track == n)
6056 {
6057 inv.push(Action::Connect {
6058 from_track: route.from_track.clone(),
6059 from_port: route.from_port,
6060 to_track: format!("midi:hw:out:{}", route.device),
6061 to_port: 0,
6062 kind: Kind::MIDI,
6063 });
6064 }
6065 }
6066
6067 let mut add_tracks = Vec::new();
6071 let mut connections = Vec::new();
6072 let mut rest = Vec::new();
6073 for action in inv {
6074 match action {
6075 Action::AddTrack { .. } => add_tracks.push(action),
6076 Action::Connect { .. } => connections.push(action),
6077 _ => rest.push(action),
6078 }
6079 }
6080 let mut ordered = add_tracks;
6081 ordered.extend(rest);
6082 ordered.extend(connections);
6083 ordered
6084 } else {
6085 Vec::new()
6086 };
6087
6088 for n in &descendant_names {
6089 self.remove_single_track(n).await;
6090 self.notify_clients(Ok(Action::RemoveTrack(n.clone())))
6091 .await;
6092 }
6093 self.remove_single_track(&name).await;
6094
6095 if record_history && !self.history_suspended && !combined_inverse.is_empty() {
6096 self.history.record(UndoEntry {
6097 forward_actions: vec![Action::RemoveTrack(name.clone())],
6098 inverse_actions: combined_inverse,
6099 });
6100 }
6101 }
6102
6103 async fn handle_connect(
6104 &mut self,
6105 from_track: &str,
6106 from_port: usize,
6107 to_track: &str,
6108 to_port: usize,
6109 kind: Kind,
6110 ) {
6111 match kind {
6112 Kind::Audio => {
6113 let (from_audio_io, to_audio_io) =
6114 self.resolve_audio_route_ports(from_track, from_port, to_track, to_port);
6115 match (from_audio_io, to_audio_io) {
6116 (Some(source), Some(target)) => {
6117 if from_track != "hw:in"
6118 && to_track != "hw:out"
6119 && self.check_if_leads_to_kind(Kind::Audio, to_track, from_track)
6120 {
6121 self.notify_clients(Err("Circular routing is not allowed!".into()))
6122 .await;
6123 return;
6124 }
6125 crate::audio::io::AudioIO::connect(&source, &target);
6126 }
6127 (None, _) => {
6128 self.notify_clients(Err(format!(
6129 "Source track '{}' not found",
6130 from_track
6131 )))
6132 .await;
6133 }
6134 (_, None) => {
6135 self.notify_clients(Err(format!(
6136 "Destination track '{}' not found",
6137 to_track
6138 )))
6139 .await;
6140 }
6141 }
6142 }
6143 Kind::MIDI => {
6144 let from_hw_in_device = Self::midi_hw_in_device(from_track);
6145 let to_hw_out_device = Self::midi_hw_out_device(to_track);
6146 let from_is_invalid_hw = Self::midi_hw_out_device(from_track).is_some();
6147 let to_is_invalid_hw = Self::midi_hw_in_device(to_track).is_some();
6148
6149 if from_is_invalid_hw || to_is_invalid_hw {
6150 self.notify_clients(Err(
6151 "Invalid MIDI hardware connection direction".to_string()
6152 ))
6153 .await;
6154 return;
6155 }
6156
6157 if from_hw_in_device.is_none()
6158 && to_hw_out_device.is_none()
6159 && self.check_if_leads_to_kind(Kind::MIDI, to_track, from_track)
6160 {
6161 self.notify_clients(Err("Circular routing is not allowed!".into()))
6162 .await;
6163 return;
6164 }
6165
6166 let state = self.state.lock();
6167 let from_track_handle = state.tracks.get(from_track);
6168 let to_track_handle = state.tracks.get(to_track);
6169
6170 if let (Some(from_device), Some(to_device)) = (from_hw_in_device, to_hw_out_device)
6171 {
6172 let route = MidiHwThruRoute {
6173 from_device: from_device.to_string(),
6174 to_device: to_device.to_string(),
6175 };
6176 if !self.midi_hw_thru_routes.iter().any(|r| r == &route) {
6177 self.midi_hw_thru_routes.push(route);
6178 }
6179 } else if let Some(device) = from_hw_in_device {
6180 if let Some(t_t) = to_track_handle {
6181 if t_t.lock().midi.ins.get(to_port).is_none() {
6182 self.notify_clients(Err(format!(
6183 "MIDI input port {} not found on track '{}'",
6184 to_port, to_track
6185 )))
6186 .await;
6187 return;
6188 }
6189 let route = MidiHwInRoute {
6190 device: device.to_string(),
6191 to_track: to_track.to_string(),
6192 to_port,
6193 };
6194 if !self.midi_hw_in_routes.iter().any(|r| r == &route) {
6195 self.midi_hw_in_routes.push(route);
6196 }
6197 } else {
6198 self.notify_clients(Err(format!(
6199 "MIDI destination track not found: {}",
6200 to_track
6201 )))
6202 .await;
6203 }
6204 } else if let Some(device) = to_hw_out_device {
6205 if let Some(f_t) = from_track_handle {
6206 if f_t.lock().midi.outs.get(from_port).is_none() {
6207 self.notify_clients(Err(format!(
6208 "MIDI output port {} not found on track '{}'",
6209 from_port, from_track
6210 )))
6211 .await;
6212 return;
6213 }
6214 let route = MidiHwOutRoute {
6215 from_track: from_track.to_string(),
6216 from_port,
6217 device: device.to_string(),
6218 };
6219 if !self.midi_hw_out_routes.iter().any(|r| r == &route) {
6220 self.midi_hw_out_routes.push(route);
6221 }
6222 } else {
6223 self.notify_clients(Err(format!(
6224 "MIDI source track not found: {}",
6225 from_track
6226 )))
6227 .await;
6228 }
6229 } else {
6230 match (from_track_handle, to_track_handle) {
6231 (Some(f_t), Some(t_t)) => {
6232 let to_in_res = t_t.lock().midi.ins.get(to_port).cloned();
6233 if let Some(to_in) = to_in_res {
6234 let from_track = f_t.lock();
6235 if let Err(e) = from_track.midi.connect_out(from_port, to_in) {
6236 self.notify_clients(Err(e)).await;
6237 return;
6238 }
6239 from_track.invalidate_midi_route_cache();
6240 } else {
6241 self.notify_clients(Err(format!(
6242 "MIDI input port {} not found on track '{}'",
6243 to_port, to_track
6244 )))
6245 .await;
6246 }
6247 }
6248 _ => {
6249 self.notify_clients(Err(format!(
6250 "MIDI tracks not found: {} or {}",
6251 from_track, to_track
6252 )))
6253 .await;
6254 }
6255 }
6256 }
6257 }
6258 };
6259 }
6260
6261 async fn handle_disconnect(&mut self, action: Action) {
6262 let Action::Disconnect {
6263 ref from_track,
6264 from_port,
6265 ref to_track,
6266 to_port,
6267 kind,
6268 } = action
6269 else {
6270 return;
6271 };
6272
6273 if kind == Kind::Audio {
6274 if let Err(e) = self.disconnect_audio_route_and_notify(action.clone()).await {
6275 self.notify_clients(Err(e)).await;
6276 }
6277 } else if kind == Kind::MIDI {
6278 let from_hw_in_device = Self::midi_hw_in_device(from_track);
6279 let to_hw_out_device = Self::midi_hw_out_device(to_track);
6280
6281 if let (Some(from_device), Some(to_device)) = (from_hw_in_device, to_hw_out_device) {
6282 let before = self.midi_hw_thru_routes.len();
6283 self.midi_hw_thru_routes
6284 .retain(|r| !(r.from_device == from_device && r.to_device == to_device));
6285 if self.midi_hw_thru_routes.len() < before {
6286 self.notify_clients(Ok(action.clone())).await;
6287 } else {
6288 self.notify_clients(Err(format!(
6289 "Disconnect failed: MIDI route not found ({} -> {})",
6290 from_track, to_track
6291 )))
6292 .await;
6293 }
6294 return;
6295 }
6296
6297 if let Some(device) = from_hw_in_device {
6298 let before = self.midi_hw_in_routes.len();
6299 self.midi_hw_in_routes.retain(|r| {
6300 !(r.device == device && r.to_track == *to_track && r.to_port == to_port)
6301 });
6302 if self.midi_hw_in_routes.len() < before {
6303 self.notify_clients(Ok(action.clone())).await;
6304 } else {
6305 self.notify_clients(Err(format!(
6306 "Disconnect failed: MIDI route not found ({} -> {})",
6307 from_track, to_track
6308 )))
6309 .await;
6310 }
6311 return;
6312 }
6313
6314 if let Some(device) = to_hw_out_device {
6315 let before = self.midi_hw_out_routes.len();
6316 self.midi_hw_out_routes.retain(|r| {
6317 !(r.from_track == *from_track && r.from_port == from_port && r.device == device)
6318 });
6319 if self.midi_hw_out_routes.len() < before {
6320 self.notify_clients(Ok(action.clone())).await;
6321 } else {
6322 self.notify_clients(Err(format!(
6323 "Disconnect failed: MIDI route not found ({} -> {})",
6324 from_track, to_track
6325 )))
6326 .await;
6327 }
6328 return;
6329 }
6330
6331 let state = self.state.lock();
6332 if let (Some(f_t), Some(t_t)) =
6333 (state.tracks.get(from_track), state.tracks.get(to_track))
6334 && let Some(to_in) = t_t.lock().midi.ins.get(to_port).cloned()
6335 {
6336 let from_track = f_t.lock();
6337 if let Err(e) = from_track.midi.disconnect_out(from_port, &to_in) {
6338 self.notify_clients(Err(e)).await;
6339 } else {
6340 from_track.invalidate_midi_route_cache();
6341 self.notify_clients(Ok(action.clone())).await;
6342 }
6343 } else {
6344 self.notify_clients(Err(format!(
6345 "Disconnect failed: MIDI ports not found ({} -> {})",
6346 from_track, to_track
6347 )))
6348 .await;
6349 }
6350 }
6351 }
6352
6353 async fn handle_request_session_diagnostics(&mut self) {
6354 let (
6355 track_count,
6356 frozen_track_count,
6357 audio_clip_count,
6358 midi_clip_count,
6359 lv2_instance_count,
6360 vst3_instance_count,
6361 clap_instance_count,
6362 ) = {
6363 let tracks = &self.state.lock().tracks;
6364 let mut track_count = 0usize;
6365 let mut frozen_track_count = 0usize;
6366 let mut audio_clip_count = 0usize;
6367 let mut midi_clip_count = 0usize;
6368 #[cfg(all(unix, not(target_os = "macos")))]
6369 let mut lv2_instance_count = 0usize;
6370 #[cfg(not(all(unix, not(target_os = "macos"))))]
6371 let lv2_instance_count = 0usize;
6372 let mut vst3_instance_count = 0usize;
6373 let mut clap_instance_count = 0usize;
6374 for track in tracks.values() {
6375 let t = track.lock();
6376 track_count += 1;
6377 if t.frozen {
6378 frozen_track_count += 1;
6379 }
6380 audio_clip_count += t.audio.clips.len();
6381 midi_clip_count += t.midi.clips.len();
6382 #[cfg(all(unix, not(target_os = "macos")))]
6383 {
6384 lv2_instance_count += t.lv2_plugins.len();
6385 }
6386 vst3_instance_count += t.vst3_plugins.len();
6387 clap_instance_count += t.clap_plugins.len();
6388 }
6389 (
6390 track_count,
6391 frozen_track_count,
6392 audio_clip_count,
6393 midi_clip_count,
6394 lv2_instance_count,
6395 vst3_instance_count,
6396 clap_instance_count,
6397 )
6398 };
6399 #[cfg(not(all(unix, not(target_os = "macos"))))]
6400 let _lv2_instance_count = lv2_instance_count;
6401 let pending_hw_midi_events = self.pending_hw_midi_events.len()
6402 + self
6403 .pending_hw_midi_events_by_device
6404 .values()
6405 .map(std::vec::Vec::len)
6406 .sum::<usize>();
6407 let sample_rate_hz = if let Some(hw) = &self.hw_driver {
6408 hw.lock().sample_rate() as usize
6409 } else {
6410 #[cfg(unix)]
6411 {
6412 self.jack_runtime
6413 .as_ref()
6414 .map(|j| j.lock().sample_rate)
6415 .unwrap_or(0)
6416 }
6417 #[cfg(not(unix))]
6418 0
6419 };
6420 let cycle_samples = self.current_cycle_samples();
6421 self.notify_clients(Ok(Action::SessionDiagnosticsReport {
6422 track_count,
6423 frozen_track_count,
6424 audio_clip_count,
6425 midi_clip_count,
6426 #[cfg(all(unix, not(target_os = "macos")))]
6427 lv2_instance_count,
6428 vst3_instance_count,
6429 clap_instance_count,
6430 pending_requests: self.pending_requests.len(),
6431 workers_total: self.workers.len(),
6432 workers_ready: self.ready_workers.len(),
6433 pending_hw_midi_events,
6434 playing: self.playing,
6435 transport_running: self.transport_running,
6436 transport_sample: self.transport_sample,
6437 tempo_bpm: self.tempo_bpm,
6438 sample_rate_hz,
6439 cycle_samples,
6440 }))
6441 .await;
6442 }
6443
6444 async fn handle_request_midi_learn_mappings_report(&mut self) {
6445 let mut lines = Vec::<String>::new();
6446 let fmt_binding = |b: &crate::message::MidiLearnBinding| {
6447 let device = b.device.as_deref().unwrap_or("*");
6448 format!("{device} CH{} CC{}", b.channel + 1, b.cc)
6449 };
6450 if let Some(b) = self.global_midi_learn_play_pause.as_ref() {
6451 lines.push(format!("Global PlayPause: {}", fmt_binding(b)));
6452 }
6453 if let Some(b) = self.global_midi_learn_stop.as_ref() {
6454 lines.push(format!("Global Stop: {}", fmt_binding(b)));
6455 }
6456 if let Some(b) = self.global_midi_learn_record_toggle.as_ref() {
6457 lines.push(format!("Global RecordToggle: {}", fmt_binding(b)));
6458 }
6459 for (track_name, track) in self.state.lock().tracks.iter() {
6460 let t = track.lock();
6461 if let Some(b) = t.midi_learn_volume.as_ref() {
6462 lines.push(format!("{} Volume: {}", track_name, fmt_binding(b)));
6463 }
6464 if let Some(b) = t.midi_learn_balance.as_ref() {
6465 lines.push(format!("{} Balance: {}", track_name, fmt_binding(b)));
6466 }
6467 if let Some(b) = t.midi_learn_mute.as_ref() {
6468 lines.push(format!("{} Mute: {}", track_name, fmt_binding(b)));
6469 }
6470 if let Some(b) = t.midi_learn_solo.as_ref() {
6471 lines.push(format!("{} Solo: {}", track_name, fmt_binding(b)));
6472 }
6473 if let Some(b) = t.midi_learn_arm.as_ref() {
6474 lines.push(format!("{} Arm: {}", track_name, fmt_binding(b)));
6475 }
6476 if let Some(b) = t.midi_learn_input_monitor.as_ref() {
6477 lines.push(format!("{} InputMonitor: {}", track_name, fmt_binding(b)));
6478 }
6479 if let Some(b) = t.midi_learn_disk_monitor.as_ref() {
6480 lines.push(format!("{} DiskMonitor: {}", track_name, fmt_binding(b)));
6481 }
6482 }
6483 for ((track_name, scene_index), binding) in &self.session_midi_learn_slots {
6484 lines.push(format!(
6485 "{} Slot {}: {}",
6486 track_name,
6487 scene_index + 1,
6488 fmt_binding(binding)
6489 ));
6490 }
6491 for (scene_index, binding) in &self.session_midi_learn_scenes {
6492 lines.push(format!(
6493 "Scene {}: {}",
6494 scene_index + 1,
6495 fmt_binding(binding)
6496 ));
6497 }
6498 for (track_name, binding) in &self.session_midi_learn_stop_track {
6499 lines.push(format!("{} Stop: {}", track_name, fmt_binding(binding)));
6500 }
6501 if let Some(binding) = self.session_midi_learn_stop_all.as_ref() {
6502 lines.push(format!("Stop All Clips: {}", fmt_binding(binding)));
6503 }
6504 if lines.is_empty() {
6505 lines.push("No MIDI learn mappings configured".to_string());
6506 }
6507 self.notify_clients(Ok(Action::MidiLearnMappingsReport { lines }))
6508 .await;
6509 }
6510
6511 async fn handle_track_set_parent(&mut self, track_name: &str, parent_name: Option<&str>) {
6512 let track = match self.track_handle_or_err(track_name) {
6513 Ok(track) => track,
6514 Err(e) => {
6515 self.notify_clients(Err(e)).await;
6516 return;
6517 }
6518 };
6519 if parent_name == Some(track_name) {
6520 self.notify_clients(Err("Track cannot be its own parent".to_string()))
6521 .await;
6522 return;
6523 }
6524
6525 if let Some(parent_name) = parent_name {
6527 let state = self.state.lock();
6528 let parent = state.tracks.get(parent_name);
6529 if parent.is_none() {
6530 self.notify_clients(Err(format!(
6531 "Parent track '{}' does not exist",
6532 parent_name
6533 )))
6534 .await;
6535 return;
6536 }
6537 if !parent.unwrap().lock().is_folder {
6538 self.notify_clients(Err(format!("Track '{}' is not a folder", parent_name)))
6539 .await;
6540 return;
6541 }
6542 }
6543
6544 {
6546 let old_parent_name = track.lock().parent_track.clone();
6547 if let Some(old_parent_name) = old_parent_name {
6548 let state = self.state.lock();
6549 if let (Some(parent_arc), Some(child_arc)) = (
6550 state.tracks.get(&old_parent_name).cloned(),
6551 state.tracks.get(track_name).cloned(),
6552 ) {
6553 {
6554 let parent = parent_arc.lock();
6555 parent.child_tracks.retain(|c| c.lock().name != *track_name);
6556 }
6557 {
6558 let child = child_arc.lock();
6559 let parent = parent_arc.lock();
6560 child.disconnect_from_parent(parent);
6561 }
6562 }
6563 }
6564 }
6565
6566 let mut disconnect_actions = Vec::new();
6567
6568 {
6570 let state = self.state.lock();
6571 let hw_inputs = self.all_hw_input_audio_ports();
6572 let hw_outputs = self.all_hw_output_audio_ports();
6573 if let Some(child_arc) = state.tracks.get(track_name).cloned() {
6574 let child = child_arc.lock();
6575 for (port_idx, inp) in child.audio.ins.iter().enumerate() {
6576 let sources = inp.connections.lock().clone();
6577 for src in sources {
6578 let _ = AudioIO::disconnect(&src, inp);
6579 if let Some((src_name, src_port)) = self.find_audio_io_owner(state, &src) {
6580 disconnect_actions.push(Action::Disconnect {
6581 from_track: src_name,
6582 from_port: src_port,
6583 to_track: track_name.to_string(),
6584 to_port: port_idx,
6585 kind: Kind::Audio,
6586 });
6587 } else if let Some(src_port) = hw_inputs
6588 .iter()
6589 .position(|hw_in| std::sync::Arc::ptr_eq(hw_in, &src))
6590 {
6591 disconnect_actions.push(Action::Disconnect {
6592 from_track: "hw:in".to_string(),
6593 from_port: src_port,
6594 to_track: track_name.to_string(),
6595 to_port: port_idx,
6596 kind: Kind::Audio,
6597 });
6598 }
6599 }
6600 }
6601 for (port_idx, out) in child.audio.outs.iter().enumerate() {
6602 let targets = out.connections.lock().clone();
6603 for tgt in targets {
6604 let _ = AudioIO::disconnect(out, &tgt);
6605 if let Some((tgt_name, tgt_port)) = self.find_audio_io_owner(state, &tgt) {
6606 disconnect_actions.push(Action::Disconnect {
6607 from_track: track_name.to_string(),
6608 from_port: port_idx,
6609 to_track: tgt_name,
6610 to_port: tgt_port,
6611 kind: Kind::Audio,
6612 });
6613 } else if let Some(tgt_port) = hw_outputs
6614 .iter()
6615 .position(|hw_out| std::sync::Arc::ptr_eq(hw_out, &tgt))
6616 {
6617 disconnect_actions.push(Action::Disconnect {
6618 from_track: track_name.to_string(),
6619 from_port: port_idx,
6620 to_track: "hw:out".to_string(),
6621 to_port: tgt_port,
6622 kind: Kind::Audio,
6623 });
6624 }
6625 }
6626 }
6627
6628 for route in self
6630 .midi_hw_in_routes
6631 .iter()
6632 .filter(|r| r.to_track == track_name)
6633 {
6634 disconnect_actions.push(Action::Disconnect {
6635 from_track: format!("midi:hw:in:{}", route.device),
6636 from_port: 0,
6637 to_track: track_name.to_string(),
6638 to_port: route.to_port,
6639 kind: Kind::MIDI,
6640 });
6641 }
6642 self.midi_hw_in_routes.retain(|r| r.to_track != track_name);
6643
6644 for route in self
6645 .midi_hw_out_routes
6646 .iter()
6647 .filter(|r| r.from_track == track_name)
6648 {
6649 disconnect_actions.push(Action::Disconnect {
6650 from_track: track_name.to_string(),
6651 from_port: route.from_port,
6652 to_track: format!("midi:hw:out:{}", route.device),
6653 to_port: 0,
6654 kind: Kind::MIDI,
6655 });
6656 }
6657 self.midi_hw_out_routes
6658 .retain(|r| r.from_track != track_name);
6659
6660 for (port_idx, out) in child.midi.outs.iter().enumerate() {
6662 let targets = out.lock().connections.clone();
6663 for tgt in targets {
6664 if let Some((tgt_name, tgt_port, _)) = self.find_midi_io_owner(state, &tgt)
6665 {
6666 let _ = MIDIIO::disconnect(out, &tgt);
6667 disconnect_actions.push(Action::Disconnect {
6668 from_track: track_name.to_string(),
6669 from_port: port_idx,
6670 to_track: tgt_name,
6671 to_port: tgt_port,
6672 kind: Kind::MIDI,
6673 });
6674 }
6675 }
6676 }
6677 }
6678
6679 let child_input_arcs: Vec<_> =
6681 if let Some(child_arc) = state.tracks.get(track_name).cloned() {
6682 let child = child_arc.lock();
6683 child.midi.ins.clone()
6684 } else {
6685 Vec::new()
6686 };
6687 for (other_name, other_track) in &state.tracks {
6688 if other_name.as_str() == track_name {
6689 continue;
6690 }
6691 let other = other_track.lock();
6692 for (out_port, out) in other.midi.outs.iter().enumerate() {
6693 let targets = out.lock().connections.clone();
6694 for tgt in targets {
6695 if let Some(to_port) = child_input_arcs
6696 .iter()
6697 .position(|inp| std::sync::Arc::ptr_eq(inp, &tgt))
6698 {
6699 let _ = MIDIIO::disconnect(out, &tgt);
6700 disconnect_actions.push(Action::Disconnect {
6701 from_track: other_name.clone(),
6702 from_port: out_port,
6703 to_track: track_name.to_string(),
6704 to_port,
6705 kind: Kind::MIDI,
6706 });
6707 }
6708 }
6709 }
6710 }
6711 }
6712
6713 {
6715 track.lock().parent_track = parent_name.map(str::to_string);
6716 }
6717
6718 if let Some(parent_name) = parent_name {
6720 let state = self.state.lock();
6721 if let (Some(parent_arc), Some(child_arc)) = (
6722 state.tracks.get(parent_name).cloned(),
6723 state.tracks.get(track_name).cloned(),
6724 ) {
6725 {
6726 let parent = parent_arc.lock();
6727 parent.child_tracks.push(child_arc.clone());
6728 }
6729 {
6730 let child = child_arc.lock();
6731 let parent = parent_arc.lock();
6732 if parent.audio.ins.len() == child.audio.ins.len() {
6734 for (parent_in, child_in) in
6735 parent.audio.ins.iter().zip(child.audio.ins.iter())
6736 {
6737 Track::connect_directed_audio(parent_in, child_in);
6738 }
6739 }
6740 if parent.audio.outs.len() == child.audio.outs.len() {
6742 for (child_out, parent_out) in
6743 child.audio.outs.iter().zip(parent.audio.outs.iter())
6744 {
6745 AudioIO::connect(child_out, parent_out);
6746 }
6747 }
6748 if parent.midi.ins.len() == child.midi.ins.len() {
6750 for (parent_in, child_in) in
6751 parent.midi.ins.iter().zip(child.midi.ins.iter())
6752 {
6753 let child_in_lock = child_in.lock();
6754 if !child_in_lock
6755 .connections
6756 .iter()
6757 .any(|c| Arc::ptr_eq(c, parent_in))
6758 {
6759 child_in_lock.connections.push(parent_in.clone());
6760 }
6761 }
6762 }
6763 if parent.midi.outs.len() == child.midi.outs.len() {
6765 for (child_out, parent_out) in
6766 child.midi.outs.iter().zip(parent.midi.outs.iter())
6767 {
6768 let child_out_lock = child_out.lock();
6769 if !child_out_lock
6770 .connections
6771 .iter()
6772 .any(|c| Arc::ptr_eq(c, parent_out))
6773 {
6774 child_out_lock.connections.push(parent_out.clone());
6775 }
6776 }
6777 }
6778 child.invalidate_audio_route_cache();
6779 parent.invalidate_audio_route_cache();
6780 child.invalidate_midi_route_cache();
6781 parent.invalidate_midi_route_cache();
6782 }
6783 }
6784 }
6785
6786 {
6789 let state = self.state.lock();
6790 if let Some(child_arc) = state.tracks.get(track_name).cloned() {
6791 let child = child_arc.lock();
6792 child.ensure_default_audio_passthrough();
6793 child.ensure_default_midi_passthrough();
6794 }
6795 }
6796
6797 for action in disconnect_actions {
6798 self.notify_clients(Ok(action)).await;
6799 }
6800
6801 self.notify_clients(Ok(Action::TrackSetParent {
6802 track_name: track_name.to_string(),
6803 parent_name: parent_name.map(str::to_string),
6804 }))
6805 .await;
6806 }
6807
6808 async fn handle_track_offline_bounce(&mut self, action: Action) {
6809 let Action::TrackOfflineBounce {
6810 track_name,
6811 output_path,
6812 start_sample,
6813 length_samples,
6814 automation_lanes,
6815 apply_fader,
6816 } = action
6817 else {
6818 return;
6819 };
6820 if self.offline_bounce_jobs.contains_key(&track_name) {
6821 self.notify_clients(Err(format!(
6822 "Offline bounce for track '{}' is already in progress",
6823 track_name
6824 )))
6825 .await;
6826 return;
6827 }
6828 if let Err(e) = self.track_handle_or_err(&track_name) {
6829 self.notify_clients(Err(e)).await;
6830 return;
6831 }
6832 if length_samples == 0 {
6833 self.notify_clients(Err(format!(
6834 "Track '{}' has no renderable content for offline bounce",
6835 track_name
6836 )))
6837 .await;
6838 return;
6839 }
6840 let Some(worker_index) = self.take_ready_worker_index() else {
6841 self.pending_requests
6842 .push_front(Action::TrackOfflineBounce {
6843 track_name,
6844 output_path,
6845 start_sample,
6846 length_samples,
6847 automation_lanes,
6848 apply_fader,
6849 });
6850 return;
6851 };
6852 let cancel = Arc::new(AtomicBool::new(false));
6853 self.offline_bounce_jobs.insert(
6854 track_name.clone(),
6855 OfflineBounceJob {
6856 cancel: cancel.clone(),
6857 },
6858 );
6859 let track_name_clone = track_name.clone();
6860 let worker = &self.workers[worker_index];
6861 let job = crate::message::OfflineBounceWork {
6862 state: self.state.clone(),
6863 track_name,
6864 output_path,
6865 start_sample,
6866 length_samples,
6867 tempo_bpm: self.tempo_bpm,
6868 tsig_num: self.tsig_num,
6869 tsig_denom: self.tsig_denom,
6870 automation_lanes,
6871 cancel,
6872 apply_fader,
6873 };
6874 if let Err(e) = worker.tx.send(Message::ProcessOfflineBounce(job)).await {
6875 self.offline_bounce_jobs.remove(&track_name_clone);
6876 self.notify_clients(Err(format!("Failed to schedule offline bounce: {e}")))
6877 .await;
6878 }
6879 }
6880
6881 async fn handle_clip_move(&mut self, action: Action) {
6882 let Action::ClipMove {
6883 ref kind,
6884 ref from,
6885 ref to,
6886 copy,
6887 } = action
6888 else {
6889 return;
6890 };
6891 if let Some(from_track_handle) = self.state.lock().tracks.get(&from.track_name)
6892 && let Some(to_track_handle) = self.state.lock().tracks.get(&to.track_name)
6893 {
6894 let from_track = from_track_handle.lock();
6895 let to_track = to_track_handle.lock();
6896 match kind {
6897 Kind::Audio => {
6898 if from.clip_index >= from_track.audio.clips.len() {
6899 self.notify_clients(Err(format!(
6900 "Clip index {} is too high, as track {} has only {} clips!",
6901 from.clip_index,
6902 from_track.name.clone(),
6903 from_track.audio.clips.len(),
6904 )))
6905 .await;
6906 return;
6907 }
6908 if from_track.audio.ins.len() != to_track.audio.ins.len() {
6909 self.notify_clients(Err(format!(
6910 "Cannot move/copy audio clip from '{}' ({} inputs) to '{}' ({} inputs)",
6911 from_track.name,
6912 from_track.audio.ins.len(),
6913 to_track.name,
6914 to_track.audio.ins.len()
6915 )))
6916 .await;
6917 return;
6918 }
6919 let clip_copy = from_track.audio.clips[from.clip_index].clone();
6920 if !copy {
6921 from_track.audio.clips.remove(from.clip_index);
6922 }
6923 let mut clip_copy = clip_copy;
6924 clip_copy.start = to.sample_offset;
6925 let max_lane = to_track.audio.ins.len().saturating_sub(1);
6926 clip_copy.input_channel = to.input_channel.min(max_lane);
6927 to_track.audio.clips.push(clip_copy);
6928 }
6929 Kind::MIDI => {
6930 if from.clip_index >= from_track.midi.clips.len() {
6931 self.notify_clients(Err(format!(
6932 "Clip index {} is too high, as track {} has only {} clips!",
6933 from.clip_index,
6934 from_track.name.clone(),
6935 from_track.midi.clips.len(),
6936 )))
6937 .await;
6938 return;
6939 }
6940 let clip_copy = from_track.midi.clips[from.clip_index].clone();
6941 if !copy {
6942 from_track.midi.clips.remove(from.clip_index);
6943 }
6944 let mut clip_copy = clip_copy;
6945 clip_copy.start = to.sample_offset;
6946 let max_lane = to_track.midi.ins.len().saturating_sub(1);
6947 clip_copy.input_channel = to.input_channel.min(max_lane);
6948 to_track.midi.clips.push(clip_copy);
6949 }
6950 }
6951 }
6952 }
6953
6954 async fn handle_open_audio_device(&mut self, action: Action) -> (bool, Option<Action>) {
6955 let Action::OpenAudioDevice {
6956 device,
6957 input_device,
6958 sample_rate_hz,
6959 bits,
6960 exclusive,
6961 period_frames,
6962 nperiods,
6963 sync_mode,
6964 ..
6965 } = action
6966 else {
6967 return (false, None);
6968 };
6969 #[cfg(unix)]
6970 {
6971 let request = AudioOpenRequest {
6972 device: &device,
6973 input_device: input_device.as_deref(),
6974 sample_rate_hz,
6975 bits,
6976 exclusive,
6977 period_frames,
6978 nperiods,
6979 sync_mode,
6980 };
6981 if self.maybe_open_jack_runtime(request).await.is_some() {
6982 return (true, None);
6983 }
6984 }
6985 let hw_opts = Self::build_hw_options(exclusive, period_frames, nperiods, sync_mode);
6986 let open_result = self
6987 .open_non_jack_audio_device(
6988 &device,
6989 input_device.as_deref(),
6990 sample_rate_hz,
6991 bits,
6992 hw_opts,
6993 )
6994 .await;
6995 match open_result {
6996 Ok(()) => {}
6997 Err(e) => {
6998 error!("Failed to open audio device: {e}");
6999 self.notify_clients(Err(e)).await;
7000 return (true, None);
7001 }
7002 }
7003 self.finalize_open_audio_device().await;
7004 if let Some(hw) = &self.hw_driver {
7005 let effective_action = {
7006 let hw = hw.lock();
7007 Action::OpenAudioDevice {
7008 device: device.clone(),
7009 input_device: input_device.clone(),
7010 sample_rate_hz: hw.sample_rate(),
7011 bits: hw.sample_bits(),
7012 exclusive,
7013 period_frames,
7014 nperiods,
7015 sync_mode,
7016 actual_period_frames: hw.cycle_samples(),
7017 input_channels: hw.input_channels(),
7018 output_channels: hw.output_channels(),
7019 bytes_per_frame: hw.frame_size_bytes(),
7020 }
7021 };
7022 return (false, Some(effective_action));
7023 }
7024 (false, None)
7025 }
7026
7027 async fn handle_jack_add_audio_input_port(&mut self, a: Action) -> bool {
7028 #[cfg(unix)]
7029 {
7030 if let Some(jack) = self.jack_runtime.clone() {
7031 let (input_channels, output_channels, rate) = {
7032 let jack = jack.lock();
7033 if let Err(e) = jack.add_audio_input_port() {
7034 self.notify_clients(Err(e)).await;
7035 return true;
7036 }
7037 (
7038 jack.input_channels(),
7039 jack.output_channels(),
7040 jack.sample_rate,
7041 )
7042 };
7043 self.publish_hw_infos(input_channels, output_channels, rate)
7044 .await;
7045 self.notify_clients(Ok(a.clone())).await;
7046 } else {
7047 self.notify_clients(Err(
7048 "JACK runtime is not active; open the JACK backend first".to_string(),
7049 ))
7050 .await;
7051 }
7052 }
7053 #[cfg(not(unix))]
7054 {
7055 self.notify_clients(Err(
7056 "JACK backend is not available on this platform build".to_string()
7057 ))
7058 .await;
7059 }
7060 false
7061 }
7062
7063 async fn handle_jack_remove_audio_input_port(
7064 &mut self,
7065 _removed_port: usize,
7066 a: Action,
7067 ) -> bool {
7068 #[cfg(unix)]
7069 {
7070 let removed_port = _removed_port;
7071 if let Some(jack) = self.jack_runtime.clone() {
7072 let (removed_port, removed_io) = {
7073 let jack = jack.lock();
7074 let removed_port = Some(removed_port);
7075 let removed_io = removed_port.and_then(|port| jack.input_audio_port(port));
7076 match (removed_port, removed_io) {
7077 (Some(port), Some(io)) => (port, io),
7078 _ => {
7079 self.notify_clients(Err(
7080 "JACK audio input port index is out of range".to_string()
7081 ))
7082 .await;
7083 return true;
7084 }
7085 }
7086 };
7087 let reindex_notifications =
7088 self.reindex_notifications_for_removed_hw_input(removed_port);
7089 for disconnect in
7090 self.disconnect_actions_for_removed_hw_input(removed_port, &removed_io)
7091 {
7092 if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await {
7093 self.notify_clients(Err(e)).await;
7094 return true;
7095 }
7096 }
7097 let (input_channels, output_channels, rate) = {
7098 let jack = jack.lock();
7099 if let Err(e) = jack.remove_audio_input_port(removed_port) {
7100 self.notify_clients(Err(e)).await;
7101 return true;
7102 }
7103 (
7104 jack.input_channels(),
7105 jack.output_channels(),
7106 jack.sample_rate,
7107 )
7108 };
7109 for action in reindex_notifications {
7110 self.notify_clients(Ok(action)).await;
7111 }
7112 self.publish_hw_infos(input_channels, output_channels, rate)
7113 .await;
7114 self.notify_clients(Ok(a.clone())).await;
7115 } else {
7116 self.notify_clients(Err(
7117 "JACK runtime is not active; open the JACK backend first".to_string(),
7118 ))
7119 .await;
7120 }
7121 }
7122 #[cfg(not(unix))]
7123 {
7124 self.notify_clients(Err(
7125 "JACK backend is not available on this platform build".to_string()
7126 ))
7127 .await;
7128 }
7129 false
7130 }
7131
7132 async fn handle_jack_add_audio_output_port(&mut self, a: Action) -> bool {
7133 #[cfg(unix)]
7134 {
7135 if let Some(jack) = self.jack_runtime.clone() {
7136 let (input_channels, output_channels, rate) = {
7137 let jack = jack.lock();
7138 if let Err(e) = jack.add_audio_output_port() {
7139 self.notify_clients(Err(e)).await;
7140 return true;
7141 }
7142 (
7143 jack.input_channels(),
7144 jack.output_channels(),
7145 jack.sample_rate,
7146 )
7147 };
7148 self.publish_hw_infos(input_channels, output_channels, rate)
7149 .await;
7150 self.notify_clients(Ok(a.clone())).await;
7151 } else {
7152 self.notify_clients(Err(
7153 "JACK runtime is not active; open the JACK backend first".to_string(),
7154 ))
7155 .await;
7156 }
7157 }
7158 #[cfg(not(unix))]
7159 {
7160 self.notify_clients(Err(
7161 "JACK backend is not available on this platform build".to_string()
7162 ))
7163 .await;
7164 }
7165 false
7166 }
7167
7168 async fn handle_jack_remove_audio_output_port(
7169 &mut self,
7170 _removed_port: usize,
7171 a: Action,
7172 ) -> bool {
7173 #[cfg(unix)]
7174 {
7175 let removed_port = _removed_port;
7176 if let Some(jack) = self.jack_runtime.clone() {
7177 let (removed_port, removed_io) = {
7178 let jack = jack.lock();
7179 let removed_port = Some(removed_port);
7180 let removed_io = removed_port.and_then(|port| jack.output_audio_port(port));
7181 match (removed_port, removed_io) {
7182 (Some(port), Some(io)) => (port, io),
7183 _ => {
7184 self.notify_clients(Err(
7185 "JACK audio output port index is out of range".to_string(),
7186 ))
7187 .await;
7188 return true;
7189 }
7190 }
7191 };
7192 let reindex_notifications =
7193 self.reindex_notifications_for_removed_hw_output(removed_port);
7194 for disconnect in
7195 self.disconnect_actions_for_removed_hw_output(removed_port, &removed_io)
7196 {
7197 if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await {
7198 self.notify_clients(Err(e)).await;
7199 return true;
7200 }
7201 }
7202 let (input_channels, output_channels, rate) = {
7203 let jack = jack.lock();
7204 if let Err(e) = jack.remove_audio_output_port(removed_port) {
7205 self.notify_clients(Err(e)).await;
7206 return true;
7207 }
7208 (
7209 jack.input_channels(),
7210 jack.output_channels(),
7211 jack.sample_rate,
7212 )
7213 };
7214 for action in reindex_notifications {
7215 self.notify_clients(Ok(action)).await;
7216 }
7217 self.publish_hw_infos(input_channels, output_channels, rate)
7218 .await;
7219 self.notify_clients(Ok(a.clone())).await;
7220 } else {
7221 self.notify_clients(Err(
7222 "JACK runtime is not active; open the JACK backend first".to_string(),
7223 ))
7224 .await;
7225 }
7226 }
7227 #[cfg(not(unix))]
7228 {
7229 self.notify_clients(Err(
7230 "JACK backend is not available on this platform build".to_string()
7231 ))
7232 .await;
7233 }
7234 false
7235 }
7236
7237 async fn handle_track_load_clap_plugin(
7238 &mut self,
7239 track_name: &str,
7240 plugin_id: &str,
7241 instance_id: Option<usize>,
7242 ) -> bool {
7243 if self
7244 .reject_if_track_frozen(track_name, "CLAP plugin loading")
7245 .await
7246 {
7247 return true;
7248 }
7249 let resolved_plugin_path = match self.resolve_plugin_identifier(PluginKind::Clap, plugin_id)
7250 {
7251 Ok(path) => path,
7252 Err(e) => {
7253 self.notify_clients(Err(e)).await;
7254 return true;
7255 }
7256 };
7257 let track = match self.track_handle_or_err(track_name) {
7258 Ok(track) => track,
7259 Err(e) => {
7260 self.notify_clients(Err(e)).await;
7261 return true;
7262 }
7263 };
7264 let track = track.lock();
7265 if track.audio.processing {
7266 self.notify_clients(Err(format!(
7267 "Track '{}' is currently processing audio; stop playback before loading CLAP plugins",
7268 track_name
7269 )))
7270 .await;
7271 return true;
7272 }
7273 if let Err(e) = track.load_clap_plugin(&resolved_plugin_path, instance_id) {
7274 self.notify_clients(Err(e)).await;
7275 return true;
7276 }
7277 self.notify_clients(Ok(Action::Log {
7278 source: "engine".to_string(),
7279 message: format!("CLAP plugin loaded on track '{track_name}': {resolved_plugin_path}"),
7280 }))
7281 .await;
7282 if let Some(instance) = track.clap_plugins.last()
7283 && let Some(stderr) = instance.processor.lock().take_stderr()
7284 {
7285 let source = format!("clap:{resolved_plugin_path}");
7286 self.spawn_plugin_host_stderr_reader(stderr, source);
7287 self.notify_clients(Ok(Action::Log {
7288 source: "engine".to_string(),
7289 message: format!("Attached stderr reader for CLAP plugin on track '{track_name}'"),
7290 }))
7291 .await;
7292 }
7293 false
7294 }
7295
7296 async fn handle_track_unload_clap_plugin(&mut self, track_name: &str, plugin_id: &str) -> bool {
7297 if self
7298 .reject_if_track_frozen(track_name, "CLAP plugin unloading")
7299 .await
7300 {
7301 return true;
7302 }
7303 let track = match self.track_handle_or_err(track_name) {
7304 Ok(track) => track,
7305 Err(e) => {
7306 self.notify_clients(Err(e)).await;
7307 return true;
7308 }
7309 };
7310 let track = track.lock();
7311 if track.audio.processing {
7312 self.notify_clients(Err(format!(
7313 "Track '{}' is currently processing audio; stop playback before unloading CLAP plugins",
7314 track_name
7315 )))
7316 .await;
7317 return true;
7318 }
7319 if let Err(e) = track.unload_clap_plugin(plugin_id) {
7320 self.notify_clients(Err(e)).await;
7321 return true;
7322 }
7323 false
7324 }
7325
7326 async fn handle_track_unload_clap_plugin_instance(
7327 &mut self,
7328 track_name: &str,
7329 instance_id: usize,
7330 ) -> bool {
7331 if self
7332 .reject_if_track_frozen(track_name, "CLAP plugin unloading")
7333 .await
7334 {
7335 return true;
7336 }
7337 let track = match self.track_handle_or_err(track_name) {
7338 Ok(track) => track,
7339 Err(e) => {
7340 self.notify_clients(Err(e)).await;
7341 return true;
7342 }
7343 };
7344 let track = track.lock();
7345 if track.audio.processing {
7346 self.notify_clients(Err(format!(
7347 "Track '{}' is currently processing audio; stop playback before unloading CLAP plugins",
7348 track_name
7349 )))
7350 .await;
7351 return true;
7352 }
7353 if let Err(e) = track.unload_clap_plugin_instance(instance_id) {
7354 self.notify_clients(Err(e)).await;
7355 return true;
7356 }
7357 false
7358 }
7359
7360 async fn handle_track_load_vst3_plugin(
7361 &mut self,
7362 track_name: &str,
7363 plugin_id: &str,
7364 instance_id: Option<usize>,
7365 ) -> bool {
7366 if self
7367 .reject_if_track_frozen(track_name, "VST3 plugin loading")
7368 .await
7369 {
7370 return true;
7371 }
7372 let resolved_plugin_path = match self.resolve_plugin_identifier(PluginKind::Vst3, plugin_id)
7373 {
7374 Ok(path) => path,
7375 Err(e) => {
7376 self.notify_clients(Err(e)).await;
7377 return true;
7378 }
7379 };
7380 let track = match self.track_handle_or_err(track_name) {
7381 Ok(track) => track,
7382 Err(e) => {
7383 self.notify_clients(Err(e)).await;
7384 return true;
7385 }
7386 };
7387 let track = track.lock();
7388 if track.audio.processing {
7389 self.notify_clients(Err(format!(
7390 "Track '{}' is currently processing audio; stop playback before loading VST3 plugins",
7391 track_name
7392 )))
7393 .await;
7394 return true;
7395 }
7396 if let Err(e) = track.load_vst3_plugin(plugin_id, &resolved_plugin_path, instance_id) {
7397 self.notify_clients(Err(e)).await;
7398 return true;
7399 }
7400 if let Some(instance) = track.vst3_plugins.last()
7401 && let Some(stderr) = instance.processor.lock().take_stderr()
7402 {
7403 let source = format!("vst3:{resolved_plugin_path}");
7404 self.spawn_plugin_host_stderr_reader(stderr, source);
7405 }
7406 false
7407 }
7408
7409 async fn handle_track_unload_vst3_plugin(&mut self, track_name: &str, plugin_id: &str) -> bool {
7410 if self
7411 .reject_if_track_frozen(track_name, "VST3 plugin unloading")
7412 .await
7413 {
7414 return true;
7415 }
7416 let track = match self.track_handle_or_err(track_name) {
7417 Ok(track) => track,
7418 Err(e) => {
7419 self.notify_clients(Err(e)).await;
7420 return true;
7421 }
7422 };
7423 let track = track.lock();
7424 if track.audio.processing {
7425 self.notify_clients(Err(format!(
7426 "Track '{}' is currently processing audio; stop playback before unloading VST3 plugins",
7427 track_name
7428 )))
7429 .await;
7430 return true;
7431 }
7432 if let Err(e) = track.unload_vst3_plugin(plugin_id) {
7433 self.notify_clients(Err(e)).await;
7434 return true;
7435 }
7436 false
7437 }
7438
7439 async fn handle_track_unload_vst3_plugin_instance(
7440 &mut self,
7441 track_name: &str,
7442 instance_id: usize,
7443 ) -> bool {
7444 if self
7445 .reject_if_track_frozen(track_name, "VST3 plugin unloading")
7446 .await
7447 {
7448 return true;
7449 }
7450 let track = match self.track_handle_or_err(track_name) {
7451 Ok(track) => track,
7452 Err(e) => {
7453 self.notify_clients(Err(e)).await;
7454 return true;
7455 }
7456 };
7457 let track = track.lock();
7458 if track.audio.processing {
7459 self.notify_clients(Err(format!(
7460 "Track '{}' is currently processing audio; stop playback before unloading VST3 plugins",
7461 track_name
7462 )))
7463 .await;
7464 return true;
7465 }
7466 if let Err(e) = track.unload_vst3_plugin_instance(instance_id) {
7467 self.notify_clients(Err(e)).await;
7468 return true;
7469 }
7470 false
7471 }
7472
7473 #[cfg(all(unix, not(target_os = "macos")))]
7474 async fn handle_track_load_lv2_plugin(
7475 &mut self,
7476 track_name: &str,
7477 plugin_uri: &str,
7478 instance_id: Option<usize>,
7479 ) -> bool {
7480 if self
7481 .reject_if_track_frozen(track_name, "LV2 plugin loading")
7482 .await
7483 {
7484 return true;
7485 }
7486 let resolved_plugin_uri = match self.resolve_plugin_identifier(PluginKind::Lv2, plugin_uri)
7487 {
7488 Ok(uri) => uri,
7489 Err(e) => {
7490 self.notify_clients(Err(e)).await;
7491 return true;
7492 }
7493 };
7494 let track = match self.track_handle_or_err(track_name) {
7495 Ok(track) => track,
7496 Err(e) => {
7497 self.notify_clients(Err(e)).await;
7498 return true;
7499 }
7500 };
7501 let track = track.lock();
7502 if track.audio.processing {
7503 self.notify_clients(Err(format!(
7504 "Track '{}' is currently processing audio; stop playback before loading LV2 plugins",
7505 track_name
7506 )))
7507 .await;
7508 return true;
7509 }
7510 if let Err(e) = track.load_lv2_plugin(&resolved_plugin_uri, instance_id) {
7511 self.notify_clients(Err(e)).await;
7512 return true;
7513 }
7514 if let Some(instance) = track.lv2_plugins.last()
7515 && let Some(stderr) = instance.processor.lock().take_stderr()
7516 {
7517 let source = format!("lv2:{resolved_plugin_uri}");
7518 self.spawn_plugin_host_stderr_reader(stderr, source);
7519 }
7520 false
7521 }
7522
7523 #[cfg(all(unix, not(target_os = "macos")))]
7524 async fn handle_track_unload_lv2_plugin(&mut self, track_name: &str, plugin_uri: &str) -> bool {
7525 if self
7526 .reject_if_track_frozen(track_name, "LV2 plugin unloading")
7527 .await
7528 {
7529 return true;
7530 }
7531 let resolved_plugin_uri = match self.resolve_plugin_identifier(PluginKind::Lv2, plugin_uri)
7532 {
7533 Ok(uri) => uri,
7534 Err(e) => {
7535 self.notify_clients(Err(e)).await;
7536 return true;
7537 }
7538 };
7539 let track = match self.track_handle_or_err(track_name) {
7540 Ok(track) => track,
7541 Err(e) => {
7542 self.notify_clients(Err(e)).await;
7543 return true;
7544 }
7545 };
7546 let track = track.lock();
7547 if track.audio.processing {
7548 self.notify_clients(Err(format!(
7549 "Track '{}' is currently processing audio; stop playback before unloading LV2 plugins",
7550 track_name
7551 )))
7552 .await;
7553 return true;
7554 }
7555 if let Err(e) = track.unload_lv2_plugin(&resolved_plugin_uri) {
7556 self.notify_clients(Err(e)).await;
7557 return true;
7558 }
7559 false
7560 }
7561
7562 #[cfg(all(unix, not(target_os = "macos")))]
7563 async fn handle_track_unload_lv2_plugin_instance(
7564 &mut self,
7565 track_name: &str,
7566 instance_id: usize,
7567 ) -> bool {
7568 tracing::info!(%track_name, instance_id, "Engine handling TrackUnloadLv2PluginInstance");
7569 if self
7570 .reject_if_track_frozen(track_name, "LV2 plugin unloading")
7571 .await
7572 {
7573 return true;
7574 }
7575 let track = match self.track_handle_or_err(track_name) {
7576 Ok(track) => track,
7577 Err(e) => {
7578 self.notify_clients(Err(e)).await;
7579 return true;
7580 }
7581 };
7582 let track = track.lock();
7583 if track.audio.processing {
7584 self.notify_clients(Err(format!(
7585 "Track '{}' is currently processing audio; stop playback before unloading LV2 plugins",
7586 track_name
7587 )))
7588 .await;
7589 return true;
7590 }
7591 if let Err(e) = track.unload_lv2_plugin_instance(instance_id) {
7592 self.notify_clients(Err(e)).await;
7593 return true;
7594 }
7595 tracing::info!(%track_name, instance_id, "Engine TrackUnloadLv2PluginInstance complete");
7596 false
7597 }
7598
7599 async fn handle_play(&mut self, action: Action) -> bool {
7600 let Action::Play = action else {
7601 return false;
7602 };
7603
7604 tracing::debug!(
7605 "Action::Play pressed, transport_sample={}",
7606 self.transport_sample
7607 );
7608 self.playing = true;
7609 self.transport_running = true;
7610 self.transport_restart_pending = true;
7611 self.notified_loop_wrap_sample = None;
7612 self.invalidate_track_cycle_state();
7613 if let Some(driver) = self.hw_driver.as_mut() {
7614 driver.lock().set_playing(true);
7615 }
7616 #[cfg(unix)]
7617 if let Some(jack) = &self.jack_runtime
7618 && let Err(e) = jack.lock().transport_start()
7619 {
7620 self.notify_clients(Err(e)).await;
7621 }
7622 self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
7623 .await;
7624 self.preload_track_clips().await;
7625 {
7626 let echoes = self.apply_modulators(self.active_transport_sample());
7627 for action in echoes {
7628 self.notify_clients(Ok(action)).await;
7629 }
7630 }
7631 let send_result = self.send_tasks().await;
7632 tracing::debug!("send_tasks after Play returned finished={}", send_result);
7633 if !self.awaiting_hwfinished
7634 && !self.handling_hwfinished
7635 && send_result
7636 && self.hw_worker.is_some()
7637 {
7638 self.transport_restart_pending = false;
7639 self.request_hw_cycle().await;
7640 }
7641
7642 false
7643 }
7644
7645 async fn handle_pause(&mut self, action: Action) -> bool {
7646 let Action::Pause = action else {
7647 return false;
7648 };
7649
7650 self.clip_playback_enabled = false;
7651 self.session_clip_playback_enabled = false;
7652 for track in self.state.lock().tracks.values() {
7653 let t = track.lock();
7654 t.set_clip_playback_enabled(false);
7655 t.set_session_clip_playback_enabled(false);
7656 }
7657 self.transport_running = false;
7658 if !self.playing {
7659 self.playing = true;
7660 self.transport_restart_pending = true;
7661 self.notified_loop_wrap_sample = None;
7662 self.invalidate_track_cycle_state();
7663 if let Some(driver) = self.hw_driver.as_mut() {
7664 driver.lock().set_playing(true);
7665 }
7666 #[cfg(unix)]
7667 if let Some(jack) = &self.jack_runtime
7668 && let Err(e) = jack.lock().transport_start()
7669 {
7670 self.notify_clients(Err(e)).await;
7671 }
7672 self.preload_track_clips().await;
7673 if !self.awaiting_hwfinished
7674 && !self.handling_hwfinished
7675 && self.send_tasks().await
7676 && self.hw_worker.is_some()
7677 {
7678 self.transport_restart_pending = false;
7679 self.request_hw_cycle().await;
7680 }
7681 }
7682 self.notify_clients(Ok(Action::Pause)).await;
7683 self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
7684 .await;
7685
7686 false
7687 }
7688
7689 async fn handle_stop(&mut self, action: Action) -> bool {
7690 let Action::Stop = action else {
7691 return false;
7692 };
7693
7694 self.playing = false;
7695 self.transport_running = false;
7696 self.transport_panic_flush_pending = false;
7697 self.transport_restart_pending = false;
7698 self.notified_loop_wrap_sample = None;
7699 self.clip_playback_enabled = true;
7700 self.session_clip_playback_enabled = false;
7701 self.session_transport_sample = 0;
7702 for track in self.state.lock().tracks.values() {
7703 let t = track.lock();
7704 t.set_clip_playback_enabled(true);
7705 t.set_session_clip_playback_enabled(false);
7706 }
7707 self.invalidate_track_cycle_state();
7708 if let Some(driver) = self.hw_driver.as_mut() {
7709 driver.lock().set_playing(false);
7710 }
7711 #[cfg(unix)]
7712 if let Some(jack) = &self.jack_runtime
7713 && let Err(e) = jack.lock().transport_stop()
7714 {
7715 self.notify_clients(Err(e)).await;
7716 }
7717 let panic_events = self.note_off_events_for_all_active_tracks();
7718 if let Some(worker) = &self.hw_worker {
7719 if !panic_events.is_empty()
7720 && let Err(e) = worker.tx.send(Message::HWMidiOutEvents(panic_events)).await
7721 {
7722 error!("Error sending stop MIDI panic events {e}");
7723 }
7724 } else {
7725 self.pending_hw_midi_out_events_by_device
7726 .extend(panic_events);
7727 }
7728 self.reset_meters_after_stop();
7729 self.flush_recordings().await;
7730 self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
7731 .await;
7732
7733 false
7734 }
7735
7736 async fn handle_session_play(&mut self, action: Action) -> bool {
7737 let Action::SessionPlay = action else {
7738 return false;
7739 };
7740
7741 tracing::info!(
7742 "Action::SessionPlay pressed, transport_sample={} playing={} transport_running={} clip_enabled={} session_enabled={}",
7743 self.transport_sample,
7744 self.playing,
7745 self.transport_running,
7746 self.clip_playback_enabled,
7747 self.session_clip_playback_enabled
7748 );
7749 self.playing = true;
7750 self.transport_running = false;
7751 self.transport_restart_pending = true;
7752 self.notified_loop_wrap_sample = None;
7753 self.invalidate_track_cycle_state();
7754 self.clip_playback_enabled = false;
7755 self.session_clip_playback_enabled = true;
7756 self.session_transport_sample = 0;
7757 if let Some(driver) = self.hw_driver.as_mut() {
7758 driver.lock().set_playing(true);
7759 }
7760 #[cfg(unix)]
7761 if let Some(jack) = &self.jack_runtime
7762 && let Err(e) = jack.lock().transport_start()
7763 {
7764 self.notify_clients(Err(e)).await;
7765 }
7766 self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
7767 .await;
7768 self.preload_track_clips().await;
7769 {
7770 let echoes = self.apply_modulators(self.active_transport_sample());
7771 for action in echoes {
7772 self.notify_clients(Ok(action)).await;
7773 }
7774 }
7775 let send_result = self.send_tasks().await;
7776 tracing::info!(
7777 "Action::SessionPlay send_result={} awaiting_hwfinished={} handling_hwfinished={} hw_worker={}",
7778 send_result,
7779 self.awaiting_hwfinished,
7780 self.handling_hwfinished,
7781 self.hw_worker.is_some()
7782 );
7783 if !self.awaiting_hwfinished
7784 && !self.handling_hwfinished
7785 && send_result
7786 && self.hw_worker.is_some()
7787 {
7788 self.transport_restart_pending = false;
7789 self.request_hw_cycle().await;
7790 }
7791
7792 false
7793 }
7794
7795 async fn handle_rename_track(&mut self, action: Action) -> bool {
7796 let Action::RenameTrack {
7797 ref old_name,
7798 ref new_name,
7799 } = action
7800 else {
7801 return false;
7802 };
7803
7804 if self.state.lock().tracks.contains_key(new_name) {
7805 self.notify_clients(Err(format!("Track '{}' already exists", new_name)))
7806 .await;
7807 return true;
7808 }
7809
7810 let Some(track) = self.state.lock().tracks.remove(old_name) else {
7811 self.notify_clients(Err(format!("Track '{}' not found", old_name)))
7812 .await;
7813 return true;
7814 };
7815
7816 track.lock().name = new_name.clone();
7817 self.state.lock().tracks.insert(new_name.clone(), track);
7818 for other in self.state.lock().tracks.values() {
7819 let other = other.lock();
7820 if other.parent_track.as_deref() == Some(old_name.as_str()) {
7821 other.parent_track = Some(new_name.clone());
7822 }
7823 }
7824
7825 if let Some(recording) = self.audio_recordings.remove(old_name) {
7826 self.audio_recordings.insert(new_name.clone(), recording);
7827 }
7828 if let Some(recording) = self.midi_recordings.remove(old_name) {
7829 self.midi_recordings.insert(new_name.clone(), recording);
7830 }
7831
7832 for route in &mut self.midi_hw_in_routes {
7833 if route.to_track == *old_name {
7834 route.to_track = new_name.clone();
7835 }
7836 }
7837 for route in &mut self.midi_hw_out_routes {
7838 if route.from_track == *old_name {
7839 route.from_track = new_name.clone();
7840 }
7841 }
7842 if let Some((armed_track, target, device)) = self.pending_midi_learn.clone()
7843 && armed_track == *old_name
7844 {
7845 self.pending_midi_learn = Some((new_name.clone(), target, device));
7846 }
7847
7848 self.notify_clients(Ok(Action::RenameTrack {
7849 old_name: old_name.clone(),
7850 new_name: new_name.clone(),
7851 }))
7852 .await;
7853
7854 false
7855 }
7856
7857 async fn handle_track_set_midi_learn_binding(&mut self, action: Action) -> bool {
7858 let Action::TrackSetMidiLearnBinding {
7859 ref track_name,
7860 target,
7861 ref binding,
7862 } = action
7863 else {
7864 return false;
7865 };
7866
7867 if let Some(binding) = binding.as_ref() {
7868 let conflicts = self.midi_learn_slot_conflicts(
7869 binding,
7870 Some(MidiLearnSlot::Track(track_name.clone(), target)),
7871 );
7872 if !conflicts.is_empty() {
7873 self.notify_clients(Err(format!(
7874 "MIDI learn conflict for '{}' {:?}: {}",
7875 track_name,
7876 target,
7877 conflicts.join(", ")
7878 )))
7879 .await;
7880 return true;
7881 }
7882 }
7883 let track = match self.track_handle_or_err(track_name) {
7884 Ok(track) => track,
7885 Err(e) => {
7886 self.notify_clients(Err(e)).await;
7887 return true;
7888 }
7889 };
7890 match target {
7891 crate::message::TrackMidiLearnTarget::Volume => {
7892 track.lock().midi_learn_volume = binding.clone();
7893 }
7894 crate::message::TrackMidiLearnTarget::Balance => {
7895 track.lock().midi_learn_balance = binding.clone();
7896 }
7897 crate::message::TrackMidiLearnTarget::Mute => {
7898 track.lock().midi_learn_mute = binding.clone();
7899 }
7900 crate::message::TrackMidiLearnTarget::Solo => {
7901 track.lock().midi_learn_solo = binding.clone();
7902 }
7903 crate::message::TrackMidiLearnTarget::Arm => {
7904 track.lock().midi_learn_arm = binding.clone();
7905 }
7906 crate::message::TrackMidiLearnTarget::InputMonitor => {
7907 track.lock().midi_learn_input_monitor = binding.clone();
7908 }
7909 crate::message::TrackMidiLearnTarget::DiskMonitor => {
7910 track.lock().midi_learn_disk_monitor = binding.clone();
7911 }
7912 }
7913
7914 false
7915 }
7916
7917 async fn handle_set_session_midi_learn_binding(&mut self, action: Action) -> bool {
7918 let Action::SetSessionMidiLearnBinding {
7919 ref target,
7920 ref binding,
7921 } = action
7922 else {
7923 return false;
7924 };
7925
7926 if let Some(binding) = binding.as_ref() {
7927 let conflicts = self
7928 .midi_learn_slot_conflicts(binding, Some(MidiLearnSlot::Session(target.clone())));
7929 if !conflicts.is_empty() {
7930 self.notify_clients(Err(format!(
7931 "Session MIDI learn conflict for {:?}: {}",
7932 target,
7933 conflicts.join(", ")
7934 )))
7935 .await;
7936 return true;
7937 }
7938 }
7939 match target {
7940 crate::message::SessionMidiLearnTarget::Slot {
7941 track_name,
7942 scene_index,
7943 } => {
7944 if binding.is_some() {
7945 self.session_midi_learn_slots
7946 .insert((track_name.clone(), *scene_index), binding.clone().unwrap());
7947 } else {
7948 self.session_midi_learn_slots
7949 .remove(&(track_name.clone(), *scene_index));
7950 }
7951 }
7952 crate::message::SessionMidiLearnTarget::Scene(scene_index) => {
7953 if binding.is_some() {
7954 self.session_midi_learn_scenes
7955 .insert(*scene_index, binding.clone().unwrap());
7956 } else {
7957 self.session_midi_learn_scenes.remove(scene_index);
7958 }
7959 }
7960 crate::message::SessionMidiLearnTarget::StopTrack(track_name) => {
7961 if binding.is_some() {
7962 self.session_midi_learn_stop_track
7963 .insert(track_name.clone(), binding.clone().unwrap());
7964 } else {
7965 self.session_midi_learn_stop_track.remove(track_name);
7966 }
7967 }
7968 crate::message::SessionMidiLearnTarget::StopAll => {
7969 self.session_midi_learn_stop_all = binding.clone();
7970 }
7971 }
7972
7973 false
7974 }
7975
7976 async fn handle_track_get_plugin_graph(&mut self, action: Action) -> bool {
7977 let Action::TrackGetPluginGraph {
7978 ref track_name,
7979 include_state,
7980 } = action
7981 else {
7982 return false;
7983 };
7984
7985 let start = std::time::Instant::now();
7986 let track = match self.track_handle_or_err(track_name) {
7987 Ok(track) => track,
7988 Err(e) => {
7989 self.notify_clients(Err(e)).await;
7990 return true;
7991 }
7992 };
7993 let (plugins, connections, connectable_connections) = {
7994 let track = track.lock();
7995 let plugins_start = std::time::Instant::now();
7996 let plugins = track.plugin_graph_plugins(include_state);
7997 let plugins_ms = plugins_start.elapsed().as_millis();
7998 let conns_start = std::time::Instant::now();
7999 let connections = track.plugin_graph_connections();
8000 let conns_ms = conns_start.elapsed().as_millis();
8001 let cc_start = std::time::Instant::now();
8002 let connectable_connections = track.connectable_connections();
8003 let cc_ms = cc_start.elapsed().as_millis();
8004 tracing::debug!(
8005 %track_name,
8006 include_state,
8007 plugins_ms,
8008 conns_ms,
8009 cc_ms,
8010 "TrackGetPluginGraph collected graph data"
8011 );
8012 (plugins, connections, connectable_connections)
8013 };
8014 let total_ms = start.elapsed().as_millis();
8015 tracing::debug!(
8016 %track_name,
8017 total_ms,
8018 plugins = plugins.len(),
8019 connections = connections.len(),
8020 connectable = connectable_connections.len(),
8021 "TrackGetPluginGraph responding"
8022 );
8023 self.notify_clients(Ok(Action::TrackPluginGraph {
8024 track_name: track_name.clone(),
8025 plugins,
8026 connections,
8027 connectable_connections,
8028 }))
8029 .await;
8030 true
8031 }
8032
8033 async fn handle_add_clip(&mut self, action: Action) -> bool {
8034 let Action::AddClip {
8035 ref clip_id,
8036 ref name,
8037 ref track_name,
8038 start,
8039 length,
8040 offset,
8041 input_channel,
8042 muted,
8043 ref peaks_file,
8044 kind,
8045 fade_enabled,
8046 fade_in_samples,
8047 fade_out_samples,
8048 ref source_name,
8049 source_offset,
8050 source_length,
8051 ref preview_name,
8052 ref pitch_correction_points,
8053 pitch_correction_frame_likeness,
8054 pitch_correction_inertia_ms,
8055 pitch_correction_formant_compensation,
8056 ref plugin_graph_json,
8057 } = action
8058 else {
8059 return false;
8060 };
8061
8062 self.add_clip_to_track(ClipAddRequest {
8063 clip_id,
8064 name,
8065 track_name,
8066 start,
8067 length,
8068 offset,
8069 input_channel,
8070 muted,
8071 peaks_file: peaks_file.clone(),
8072 kind,
8073 fade_enabled,
8074 fade_in_samples,
8075 fade_out_samples,
8076 source_name: source_name.clone(),
8077 source_offset,
8078 source_length,
8079 preview_name: preview_name.clone(),
8080 pitch_correction_points: pitch_correction_points.clone(),
8081 pitch_correction_frame_likeness,
8082 pitch_correction_inertia_ms,
8083 pitch_correction_formant_compensation,
8084 plugin_graph_json: plugin_graph_json.clone(),
8085 });
8086 if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
8087 let track_name = track_name.clone();
8088 tokio::task::spawn_blocking(move || {
8089 track.lock().preload_clips();
8090 tracing::debug!("Preloaded clips for track '{}' after AddClip", track_name);
8091 });
8092 }
8093
8094 false
8095 }
8096
8097 async fn handle_transport_position(&mut self, a: Action) -> bool {
8098 let Action::TransportPosition(sample) = a else {
8099 return false;
8100 };
8101
8102 self.transport_sample = self.normalize_transport_sample(sample);
8103 self.notified_loop_wrap_sample = None;
8104 {
8105 let echoes = self.apply_modulators(self.active_transport_sample());
8106 for action in echoes {
8107 self.notify_clients(Ok(action)).await;
8108 }
8109 }
8110 #[cfg(unix)]
8111 if let Some(jack) = &self.jack_runtime
8112 && let Err(e) = jack.lock().transport_locate(self.transport_sample)
8113 {
8114 self.notify_clients(Err(e)).await;
8115 }
8116 if self.playing {
8117 self.transport_restart_pending = true;
8118 self.invalidate_track_cycle_state();
8119 self.transport_panic_flush_pending = self.hw_worker.is_some();
8120 self.clear_hw_midi_output_state(true).await;
8121 if !self.awaiting_hwfinished && !self.handling_hwfinished {
8122 if self.hw_worker.is_some() {
8123 self.request_hw_cycle().await;
8124 } else if self.send_tasks().await {
8125 self.transport_restart_pending = false;
8126 self.request_hw_cycle().await;
8127 }
8128 }
8129 }
8130
8131 false
8132 }
8133
8134 async fn handle_track_automation_insert_point(&mut self, a: Action) -> bool {
8135 let Action::TrackAutomationInsertPoint {
8136 ref track_name,
8137 ref target,
8138 sample,
8139 value,
8140 } = a
8141 else {
8142 return false;
8143 };
8144
8145 if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
8146 let track = track.lock();
8147 let mut lanes = Self::parse_automation_lanes(&track.automation_lanes);
8148 let lane = match lanes.iter_mut().find(|lane| lane.target == *target) {
8149 Some(lane) => lane,
8150 None => {
8151 lanes.push(OfflineAutomationLane {
8152 target: target.clone(),
8153 visible: true,
8154 points: vec![],
8155 });
8156 lanes.last_mut().expect("just pushed")
8157 }
8158 };
8159 if let Some(point) = lane.points.iter_mut().find(|p| p.sample == sample) {
8160 point.value = value;
8161 } else {
8162 lane.points.push(OfflineAutomationPoint { sample, value });
8163 lane.points.sort_unstable_by_key(|p| p.sample);
8164 }
8165 track.automation_lanes = serde_json::to_value(&lanes).unwrap_or_default();
8166 }
8167
8168 false
8169 }
8170
8171 async fn handle_set_global_midi_learn_binding(&mut self, a: Action) -> bool {
8172 let Action::SetGlobalMidiLearnBinding {
8173 target,
8174 ref binding,
8175 } = a
8176 else {
8177 return false;
8178 };
8179
8180 if let Some(binding) = binding.as_ref() {
8181 let conflicts =
8182 self.midi_learn_slot_conflicts(binding, Some(MidiLearnSlot::Global(target)));
8183 if !conflicts.is_empty() {
8184 self.notify_clients(Err(format!(
8185 "Global MIDI learn conflict for {:?}: {}",
8186 target,
8187 conflicts.join(", ")
8188 )))
8189 .await;
8190 return true;
8191 }
8192 }
8193 match target {
8194 crate::message::GlobalMidiLearnTarget::PlayPause => {
8195 self.global_midi_learn_play_pause = binding.clone();
8196 }
8197 crate::message::GlobalMidiLearnTarget::Stop => {
8198 self.global_midi_learn_stop = binding.clone();
8199 }
8200 crate::message::GlobalMidiLearnTarget::RecordToggle => {
8201 self.global_midi_learn_record_toggle = binding.clone();
8202 }
8203 }
8204
8205 false
8206 }
8207
8208 async fn handle_track_set_folder(&mut self, a: Action) -> bool {
8209 let Action::TrackSetFolder {
8210 ref track_name,
8211 is_folder,
8212 } = a
8213 else {
8214 return false;
8215 };
8216
8217 let track = match self.track_handle_or_err(track_name) {
8218 Ok(track) => track,
8219 Err(e) => {
8220 self.notify_clients(Err(e)).await;
8221 return true;
8222 }
8223 };
8224 if is_folder {
8225 let is_master = track.lock().is_master;
8226 if is_master {
8227 self.notify_clients(Err(format!(
8228 "Track '{}' is the master track and cannot be made a folder",
8229 track_name
8230 )))
8231 .await;
8232 return true;
8233 }
8234 }
8235 {
8236 let track = track.lock();
8237 track.is_folder = is_folder;
8238 track.ensure_default_audio_passthrough();
8239 track.ensure_default_midi_passthrough();
8240 }
8241 self.notify_clients(Ok(Action::TrackSetFolder {
8242 track_name: track_name.clone(),
8243 is_folder,
8244 }))
8245 .await;
8246
8247 false
8248 }
8249
8250 async fn handle_track_set_session_slot(&mut self, a: Action) -> bool {
8251 let Action::TrackSetSessionSlot {
8252 ref track_name,
8253 scene_index,
8254 ref clip_id,
8255 } = a
8256 else {
8257 return false;
8258 };
8259
8260 let track = match self.track_handle_or_err(track_name) {
8261 Ok(track) => track,
8262 Err(e) => {
8263 self.notify_clients(Err(e)).await;
8264 return true;
8265 }
8266 };
8267 let track = track.lock();
8268 match clip_id {
8269 Some(id) => {
8270 let play_enabled = track
8271 .session_slots
8272 .get(&scene_index)
8273 .map(|slot| slot.play_enabled)
8274 .unwrap_or(true);
8275 track.session_slots.insert(
8276 scene_index,
8277 SessionSlot {
8278 clip_id: id.clone(),
8279 play_enabled,
8280 },
8281 );
8282 }
8283 None => {
8284 track.session_slots.remove(&scene_index);
8285 }
8286 }
8287
8288 false
8289 }
8290
8291 async fn handle_track_connect_plugin_audio(&mut self, a: Action) -> bool {
8292 let Action::TrackConnectPluginAudio {
8293 ref track_name,
8294 ref from_node,
8295 from_port,
8296 ref to_node,
8297 to_port,
8298 } = a
8299 else {
8300 return false;
8301 };
8302
8303 if self
8304 .reject_if_track_frozen(track_name, "plugin routing changes")
8305 .await
8306 {
8307 return true;
8308 }
8309 let track = match self.track_handle_or_err(track_name) {
8310 Ok(track) => track,
8311 Err(e) => {
8312 self.notify_clients(Err(e)).await;
8313 return true;
8314 }
8315 };
8316 if let Err(e) = track.lock().connect_plugin_audio(
8317 from_node.clone(),
8318 from_port,
8319 to_node.clone(),
8320 to_port,
8321 ) {
8322 self.notify_clients(Err(e)).await;
8323 return true;
8324 }
8325
8326 false
8327 }
8328
8329 async fn handle_track_connect_plugin_midi(&mut self, a: Action) -> bool {
8330 let Action::TrackConnectPluginMidi {
8331 ref track_name,
8332 ref from_node,
8333 from_port,
8334 ref to_node,
8335 to_port,
8336 } = a
8337 else {
8338 return false;
8339 };
8340
8341 if self
8342 .reject_if_track_frozen(track_name, "plugin routing changes")
8343 .await
8344 {
8345 return true;
8346 }
8347 let track = match self.track_handle_or_err(track_name) {
8348 Ok(track) => track,
8349 Err(e) => {
8350 self.notify_clients(Err(e)).await;
8351 return true;
8352 }
8353 };
8354 if let Err(e) =
8355 track
8356 .lock()
8357 .connect_plugin_midi(from_node.clone(), from_port, to_node.clone(), to_port)
8358 {
8359 self.notify_clients(Err(e)).await;
8360 return true;
8361 }
8362
8363 false
8364 }
8365
8366 async fn handle_track_disconnect_plugin_audio(&mut self, a: Action) -> bool {
8367 let Action::TrackDisconnectPluginAudio {
8368 ref track_name,
8369 ref from_node,
8370 from_port,
8371 ref to_node,
8372 to_port,
8373 } = a
8374 else {
8375 return false;
8376 };
8377
8378 if self
8379 .reject_if_track_frozen(track_name, "plugin routing changes")
8380 .await
8381 {
8382 return true;
8383 }
8384 let track = match self.track_handle_or_err(track_name) {
8385 Ok(track) => track,
8386 Err(e) => {
8387 self.notify_clients(Err(e)).await;
8388 return true;
8389 }
8390 };
8391 if let Err(e) = track.lock().disconnect_plugin_audio(
8392 from_node.clone(),
8393 from_port,
8394 to_node.clone(),
8395 to_port,
8396 ) {
8397 self.notify_clients(Err(e)).await;
8398 return true;
8399 }
8400
8401 false
8402 }
8403
8404 async fn handle_track_disconnect_plugin_midi(&mut self, a: Action) -> bool {
8405 let Action::TrackDisconnectPluginMidi {
8406 ref track_name,
8407 ref from_node,
8408 from_port,
8409 ref to_node,
8410 to_port,
8411 } = a
8412 else {
8413 return false;
8414 };
8415
8416 if self
8417 .reject_if_track_frozen(track_name, "plugin routing changes")
8418 .await
8419 {
8420 return true;
8421 }
8422 let track = match self.track_handle_or_err(track_name) {
8423 Ok(track) => track,
8424 Err(e) => {
8425 self.notify_clients(Err(e)).await;
8426 return true;
8427 }
8428 };
8429 if let Err(e) = track.lock().disconnect_plugin_midi(
8430 from_node.clone(),
8431 from_port,
8432 to_node.clone(),
8433 to_port,
8434 ) {
8435 self.notify_clients(Err(e)).await;
8436 return true;
8437 }
8438
8439 false
8440 }
8441
8442 async fn handle_track_connect_audio(&mut self, a: Action) -> bool {
8443 let Action::TrackConnectAudio {
8444 ref track_name,
8445 ref from,
8446 from_port,
8447 ref to,
8448 to_port,
8449 } = a
8450 else {
8451 return false;
8452 };
8453
8454 if self
8455 .reject_if_track_frozen(track_name, "routing changes")
8456 .await
8457 {
8458 return true;
8459 }
8460 let track = match self.track_handle_or_err(track_name) {
8461 Ok(track) => track,
8462 Err(e) => {
8463 self.notify_clients(Err(e)).await;
8464 return true;
8465 }
8466 };
8467 if let Err(e) =
8468 track
8469 .lock()
8470 .connect_audio_connectable(from.clone(), from_port, to.clone(), to_port)
8471 {
8472 self.notify_clients(Err(e)).await;
8473 return true;
8474 }
8475
8476 false
8477 }
8478
8479 async fn handle_track_disconnect_audio(&mut self, a: Action) -> bool {
8480 let Action::TrackDisconnectAudio {
8481 ref track_name,
8482 ref from,
8483 from_port,
8484 ref to,
8485 to_port,
8486 } = a
8487 else {
8488 return false;
8489 };
8490
8491 if self
8492 .reject_if_track_frozen(track_name, "routing changes")
8493 .await
8494 {
8495 return true;
8496 }
8497 let track = match self.track_handle_or_err(track_name) {
8498 Ok(track) => track,
8499 Err(e) => {
8500 self.notify_clients(Err(e)).await;
8501 return true;
8502 }
8503 };
8504 if let Err(e) =
8505 track
8506 .lock()
8507 .disconnect_audio_connectable(from.clone(), from_port, to.clone(), to_port)
8508 {
8509 self.notify_clients(Err(e)).await;
8510 return true;
8511 }
8512
8513 false
8514 }
8515
8516 async fn handle_track_connect_midi(&mut self, a: Action) -> bool {
8517 let Action::TrackConnectMidi {
8518 ref track_name,
8519 ref from,
8520 from_port,
8521 ref to,
8522 to_port,
8523 } = a
8524 else {
8525 return false;
8526 };
8527
8528 if self
8529 .reject_if_track_frozen(track_name, "routing changes")
8530 .await
8531 {
8532 return true;
8533 }
8534 let track = match self.track_handle_or_err(track_name) {
8535 Ok(track) => track,
8536 Err(e) => {
8537 self.notify_clients(Err(e)).await;
8538 return true;
8539 }
8540 };
8541 if let Err(e) =
8542 track
8543 .lock()
8544 .connect_midi_connectable(from.clone(), from_port, to.clone(), to_port)
8545 {
8546 self.notify_clients(Err(e)).await;
8547 return true;
8548 }
8549
8550 false
8551 }
8552
8553 async fn handle_track_disconnect_midi(&mut self, a: Action) -> bool {
8554 let Action::TrackDisconnectMidi {
8555 ref track_name,
8556 ref from,
8557 from_port,
8558 ref to,
8559 to_port,
8560 } = a
8561 else {
8562 return false;
8563 };
8564
8565 if self
8566 .reject_if_track_frozen(track_name, "routing changes")
8567 .await
8568 {
8569 return true;
8570 }
8571 let track = match self.track_handle_or_err(track_name) {
8572 Ok(track) => track,
8573 Err(e) => {
8574 self.notify_clients(Err(e)).await;
8575 return true;
8576 }
8577 };
8578 if let Err(e) =
8579 track
8580 .lock()
8581 .disconnect_midi_connectable(from.clone(), from_port, to.clone(), to_port)
8582 {
8583 self.notify_clients(Err(e)).await;
8584 return true;
8585 }
8586
8587 false
8588 }
8589
8590 async fn handle_track_set_plugin_resource_dir(&mut self, a: Action) -> bool {
8591 let Action::TrackSetPluginResourceDir {
8592 ref track_name,
8593 instance_id,
8594 ref format,
8595 ref directory,
8596 } = a
8597 else {
8598 return false;
8599 };
8600
8601 let track = match self.track_handle_or_err(track_name) {
8602 Ok(track) => track,
8603 Err(e) => {
8604 self.notify_clients(Err(e)).await;
8605 return true;
8606 }
8607 };
8608 let dir = std::path::Path::new(directory);
8609 let result = if format.eq_ignore_ascii_case("CLAP") {
8610 track.lock().set_clap_plugin_resource_dir(instance_id, dir)
8611 } else if format.eq_ignore_ascii_case("LV2") {
8612 #[cfg(all(unix, not(target_os = "macos")))]
8613 {
8614 track.lock().set_lv2_plugin_resource_dir(instance_id, dir)
8615 }
8616 #[cfg(not(all(unix, not(target_os = "macos"))))]
8617 Err("LV2 is not supported on this platform".to_string())
8618 } else {
8619 Err(format!(
8620 "Unsupported plugin format for resource dir: {format}"
8621 ))
8622 };
8623 if let Err(e) = result {
8624 self.notify_clients(Err(e)).await;
8625 return true;
8626 }
8627
8628 false
8629 }
8630
8631 async fn handle_clip_set_plugin_resource_dir(&mut self, a: Action) -> bool {
8632 let Action::ClipSetPluginResourceDir {
8633 ref track_name,
8634 clip_idx,
8635 instance_id,
8636 ref format,
8637 ref directory,
8638 } = a
8639 else {
8640 return false;
8641 };
8642
8643 let track = match self.track_handle_or_err(track_name) {
8644 Ok(track) => track,
8645 Err(e) => {
8646 self.notify_clients(Err(e)).await;
8647 return true;
8648 }
8649 };
8650 let dir = std::path::Path::new(directory);
8651 let track = track.lock();
8652 let result = if format.eq_ignore_ascii_case("CLAP") {
8653 track.clip_set_clap_plugin_resource_dir(clip_idx, instance_id, dir)
8654 } else if format.eq_ignore_ascii_case("LV2") {
8655 #[cfg(all(unix, not(target_os = "macos")))]
8656 {
8657 track.clip_set_lv2_plugin_resource_dir(clip_idx, instance_id, dir)
8658 }
8659 #[cfg(not(all(unix, not(target_os = "macos"))))]
8660 Err("LV2 is not supported on this platform".to_string())
8661 } else {
8662 Err(format!(
8663 "Unsupported plugin format for resource dir: {format}"
8664 ))
8665 };
8666 if let Err(e) = result {
8667 self.notify_clients(Err(e)).await;
8668 return true;
8669 }
8670
8671 false
8672 }
8673
8674 async fn handle_clip_clap_file_references(&mut self, a: Action) -> bool {
8675 let Action::ClipClapFileReferences {
8676 ref track_name,
8677 clip_idx,
8678 instance_id,
8679 refs: _,
8680 } = a
8681 else {
8682 return false;
8683 };
8684 match self.track_handle_or_err(track_name) {
8685 Ok(track) => {
8686 let track = track.lock();
8687 let refs = track
8688 .clip_clap_file_references(clip_idx, instance_id)
8689 .unwrap_or_else(|e| {
8690 tracing::warn!(
8691 track_name = %track_name,
8692 clip_idx,
8693 instance_id,
8694 error = %e,
8695 "Failed to enumerate clip CLAP file references"
8696 );
8697 Vec::new()
8698 });
8699 self.notify_clients(Ok(Action::ClipClapFileReferences {
8700 track_name: track_name.clone(),
8701 clip_idx,
8702 instance_id,
8703 refs,
8704 }))
8705 .await;
8706 }
8707 Err(e) => {
8708 self.notify_clients(Err(e)).await;
8709 }
8710 };
8711 false
8712 }
8713
8714 async fn handle_track_set_clap_parameter(&mut self, a: Action) -> bool {
8715 let Action::TrackSetClapParameter {
8716 ref track_name,
8717 instance_id,
8718 param_id,
8719 value,
8720 } = a
8721 else {
8722 return false;
8723 };
8724
8725 if self
8726 .reject_if_track_frozen(track_name, "CLAP parameter changes")
8727 .await
8728 {
8729 return true;
8730 }
8731 match self.track_handle_or_err(track_name) {
8732 Ok(track) => {
8733 if let Err(e) = track
8734 .lock()
8735 .set_clap_parameter(instance_id, param_id, value)
8736 {
8737 self.notify_clients(Err(e)).await;
8738 return true;
8739 }
8740 self.notify_clients(Ok(a.clone())).await;
8741 }
8742 Err(e) => {
8743 self.notify_clients(Err(e)).await;
8744 }
8745 }
8746
8747 false
8748 }
8749
8750 async fn handle_clip_set_clap_parameter(&mut self, a: Action) -> bool {
8751 let Action::ClipSetClapParameter {
8752 ref track_name,
8753 clip_idx,
8754 instance_id,
8755 param_id,
8756 value,
8757 } = a
8758 else {
8759 return false;
8760 };
8761
8762 if self
8763 .reject_if_track_frozen(track_name, "CLAP parameter changes")
8764 .await
8765 {
8766 return true;
8767 }
8768 match self.track_handle_or_err(track_name) {
8769 Ok(track) => {
8770 if let Err(e) =
8771 track
8772 .lock()
8773 .clip_set_clap_parameter(clip_idx, instance_id, param_id, value)
8774 {
8775 self.notify_clients(Err(e)).await;
8776 return true;
8777 }
8778 self.notify_clients(Ok(a.clone())).await;
8779 }
8780 Err(e) => {
8781 self.notify_clients(Err(e)).await;
8782 }
8783 }
8784
8785 false
8786 }
8787
8788 async fn handle_track_set_clap_parameter_at(&mut self, a: Action) -> bool {
8789 let Action::TrackSetClapParameterAt {
8790 ref track_name,
8791 instance_id,
8792 param_id,
8793 value,
8794 frame,
8795 } = a
8796 else {
8797 return false;
8798 };
8799
8800 if self
8801 .reject_if_track_frozen(track_name, "CLAP parameter changes")
8802 .await
8803 {
8804 return true;
8805 }
8806 match self.track_handle_or_err(track_name) {
8807 Ok(track) => {
8808 if let Err(e) =
8809 track
8810 .lock()
8811 .set_clap_parameter_at(instance_id, param_id, value, frame)
8812 {
8813 self.notify_clients(Err(e)).await;
8814 return true;
8815 }
8816 self.notify_clients(Ok(a.clone())).await;
8817 }
8818 Err(e) => {
8819 self.notify_clients(Err(e)).await;
8820 }
8821 }
8822
8823 false
8824 }
8825
8826 async fn handle_track_begin_clap_parameter_edit(&mut self, a: Action) -> bool {
8827 let Action::TrackBeginClapParameterEdit {
8828 ref track_name,
8829 instance_id,
8830 param_id,
8831 frame,
8832 } = a
8833 else {
8834 return false;
8835 };
8836
8837 if self
8838 .reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
8839 .await
8840 {
8841 return true;
8842 }
8843 match self.track_handle_or_err(track_name) {
8844 Ok(track) => {
8845 if let Err(e) = track
8846 .lock()
8847 .begin_clap_parameter_edit(instance_id, param_id, frame)
8848 {
8849 self.notify_clients(Err(e)).await;
8850 return true;
8851 }
8852 self.notify_clients(Ok(a.clone())).await;
8853 }
8854 Err(e) => {
8855 self.notify_clients(Err(e)).await;
8856 }
8857 }
8858
8859 false
8860 }
8861
8862 async fn handle_track_end_clap_parameter_edit(&mut self, a: Action) -> bool {
8863 let Action::TrackEndClapParameterEdit {
8864 ref track_name,
8865 instance_id,
8866 param_id,
8867 frame,
8868 } = a
8869 else {
8870 return false;
8871 };
8872
8873 if self
8874 .reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
8875 .await
8876 {
8877 return true;
8878 }
8879 match self.track_handle_or_err(track_name) {
8880 Ok(track) => {
8881 if let Err(e) = track
8882 .lock()
8883 .end_clap_parameter_edit(instance_id, param_id, frame)
8884 {
8885 self.notify_clients(Err(e)).await;
8886 return true;
8887 }
8888 self.notify_clients(Ok(a.clone())).await;
8889 }
8890 Err(e) => {
8891 self.notify_clients(Err(e)).await;
8892 }
8893 }
8894
8895 false
8896 }
8897
8898 async fn handle_track_clap_snapshot_state(&mut self, a: Action) -> bool {
8899 let Action::TrackClapSnapshotState {
8900 ref track_name,
8901 instance_id,
8902 } = a
8903 else {
8904 return false;
8905 };
8906 match self.track_handle_or_err(track_name) {
8907 Ok(track) => {
8908 let plugin_id = track
8909 .lock()
8910 .clap_plugins
8911 .iter()
8912 .find(|instance| instance.id == instance_id)
8913 .map(|instance| instance.processor.lock().plugin_id().to_string())
8914 .unwrap_or_default();
8915 match track.lock().clap_snapshot_state(instance_id) {
8916 Ok(state) => {
8917 self.notify_clients(Ok(Action::TrackClapStateSnapshot {
8918 track_name: track_name.clone(),
8919 instance_id,
8920 plugin_id,
8921 state,
8922 }))
8923 .await;
8924 }
8925 Err(e) => {
8926 self.notify_clients(Err(e)).await;
8927 }
8928 }
8929 }
8930 Err(e) => {
8931 self.notify_clients(Err(e)).await;
8932 }
8933 };
8934 false
8935 }
8936
8937 async fn handle_track_clap_restore_state(&mut self, a: Action) -> bool {
8938 let Action::TrackClapRestoreState {
8939 ref track_name,
8940 instance_id,
8941 ref state,
8942 } = a
8943 else {
8944 return false;
8945 };
8946
8947 if self
8948 .reject_if_track_frozen(track_name, "CLAP state restore")
8949 .await
8950 {
8951 return true;
8952 }
8953 let track = match self.track_handle_or_err(track_name) {
8954 Ok(track) => track,
8955 Err(e) => {
8956 self.notify_clients(Err(e)).await;
8957 return true;
8958 }
8959 };
8960 let track = track.lock();
8961 if track.audio.processing {
8962 self.notify_clients(Err(format!(
8963 "Track '{}' is currently processing audio; stop playback before restoring CLAP state",
8964 track_name
8965 )))
8966 .await;
8967 return true;
8968 }
8969 if let Err(e) = track.clap_restore_state(instance_id, state) {
8970 self.notify_clients(Err(e)).await;
8971 return true;
8972 }
8973
8974 false
8975 }
8976
8977 async fn handle_clip_clap_restore_state(&mut self, a: Action) -> bool {
8978 let Action::ClipClapRestoreState {
8979 ref track_name,
8980 clip_idx,
8981 instance_id,
8982 ref state,
8983 } = a
8984 else {
8985 return false;
8986 };
8987
8988 if self
8989 .reject_if_track_frozen(track_name, "CLAP state restore")
8990 .await
8991 {
8992 return true;
8993 }
8994 let track = match self.track_handle_or_err(track_name) {
8995 Ok(track) => track,
8996 Err(e) => {
8997 self.notify_clients(Err(e)).await;
8998 return true;
8999 }
9000 };
9001 let track = track.lock();
9002 if track.audio.processing {
9003 self.notify_clients(Err(format!(
9004 "Track '{}' is currently processing audio; stop playback before restoring CLAP state",
9005 track_name
9006 )))
9007 .await;
9008 return true;
9009 }
9010 if let Err(e) = track.clip_clap_restore_state(clip_idx, instance_id, state) {
9011 self.notify_clients(Err(e)).await;
9012 return true;
9013 }
9014
9015 false
9016 }
9017
9018 async fn handle_track_snapshot_all_clap_states(&mut self, a: Action) -> bool {
9019 let Action::TrackSnapshotAllClapStates { ref track_name } = a else {
9020 return false;
9021 };
9022
9023 let track = match self.track_handle_or_err(track_name) {
9024 Ok(track) => track,
9025 Err(e) => {
9026 self.notify_clients(Err(e)).await;
9027 return true;
9028 }
9029 };
9030 let instances: Vec<_> = {
9031 let locked = track.lock();
9032 locked
9033 .clap_plugins
9034 .iter()
9035 .map(|i| (i.id, i.processor.lock().plugin_id().to_string()))
9036 .collect()
9037 };
9038 for (instance_id, plugin_id) in instances {
9039 match track.lock().clap_snapshot_state(instance_id) {
9040 Ok(state) => {
9041 self.notify_clients(Ok(Action::TrackClapStateSnapshot {
9042 track_name: track_name.clone(),
9043 instance_id,
9044 plugin_id,
9045 state,
9046 }))
9047 .await;
9048 }
9049 Err(_e) => {}
9050 }
9051 }
9052 self.notify_clients(Ok(Action::TrackSnapshotAllClapStatesDone {
9053 track_name: track_name.clone(),
9054 }))
9055 .await;
9056
9057 false
9058 }
9059
9060 async fn handle_track_set_vst3_parameter(&mut self, a: Action) -> bool {
9061 let Action::TrackSetVst3Parameter {
9062 ref track_name,
9063 instance_id,
9064 param_id,
9065 value,
9066 } = a
9067 else {
9068 return false;
9069 };
9070
9071 if self
9072 .reject_if_track_frozen(track_name, "VST3 parameter changes")
9073 .await
9074 {
9075 return true;
9076 }
9077 match self.track_handle_or_err(track_name) {
9078 Ok(track) => {
9079 if let Err(e) = track
9080 .lock()
9081 .set_vst3_parameter(instance_id, param_id, value)
9082 {
9083 self.notify_clients(Err(e)).await;
9084 return true;
9085 }
9086 self.notify_clients(Ok(a.clone())).await;
9087 }
9088 Err(e) => {
9089 self.notify_clients(Err(e)).await;
9090 }
9091 }
9092
9093 false
9094 }
9095
9096 async fn handle_track_set_plugin_bypassed(&mut self, a: Action) -> bool {
9097 let Action::TrackSetPluginBypassed {
9098 ref track_name,
9099 instance_id,
9100 ref format,
9101 bypassed,
9102 } = a
9103 else {
9104 return false;
9105 };
9106 match self.track_handle_or_err(track_name) {
9107 Ok(track) => {
9108 let result = match format.as_str() {
9109 "CLAP" => track.lock().set_clap_plugin_bypassed(instance_id, bypassed),
9110 "VST3" => track.lock().set_vst3_plugin_bypassed(instance_id, bypassed),
9111 #[cfg(all(unix, not(target_os = "macos")))]
9112 "LV2" => track.lock().set_lv2_plugin_bypassed(instance_id, bypassed),
9113 _ => Err(format!("Unknown plugin format for bypass: {format}")),
9114 };
9115 if let Err(e) = result {
9116 self.notify_clients(Err(e)).await;
9117 return true;
9118 }
9119 self.notify_clients(Ok(a.clone())).await;
9120 }
9121 Err(e) => {
9122 self.notify_clients(Err(e)).await;
9123 }
9124 };
9125 false
9126 }
9127
9128 async fn handle_track_connect_vst3_audio(&mut self, a: Action) -> bool {
9129 let Action::TrackConnectVst3Audio {
9130 ref track_name,
9131 ref from_node,
9132 from_port,
9133 ref to_node,
9134 to_port,
9135 } = a
9136 else {
9137 return false;
9138 };
9139
9140 if self
9141 .reject_if_track_frozen(track_name, "VST3 routing changes")
9142 .await
9143 {
9144 return true;
9145 }
9146 match self.track_handle_or_err(track_name) {
9147 Ok(track) => {
9148 if let Err(e) = track
9149 .lock()
9150 .connect_vst3_audio(from_node, from_port, to_node, to_port)
9151 {
9152 self.notify_clients(Err(e)).await;
9153 return true;
9154 }
9155 self.notify_clients(Ok(a.clone())).await;
9156 }
9157 Err(e) => {
9158 self.notify_clients(Err(e)).await;
9159 }
9160 }
9161
9162 false
9163 }
9164
9165 async fn handle_track_disconnect_vst3_audio(&mut self, a: Action) -> bool {
9166 let Action::TrackDisconnectVst3Audio {
9167 ref track_name,
9168 ref from_node,
9169 from_port,
9170 ref to_node,
9171 to_port,
9172 } = a
9173 else {
9174 return false;
9175 };
9176
9177 if self
9178 .reject_if_track_frozen(track_name, "VST3 routing changes")
9179 .await
9180 {
9181 return true;
9182 }
9183 match self.track_handle_or_err(track_name) {
9184 Ok(track) => {
9185 if let Err(e) = track
9186 .lock()
9187 .disconnect_vst3_audio(from_node, from_port, to_node, to_port)
9188 {
9189 self.notify_clients(Err(e)).await;
9190 return true;
9191 }
9192 self.notify_clients(Ok(a.clone())).await;
9193 }
9194 Err(e) => {
9195 self.notify_clients(Err(e)).await;
9196 }
9197 }
9198
9199 false
9200 }
9201
9202 async fn handle_set_clip_pitch_correction(&mut self, a: Action) -> bool {
9203 let Action::SetClipPitchCorrection {
9204 ref track_name,
9205 clip_index,
9206 ref preview_name,
9207 ref source_name,
9208 source_offset,
9209 source_length,
9210 ref pitch_correction_points,
9211 pitch_correction_frame_likeness,
9212 pitch_correction_inertia_ms,
9213 pitch_correction_formant_compensation,
9214 } = a
9215 else {
9216 return false;
9217 };
9218
9219 self.set_clip_pitch_correction(
9220 track_name,
9221 clip_index,
9222 preview_name.clone(),
9223 source_name.clone(),
9224 source_offset,
9225 source_length,
9226 pitch_correction_points.clone(),
9227 pitch_correction_frame_likeness,
9228 pitch_correction_inertia_ms,
9229 pitch_correction_formant_compensation,
9230 );
9231
9232 false
9233 }
9234
9235 async fn handle_set_loop_range(&mut self, a: Action) -> bool {
9236 let Action::SetLoopRange(range) = a else {
9237 return false;
9238 };
9239
9240 self.loop_range_samples = range.and_then(|(start, end)| {
9241 if end > start {
9242 Some((start, end))
9243 } else {
9244 None
9245 }
9246 });
9247 self.loop_enabled = self.loop_range_samples.is_some();
9248 self.notified_loop_wrap_sample = None;
9249 if self.loop_enabled
9250 && let Some((loop_start, loop_end)) = self.loop_range_samples
9251 && self.transport_sample >= loop_end
9252 {
9253 self.transport_sample = loop_start;
9254 self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
9255 .await;
9256 }
9257
9258 false
9259 }
9260
9261 async fn handle_track_automation_toggle_lane(&mut self, a: Action) -> bool {
9262 let Action::TrackAutomationToggleLane {
9263 ref track_name,
9264 ref target,
9265 } = a
9266 else {
9267 return false;
9268 };
9269
9270 if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
9271 let track = track.lock();
9272 let mut lanes = Self::parse_automation_lanes(&track.automation_lanes);
9273 if let Some(lane) = lanes.iter_mut().find(|lane| lane.target == *target) {
9274 lane.visible = !lane.visible;
9275 } else {
9276 lanes.push(OfflineAutomationLane {
9277 target: target.clone(),
9278 visible: true,
9279 points: vec![],
9280 });
9281 }
9282 track.automation_lanes = serde_json::to_value(&lanes).unwrap_or_default();
9283 }
9284
9285 false
9286 }
9287
9288 async fn handle_track_remove_audio_output(&mut self, a: Action) -> bool {
9289 let Action::TrackRemoveAudioOutput(ref name) = a else {
9290 return false;
9291 };
9292
9293 let track = match self.track_handle_or_err(name) {
9294 Ok(track) => track,
9295 Err(e) => {
9296 self.notify_clients(Err(e)).await;
9297 return true;
9298 }
9299 };
9300 let (hw_outputs, track_inputs) = {
9301 let state = self.state.lock();
9302 let hw_outputs = self.all_hw_output_audio_ports();
9303 let track_inputs = state
9304 .tracks
9305 .iter()
9306 .filter(|(track_name, _)| *track_name != name)
9307 .flat_map(|(_, handle)| handle.lock().audio.ins.clone())
9308 .collect::<Vec<_>>();
9309 (hw_outputs, track_inputs)
9310 };
9311 if let Err(e) = track.lock().remove_audio_output(&hw_outputs, &track_inputs) {
9312 self.notify_clients(Err(e)).await;
9313 return true;
9314 }
9315
9316 false
9317 }
9318
9319 async fn handle_track_toggle_folder(&mut self, a: Action) -> bool {
9320 let Action::TrackToggleFolder { ref track_name } = a else {
9321 return false;
9322 };
9323
9324 let track = match self.track_handle_or_err(track_name) {
9325 Ok(track) => track,
9326 Err(e) => {
9327 self.notify_clients(Err(e)).await;
9328 return true;
9329 }
9330 };
9331 {
9332 let t = track.lock();
9333 t.folder_open = !t.folder_open;
9334 }
9335 self.notify_clients(Ok(Action::TrackToggleFolder {
9336 track_name: track_name.clone(),
9337 }))
9338 .await;
9339
9340 self.notify_clients(Ok(Action::TrackSetFolder {
9341 track_name: track_name.clone(),
9342 is_folder: track.lock().is_folder,
9343 }))
9344 .await;
9345
9346 false
9347 }
9348
9349 async fn handle_track_clear_plugins(&mut self, a: Action) -> bool {
9350 let Action::TrackClearPlugins { ref track_name } = a else {
9351 return false;
9352 };
9353
9354 if self
9355 .reject_if_track_frozen(track_name, "plugin graph editing")
9356 .await
9357 {
9358 return true;
9359 }
9360 let track = match self.track_handle_or_err(track_name) {
9361 Ok(track) => track,
9362 Err(e) => {
9363 self.notify_clients(Err(e)).await;
9364 return true;
9365 }
9366 };
9367 track.lock().clear_plugins();
9368 self.notify_clients(Ok(Action::Log {
9369 source: "engine".to_string(),
9370 message: format!("Cleared plugins from track '{track_name}'"),
9371 }))
9372 .await;
9373
9374 false
9375 }
9376
9377 async fn handle_track_clap_file_references(&mut self, a: Action) -> bool {
9378 let Action::TrackClapFileReferences {
9379 ref track_name,
9380 instance_id,
9381 refs: _,
9382 } = a
9383 else {
9384 return false;
9385 };
9386 match self.track_handle_or_err(track_name) {
9387 Ok(track) => {
9388 let refs = track.lock().clap_file_references(instance_id).unwrap_or_else(|e| {
9389 tracing::warn!(track_name = %track_name, instance_id, error = %e, "Failed to enumerate CLAP file references");
9390 Vec::new()
9391 });
9392 self.notify_clients(Ok(Action::TrackClapFileReferences {
9393 track_name: track_name.clone(),
9394 instance_id,
9395 refs,
9396 }))
9397 .await;
9398 }
9399 Err(e) => {
9400 self.notify_clients(Err(e)).await;
9401 }
9402 };
9403 false
9404 }
9405
9406 async fn handle_track_update_clap_file_reference(&mut self, a: Action) -> bool {
9407 let Action::TrackUpdateClapFileReference {
9408 ref track_name,
9409 instance_id,
9410 index,
9411 ref path,
9412 } = a
9413 else {
9414 return false;
9415 };
9416
9417 let track = match self.track_handle_or_err(track_name) {
9418 Ok(track) => track,
9419 Err(e) => {
9420 self.notify_clients(Err(e)).await;
9421 return true;
9422 }
9423 };
9424 if let Err(e) = track
9425 .lock()
9426 .update_clap_file_reference(instance_id, index, path)
9427 {
9428 self.notify_clients(Err(e)).await;
9429 return true;
9430 }
9431
9432 false
9433 }
9434
9435 async fn handle_clip_update_clap_file_reference(&mut self, a: Action) -> bool {
9436 let Action::ClipUpdateClapFileReference {
9437 ref track_name,
9438 clip_idx,
9439 instance_id,
9440 index,
9441 ref path,
9442 } = a
9443 else {
9444 return false;
9445 };
9446
9447 let track = match self.track_handle_or_err(track_name) {
9448 Ok(track) => track,
9449 Err(e) => {
9450 self.notify_clients(Err(e)).await;
9451 return true;
9452 }
9453 };
9454 if let Err(e) =
9455 track
9456 .lock()
9457 .clip_update_clap_file_reference(clip_idx, instance_id, index, path)
9458 {
9459 self.notify_clients(Err(e)).await;
9460 return true;
9461 }
9462
9463 false
9464 }
9465
9466 async fn handle_track_get_clap_parameters(&mut self, a: Action) -> bool {
9467 let Action::TrackGetClapParameters {
9468 ref track_name,
9469 instance_id,
9470 } = a
9471 else {
9472 return false;
9473 };
9474 match self.track_handle_or_err(track_name) {
9475 Ok(track) => match track.lock().get_clap_parameters(instance_id) {
9476 Ok(parameters) => {
9477 self.notify_clients(Ok(Action::TrackClapParameters {
9478 track_name: track_name.clone(),
9479 instance_id,
9480 parameters,
9481 }))
9482 .await;
9483 }
9484 Err(e) => {
9485 self.notify_clients(Err(e)).await;
9486 }
9487 },
9488 Err(e) => {
9489 self.notify_clients(Err(e)).await;
9490 }
9491 };
9492 false
9493 }
9494
9495 async fn handle_clip_clap_snapshot_state(&mut self, a: Action) -> bool {
9496 let Action::ClipClapSnapshotState {
9497 ref track_name,
9498 clip_idx,
9499 instance_id,
9500 } = a
9501 else {
9502 return false;
9503 };
9504 match self.track_handle_or_err(track_name) {
9505 Ok(track) => match track.lock().clip_clap_snapshot_state(clip_idx, instance_id) {
9506 Ok((plugin_id, state)) => {
9507 self.notify_clients(Ok(Action::ClipClapStateSnapshot {
9508 track_name: track_name.clone(),
9509 clip_idx,
9510 instance_id,
9511 plugin_id,
9512 state,
9513 }))
9514 .await;
9515 }
9516 Err(e) => {
9517 self.notify_clients(Err(e)).await;
9518 }
9519 },
9520 Err(e) => {
9521 self.notify_clients(Err(e)).await;
9522 }
9523 };
9524 false
9525 }
9526
9527 async fn handle_track_get_vst3_graph(&mut self, a: Action) -> bool {
9528 let Action::TrackGetVst3Graph { ref track_name } = a else {
9529 return false;
9530 };
9531
9532 match self.track_handle_or_err(track_name) {
9533 Ok(track) => {
9534 let t = track.lock();
9535 let plugins = t.vst3_graph_plugins();
9536 let connections = t.vst3_graph_connections();
9537 self.notify_clients(Ok(Action::TrackVst3Graph {
9538 track_name: track_name.clone(),
9539 plugins,
9540 connections,
9541 }))
9542 .await;
9543 }
9544 Err(e) => {
9545 self.notify_clients(Err(e)).await;
9546 }
9547 }
9548
9549 false
9550 }
9551
9552 async fn handle_track_get_vst3_parameters(&mut self, a: Action) -> bool {
9553 let Action::TrackGetVst3Parameters {
9554 ref track_name,
9555 instance_id,
9556 } = a
9557 else {
9558 return false;
9559 };
9560 match self.track_handle_or_err(track_name) {
9561 Ok(track) => match track.lock().get_vst3_parameters(instance_id) {
9562 Ok(parameters) => {
9563 self.notify_clients(Ok(Action::TrackVst3Parameters {
9564 track_name: track_name.clone(),
9565 instance_id,
9566 parameters,
9567 }))
9568 .await;
9569 }
9570 Err(e) => {
9571 self.notify_clients(Err(e)).await;
9572 }
9573 },
9574 Err(e) => {
9575 self.notify_clients(Err(e)).await;
9576 }
9577 };
9578 false
9579 }
9580
9581 #[cfg(all(unix, not(target_os = "macos")))]
9582 async fn handle_track_get_lv2_plugin_controls(&mut self, a: Action) -> bool {
9583 let Action::TrackGetLv2PluginControls {
9584 ref track_name,
9585 instance_id,
9586 } = a
9587 else {
9588 return false;
9589 };
9590 match self.track_handle_or_err(track_name) {
9591 Ok(track) => match track.lock().get_lv2_plugin_controls(instance_id) {
9592 Ok(controls) => {
9593 self.notify_clients(Ok(Action::TrackLv2PluginControls {
9594 track_name: track_name.clone(),
9595 instance_id,
9596 controls,
9597 instance_access_handle: None,
9598 }))
9599 .await;
9600 }
9601 Err(e) => {
9602 self.notify_clients(Err(e)).await;
9603 }
9604 },
9605 Err(e) => {
9606 self.notify_clients(Err(e)).await;
9607 }
9608 };
9609 false
9610 }
9611
9612 #[cfg(all(unix, not(target_os = "macos")))]
9613 async fn handle_track_lv2_snapshot_state(&mut self, a: Action) -> bool {
9614 let Action::TrackLv2SnapshotState {
9615 ref track_name,
9616 instance_id,
9617 } = a
9618 else {
9619 return false;
9620 };
9621 match self.track_handle_or_err(track_name) {
9622 Ok(track) => match track.lock().lv2_snapshot_state(instance_id) {
9623 Ok(state) => {
9624 self.notify_clients(Ok(Action::TrackLv2StateSnapshot {
9625 track_name: track_name.clone(),
9626 instance_id,
9627 state,
9628 }))
9629 .await;
9630 }
9631 Err(e) => {
9632 self.notify_clients(Err(e)).await;
9633 }
9634 },
9635 Err(e) => {
9636 self.notify_clients(Err(e)).await;
9637 }
9638 };
9639 false
9640 }
9641
9642 #[cfg(all(unix, not(target_os = "macos")))]
9643 async fn handle_clip_lv2_snapshot_state(&mut self, a: Action) -> bool {
9644 let Action::ClipLv2SnapshotState {
9645 ref track_name,
9646 clip_idx,
9647 instance_id,
9648 } = a
9649 else {
9650 return false;
9651 };
9652 match self.track_handle_or_err(track_name) {
9653 Ok(track) => match track.lock().clip_lv2_snapshot_state(clip_idx, instance_id) {
9654 Ok(state) => {
9655 self.notify_clients(Ok(Action::ClipLv2StateSnapshot {
9656 track_name: track_name.clone(),
9657 clip_idx,
9658 instance_id,
9659 state,
9660 }))
9661 .await;
9662 }
9663 Err(e) => {
9664 self.notify_clients(Err(e)).await;
9665 }
9666 },
9667 Err(e) => {
9668 self.notify_clients(Err(e)).await;
9669 }
9670 };
9671 false
9672 }
9673
9674 async fn handle_track_vst3_snapshot_state(&mut self, a: Action) -> bool {
9675 let Action::TrackVst3SnapshotState {
9676 ref track_name,
9677 instance_id,
9678 } = a
9679 else {
9680 return false;
9681 };
9682 match self.track_handle_or_err(track_name) {
9683 Ok(track) => match track.lock().vst3_snapshot_state(instance_id) {
9684 Ok(state) => {
9685 self.notify_clients(Ok(Action::TrackVst3StateSnapshot {
9686 track_name: track_name.clone(),
9687 instance_id,
9688 state,
9689 }))
9690 .await;
9691 }
9692 Err(e) => {
9693 self.notify_clients(Err(e)).await;
9694 }
9695 },
9696 Err(e) => {
9697 self.notify_clients(Err(e)).await;
9698 }
9699 };
9700 false
9701 }
9702
9703 async fn handle_clip_vst3_snapshot_state(&mut self, a: Action) -> bool {
9704 let Action::ClipVst3SnapshotState {
9705 ref track_name,
9706 clip_idx,
9707 instance_id,
9708 } = a
9709 else {
9710 return false;
9711 };
9712 match self.track_handle_or_err(track_name) {
9713 Ok(track) => match track.lock().clip_vst3_snapshot_state(clip_idx, instance_id) {
9714 Ok(state) => {
9715 self.notify_clients(Ok(Action::ClipVst3StateSnapshot {
9716 track_name: track_name.clone(),
9717 clip_idx,
9718 instance_id,
9719 state,
9720 }))
9721 .await;
9722 }
9723 Err(e) => {
9724 self.notify_clients(Err(e)).await;
9725 }
9726 },
9727 Err(e) => {
9728 self.notify_clients(Err(e)).await;
9729 }
9730 };
9731 false
9732 }
9733
9734 async fn handle_add_grouped_clip(&mut self, a: Action) -> bool {
9735 let Action::AddGroupedClip {
9736 ref track_name,
9737 kind,
9738 ref audio_clip,
9739 ref midi_clip,
9740 } = a
9741 else {
9742 return false;
9743 };
9744
9745 self.add_grouped_clip_to_track(track_name, kind, audio_clip.clone(), midi_clip.clone());
9746 if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
9747 let track_name = track_name.clone();
9748 tokio::task::spawn_blocking(move || {
9749 track.lock().preload_clips();
9750 tracing::debug!(
9751 "Preloaded clips for track '{}' after AddGroupedClip",
9752 track_name
9753 );
9754 });
9755 }
9756
9757 false
9758 }
9759
9760 async fn handle_clear_all_midi_learn_bindings(&mut self, a: Action) -> bool {
9761 let Action::ClearAllMidiLearnBindings = a else {
9762 return false;
9763 };
9764
9765 self.pending_midi_learn = None;
9766 self.pending_global_midi_learn = None;
9767 self.pending_session_midi_learn = None;
9768 self.global_midi_learn_play_pause = None;
9769 self.global_midi_learn_stop = None;
9770 self.global_midi_learn_record_toggle = None;
9771 self.session_midi_learn_slots.clear();
9772 self.session_midi_learn_scenes.clear();
9773 self.session_midi_learn_stop_track.clear();
9774 self.session_midi_learn_stop_all = None;
9775 self.midi_cc_gate.clear();
9776 for track in self.state.lock().tracks.values() {
9777 let t = track.lock();
9778 t.midi_learn_volume = None;
9779 t.midi_learn_balance = None;
9780 t.midi_learn_mute = None;
9781 t.midi_learn_solo = None;
9782 t.midi_learn_arm = None;
9783 t.midi_learn_input_monitor = None;
9784 t.midi_learn_disk_monitor = None;
9785 }
9786
9787 false
9788 }
9789
9790 async fn handle_panic(&mut self, a: Action) -> bool {
9791 let Action::Panic = a else {
9792 return false;
9793 };
9794
9795 let panic_events = self.panic_events_for_all_hw_midi_outputs();
9796 if let Some(worker) = &self.hw_worker {
9797 if !panic_events.is_empty() {
9798 if let Err(e) = worker.tx.send(Message::ClearHWMidiOutEvents).await {
9799 error!("Error clearing HW MIDI queue for panic {e}");
9800 }
9801 self.midi_hub
9802 .lock()
9803 .write_events_blocking(&panic_events, Duration::from_millis(250));
9804 }
9805 } else if !panic_events.is_empty() {
9806 self.pending_hw_midi_out_events_by_device
9807 .extend(panic_events);
9808 }
9809
9810 false
9811 }
9812
9813 async fn handle_set_record_enabled(&mut self, a: Action) -> bool {
9814 let Action::SetRecordEnabled(enabled) = a else {
9815 return false;
9816 };
9817
9818 self.record_enabled = enabled;
9819 if !enabled {
9820 if self.awaiting_hwfinished {
9821 self.append_recorded_cycle();
9822 }
9823 self.flush_recordings().await;
9824 } else if self.session_dir.is_none() {
9825 self.notify_clients(Err(
9826 "Recording enabled but session path is not set".to_string()
9827 ))
9828 .await;
9829 }
9830
9831 false
9832 }
9833
9834 async fn handle_track_automation_delete_point(&mut self, a: Action) -> bool {
9835 let Action::TrackAutomationDeletePoint {
9836 ref track_name,
9837 ref target,
9838 sample,
9839 } = a
9840 else {
9841 return false;
9842 };
9843
9844 if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
9845 let track = track.lock();
9846 let mut lanes = Self::parse_automation_lanes(&track.automation_lanes);
9847 if let Some(lane) = lanes.iter_mut().find(|lane| lane.target == *target) {
9848 lane.points.retain(|point| point.sample != sample);
9849 }
9850 track.automation_lanes = serde_json::to_value(&lanes).unwrap_or_default();
9851 }
9852
9853 false
9854 }
9855
9856 async fn handle_track_add_audio_input(&mut self, a: Action) -> bool {
9857 let Action::TrackAddAudioInput(ref name) = a else {
9858 return false;
9859 };
9860
9861 let track = match self.track_handle_or_err(name) {
9862 Ok(track) => track,
9863 Err(e) => {
9864 self.notify_clients(Err(e)).await;
9865 return true;
9866 }
9867 };
9868 if let Err(e) = track.lock().add_audio_input() {
9869 self.notify_clients(Err(e)).await;
9870 return true;
9871 }
9872
9873 false
9874 }
9875
9876 async fn handle_track_add_audio_output(&mut self, a: Action) -> bool {
9877 let Action::TrackAddAudioOutput(ref name) = a else {
9878 return false;
9879 };
9880
9881 let track = match self.track_handle_or_err(name) {
9882 Ok(track) => track,
9883 Err(e) => {
9884 self.notify_clients(Err(e)).await;
9885 return true;
9886 }
9887 };
9888 if let Err(e) = track.lock().add_audio_output() {
9889 self.notify_clients(Err(e)).await;
9890 return true;
9891 }
9892
9893 false
9894 }
9895
9896 async fn handle_track_remove_audio_input(&mut self, a: Action) -> bool {
9897 let Action::TrackRemoveAudioInput(ref name) = a else {
9898 return false;
9899 };
9900
9901 let track = match self.track_handle_or_err(name) {
9902 Ok(track) => track,
9903 Err(e) => {
9904 self.notify_clients(Err(e)).await;
9905 return true;
9906 }
9907 };
9908 if let Err(e) = track.lock().remove_audio_input() {
9909 self.notify_clients(Err(e)).await;
9910 return true;
9911 }
9912
9913 false
9914 }
9915
9916 async fn handle_track_midi_cc(&mut self, a: Action) -> bool {
9917 let Action::TrackMidiCc {
9918 ref track_name,
9919 channel,
9920 cc,
9921 value,
9922 } = a
9923 else {
9924 return false;
9925 };
9926
9927 if let Some(track) = self.state.lock().tracks.get(track_name) {
9928 track
9929 .lock()
9930 .pending_automation_midi_events
9931 .push(MidiEvent::new(
9932 0,
9933 vec![0xB0 | channel.min(15), cc.min(127), value.min(127)],
9934 ));
9935 }
9936
9937 false
9938 }
9939
9940 async fn handle_track_toggle_arm(&mut self, a: Action) -> bool {
9941 let Action::TrackToggleArm(ref name) = a else {
9942 return false;
9943 };
9944
9945 if self.reject_if_track_frozen(name, "arming/disarming").await {
9946 return true;
9947 }
9948 if let Some(track) = self.state.lock().tracks.get(name).cloned() {
9949 track.lock().arm();
9950 let armed = track.lock().armed;
9951 if !armed && self.audio_recordings.contains_key(name) {
9952 self.flush_track_recording(name).await;
9953 }
9954 } else {
9955 tracing::warn!(
9956 "TrackToggleArm for '{}' but track not found in engine",
9957 name
9958 );
9959 }
9960
9961 false
9962 }
9963
9964 async fn handle_track_set_midi_lane_channel(&mut self, a: Action) -> bool {
9965 let Action::TrackSetMidiLaneChannel {
9966 ref track_name,
9967 lane,
9968 channel,
9969 } = a
9970 else {
9971 return false;
9972 };
9973
9974 let track = match self.track_handle_or_err(track_name) {
9975 Ok(track) => track,
9976 Err(e) => {
9977 self.notify_clients(Err(e)).await;
9978 return true;
9979 }
9980 };
9981 track.lock().set_midi_lane_channel(lane, channel);
9982
9983 false
9984 }
9985
9986 async fn handle_track_set_frozen(&mut self, a: Action) -> bool {
9987 let Action::TrackSetFrozen {
9988 ref track_name,
9989 frozen,
9990 } = a
9991 else {
9992 return false;
9993 };
9994
9995 let track = match self.track_handle_or_err(track_name) {
9996 Ok(track) => track,
9997 Err(e) => {
9998 self.notify_clients(Err(e)).await;
9999 return true;
10000 }
10001 };
10002 track.lock().set_frozen(frozen);
10003
10004 false
10005 }
10006
10007 async fn handle_track_set_session_slot_play_enabled(&mut self, a: Action) -> bool {
10008 let Action::TrackSetSessionSlotPlayEnabled {
10009 ref track_name,
10010 scene_index,
10011 enabled,
10012 } = a
10013 else {
10014 return false;
10015 };
10016
10017 let track = match self.track_handle_or_err(track_name) {
10018 Ok(track) => track,
10019 Err(e) => {
10020 self.notify_clients(Err(e)).await;
10021 return true;
10022 }
10023 };
10024 let track = track.lock();
10025 if let Some(slot) = track.session_slots.get_mut(&scene_index) {
10026 slot.play_enabled = enabled;
10027 }
10028
10029 false
10030 }
10031
10032 async fn handle_track_clear_default_passthrough(&mut self, a: Action) -> bool {
10033 let Action::TrackClearDefaultPassthrough { ref track_name } = a else {
10034 return false;
10035 };
10036
10037 if self
10038 .reject_if_track_frozen(track_name, "plugin graph editing")
10039 .await
10040 {
10041 return true;
10042 }
10043 let track = match self.track_handle_or_err(track_name) {
10044 Ok(track) => track,
10045 Err(e) => {
10046 self.notify_clients(Err(e)).await;
10047 return true;
10048 }
10049 };
10050 track.lock().clear_default_passthrough();
10051
10052 false
10053 }
10054
10055 async fn handle_track_get_clap_note_names(&mut self, a: Action) -> bool {
10056 let Action::TrackGetClapNoteNames { ref track_name } = a else {
10057 return false;
10058 };
10059
10060 let track = match self.track_handle_or_err(track_name) {
10061 Ok(track) => track,
10062 Err(e) => {
10063 self.notify_clients(Err(e)).await;
10064 return true;
10065 }
10066 };
10067 let note_names = track.lock().get_clap_note_names();
10068 self.notify_clients(Ok(Action::TrackClapNoteNames {
10069 track_name: track_name.clone(),
10070 note_names,
10071 }))
10072 .await;
10073
10074 false
10075 }
10076
10077 #[cfg(all(unix, not(target_os = "macos")))]
10078 async fn handle_list_lv2_plugins(&mut self, a: Action) -> bool {
10079 let Action::ListLv2Plugins = a else {
10080 return false;
10081 };
10082
10083 match crate::plugins::scan_plugins::<crate::plugins::types::Lv2PluginInfo>("lv2") {
10084 Ok(plugins) => {
10085 self.notify_clients(Ok(Action::Lv2Plugins(plugins))).await;
10086 }
10087 Err(e) => {
10088 tracing::error!("LV2 plugin scan failed: {e}");
10089 self.notify_clients(Ok(Action::Lv2PluginsUnavailable { error: e }))
10090 .await;
10091 }
10092 }
10093 true
10094 }
10095
10096 async fn handle_list_vst3_plugins(&mut self, a: Action) -> bool {
10097 let Action::ListVst3Plugins = a else {
10098 return false;
10099 };
10100
10101 match crate::plugins::scan_plugins::<crate::plugins::types::Vst3PluginInfo>("vst3") {
10102 Ok(plugins) => {
10103 self.notify_clients(Ok(Action::Vst3Plugins(plugins))).await;
10104 }
10105 Err(e) => {
10106 tracing::error!("VST3 plugin scan failed: {e}");
10107 self.notify_clients(Ok(Action::Vst3PluginsUnavailable { error: e }))
10108 .await;
10109 }
10110 }
10111 true
10112 }
10113
10114 async fn handle_list_clap_plugins(&mut self, a: Action) -> bool {
10115 let Action::ListClapPlugins = a else {
10116 return false;
10117 };
10118
10119 match crate::plugins::scan_plugins::<crate::plugins::types::ClapPluginInfo>("clap") {
10120 Ok(plugins) => {
10121 self.notify_clients(Ok(Action::ClapPlugins(plugins))).await;
10122 }
10123 Err(e) => {
10124 tracing::error!("CLAP plugin scan failed: {e}");
10125 self.notify_clients(Ok(Action::ClapPluginsUnavailable { error: e }))
10126 .await;
10127 }
10128 }
10129 true
10130 }
10131
10132 async fn handle_list_clap_plugins_with_capabilities(&mut self, a: Action) -> bool {
10133 let Action::ListClapPluginsWithCapabilities = a else {
10134 return false;
10135 };
10136
10137 match crate::plugins::scan_plugins::<crate::plugins::types::ClapPluginInfo>("clap") {
10138 Ok(plugins) => {
10139 self.notify_clients(Ok(Action::ClapPlugins(plugins))).await;
10140 }
10141 Err(e) => {
10142 tracing::error!("CLAP plugin scan failed: {e}");
10143 self.notify_clients(Ok(Action::ClapPluginsUnavailable { error: e }))
10144 .await;
10145 }
10146 }
10147 true
10148 }
10149
10150 async fn handle_track_show_clap_gui(&mut self, a: Action) -> bool {
10151 let Action::TrackShowClapGui {
10152 ref track_name,
10153 instance_id,
10154 } = a
10155 else {
10156 return false;
10157 };
10158
10159 let track = match self.track_handle_or_err(track_name) {
10160 Ok(track) => track,
10161 Err(e) => {
10162 self.notify_clients(Err(e)).await;
10163 return true;
10164 }
10165 };
10166 if let Err(e) = track.lock().show_clap_gui(instance_id) {
10167 self.notify_clients(Err(e)).await;
10168 return true;
10169 }
10170
10171 false
10172 }
10173
10174 async fn handle_track_show_vst3_gui(&mut self, a: Action) -> bool {
10175 let Action::TrackShowVst3Gui {
10176 ref track_name,
10177 instance_id,
10178 } = a
10179 else {
10180 return false;
10181 };
10182
10183 let track = match self.track_handle_or_err(track_name) {
10184 Ok(track) => track,
10185 Err(e) => {
10186 self.notify_clients(Err(e)).await;
10187 return true;
10188 }
10189 };
10190 if let Err(e) = track.lock().show_vst3_gui(instance_id) {
10191 self.notify_clients(Err(e)).await;
10192 return true;
10193 }
10194
10195 false
10196 }
10197
10198 #[cfg(all(unix, not(target_os = "macos")))]
10199 async fn handle_track_show_lv2_gui(&mut self, a: Action) -> bool {
10200 let Action::TrackShowLv2Gui {
10201 ref track_name,
10202 instance_id,
10203 } = a
10204 else {
10205 return false;
10206 };
10207
10208 let track = match self.track_handle_or_err(track_name) {
10209 Ok(track) => track,
10210 Err(e) => {
10211 self.notify_clients(Err(e)).await;
10212 return true;
10213 }
10214 };
10215 if let Err(e) = track.lock().show_lv2_gui(instance_id) {
10216 self.notify_clients(Err(e)).await;
10217 return true;
10218 }
10219
10220 false
10221 }
10222
10223 async fn handle_track_vst3_restore_state(&mut self, a: Action) -> bool {
10224 let Action::TrackVst3RestoreState {
10225 ref track_name,
10226 instance_id,
10227 ref state,
10228 } = a
10229 else {
10230 return false;
10231 };
10232 match self.track_handle_or_err(track_name) {
10233 Ok(track) => {
10234 if let Err(e) = track.lock().vst3_restore_state(instance_id, state) {
10235 self.notify_clients(Err(e)).await;
10236 return true;
10237 }
10238 self.notify_clients(Ok(a.clone())).await;
10239 }
10240 Err(e) => {
10241 self.notify_clients(Err(e)).await;
10242 }
10243 };
10244 false
10245 }
10246
10247 async fn handle_set_clip_fade(&mut self, a: Action) -> bool {
10248 let Action::SetClipFade {
10249 ref track_name,
10250 clip_index,
10251 kind,
10252 fade_enabled,
10253 fade_in_samples,
10254 fade_out_samples,
10255 } = a
10256 else {
10257 return false;
10258 };
10259
10260 self.set_clip_fade(
10261 track_name,
10262 clip_index,
10263 kind,
10264 fade_enabled,
10265 fade_in_samples,
10266 fade_out_samples,
10267 );
10268
10269 false
10270 }
10271
10272 #[inline]
10273 fn box_bool<'a>(
10274 fut: impl std::future::Future<Output = bool> + Send + 'a,
10275 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = bool> + Send + 'a>> {
10276 Box::pin(fut)
10277 }
10278
10279 async fn handle_request_inner(&mut self, mut action_to_process: Action, record_history: bool) {
10280 let a = action_to_process.clone();
10281 let suppress_timing_history = self.playing
10282 && matches!(
10283 &action_to_process,
10284 Action::SetTempo(_) | Action::SetTimeSignature { .. } | Action::SetTempoMap { .. }
10285 );
10286 let mut inverse_actions = self.prepare_inverse_actions(
10287 &action_to_process,
10288 record_history,
10289 suppress_timing_history,
10290 );
10291
10292 match action_to_process {
10293 Action::Play => {
10294 if Self::box_bool(self.handle_play(a.clone())).await {
10295 return;
10296 }
10297 }
10298 Action::Pause => {
10299 if Self::box_bool(self.handle_pause(a.clone())).await {
10300 return;
10301 }
10302 }
10303 Action::Stop => {
10304 if Self::box_bool(self.handle_stop(a.clone())).await {
10305 return;
10306 }
10307 }
10308 Action::SessionPlay => {
10309 if Self::box_bool(self.handle_session_play(a.clone())).await {
10310 return;
10311 }
10312 }
10313 Action::JumpToEnd => {
10314 self.transport_sample = self.normalize_transport_sample(self.session_end_sample());
10315 self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
10316 .await;
10317 }
10318 Action::Panic => {
10319 if Self::box_bool(self.handle_panic(a.clone())).await {
10320 return;
10321 }
10322 }
10323 Action::Session(ref session_action) => {
10324 self.handle_session_action(session_action.clone()).await;
10325 }
10326 Action::SessionRuntimeReport { .. } => {}
10327 Action::SessionMidiLearnTriggered { .. } => {}
10328 Action::SetClipPlaybackEnabled(enabled) => {
10329 self.clip_playback_enabled = enabled;
10330 for track in self.state.lock().tracks.values() {
10331 track.lock().set_clip_playback_enabled(enabled);
10332 }
10333 }
10334 Action::SetSessionClipPlaybackEnabled(enabled) => {
10335 self.session_clip_playback_enabled = enabled;
10336 for track in self.state.lock().tracks.values() {
10337 track.lock().set_session_clip_playback_enabled(enabled);
10338 }
10339 }
10340 Action::TransportPosition(..) => {
10341 if Self::box_bool(self.handle_transport_position(a.clone())).await {
10342 return;
10343 }
10344 }
10345 Action::SetLoopEnabled(enabled) => {
10346 self.loop_enabled = enabled && self.loop_range_samples.is_some();
10347 self.notified_loop_wrap_sample = None;
10348 }
10349 Action::SetLoopRange(..) => {
10350 if Self::box_bool(self.handle_set_loop_range(a.clone())).await {
10351 return;
10352 }
10353 }
10354 Action::SetPunchEnabled(enabled) => {
10355 self.punch_enabled = enabled && self.punch_range_samples.is_some();
10356 }
10357 Action::SetPunchRange(range) => {
10358 self.punch_range_samples = range.and_then(|(start, end)| {
10359 if end > start {
10360 Some((start, end))
10361 } else {
10362 None
10363 }
10364 });
10365 self.punch_enabled = self.punch_range_samples.is_some();
10366 }
10367 Action::SetMetronomeEnabled(enabled) => {
10368 self.metronome_enabled = enabled;
10369 if enabled {
10370 self.ensure_metronome_track().await;
10371 }
10372 if let Some(track) = self.state.lock().tracks.get(Self::METRONOME_TRACK).cloned() {
10373 track.lock().set_metronome_enabled(enabled);
10374 }
10375 }
10376 Action::SetTempo(bpm) => {
10377 self.tempo_bpm = bpm.max(1.0);
10378 }
10379 Action::SetTimeSignature {
10380 numerator,
10381 denominator,
10382 } => {
10383 self.tsig_num = numerator.max(1);
10384 self.tsig_denom = denominator.max(1);
10385 }
10386 Action::SetTempoMap {
10387 ref tempo_points,
10388 ref time_signature_points,
10389 } => {
10390 self.tempo_points = tempo_points.clone();
10391 self.time_signature_points = time_signature_points.clone();
10392 self.update_global_tempo_from_map();
10393 }
10394 Action::SetOscEnabled(enabled) => {
10395 if let Err(err) = self.set_osc_enabled_with(enabled, OscServer::start) {
10396 self.notify_clients(Err(err)).await;
10397 }
10398 }
10399 Action::SetRecordEnabled(..) => {
10400 if Self::box_bool(self.handle_set_record_enabled(a.clone())).await {
10401 return;
10402 }
10403 }
10404 Action::SetModulators(ref modulators) => {
10405 self.modulators = modulators.clone();
10406 let echoes = self.apply_modulators(self.active_transport_sample());
10407 for action in echoes {
10408 self.notify_clients(Ok(action)).await;
10409 }
10410 }
10411 Action::SetTrackAutomationLanes {
10412 ref track_name,
10413 ref lanes,
10414 mode,
10415 } => {
10416 if let Some(track) = self.state.lock().tracks.get(track_name) {
10417 let track = track.lock();
10418 track.automation_lanes = lanes.clone();
10419 track.automation_mode = mode;
10420 }
10421 }
10422 Action::TrackAutomationToggleLane { .. } => {
10423 if Self::box_bool(self.handle_track_automation_toggle_lane(a.clone())).await {
10424 return;
10425 }
10426 }
10427 Action::TrackAutomationInsertPoint { .. } => {
10428 if Self::box_bool(self.handle_track_automation_insert_point(a.clone())).await {
10429 return;
10430 }
10431 }
10432 Action::TrackAutomationDeletePoint { .. } => {
10433 if Self::box_bool(self.handle_track_automation_delete_point(a.clone())).await {
10434 return;
10435 }
10436 }
10437 Action::TrackAutomationSetMode {
10438 ref track_name,
10439 mode,
10440 } => {
10441 if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
10442 track.lock().automation_mode = mode;
10443 }
10444 }
10445 Action::RequestTrackList => {
10446 let names: Vec<String> = self.state.lock().tracks.keys().cloned().collect();
10447 self.notify_clients(Ok(Action::TrackList(names))).await;
10448 }
10449 Action::TrackList(_) => {}
10450 Action::RequestTransportState => {
10451 self.notify_clients(Ok(Action::TransportState {
10452 sample: self.transport_sample,
10453 tempo_bpm: self.tempo_bpm,
10454 playing: self.playing,
10455 paused: !self.transport_running && self.playing,
10456 tsig_num: self.tsig_num,
10457 tsig_denom: self.tsig_denom,
10458 }))
10459 .await;
10460 }
10461 Action::TransportState { .. } => {}
10462 Action::SetStepRecording(enabled) => {
10463 self.step_recording_enabled = enabled;
10464 }
10465 Action::BeginHistoryGroup if self.history_group.is_none() => {
10466 self.history_group = Some(UndoEntry {
10467 forward_actions: vec![],
10468 inverse_actions: vec![],
10469 });
10470 }
10471 Action::EndHistoryGroup => {
10472 if let Some(mut group) = self.history_group.take()
10473 && !group.forward_actions.is_empty()
10474 && !group.inverse_actions.is_empty()
10475 {
10476 let mut add_tracks = Vec::new();
10477 let mut connections = Vec::new();
10478 let mut rest = Vec::new();
10479 for action in group.inverse_actions {
10480 if matches!(action, Action::AddTrack { .. }) {
10481 add_tracks.push(action);
10482 } else if matches!(action, Action::Connect { .. }) {
10483 connections.push(action);
10484 } else {
10485 rest.push(action);
10486 }
10487 }
10488 group.inverse_actions = add_tracks;
10489 group.inverse_actions.extend(rest);
10490 group.inverse_actions.extend(connections);
10491 self.history.record(group);
10492 }
10493 }
10494 Action::SetSessionPath(ref path) => {
10495 self.session_dir = Some(Path::new(path).to_path_buf());
10496 self.ensure_session_subdirs();
10497 #[cfg(all(unix, not(target_os = "macos")))]
10498 let _lv2_dir = self.session_plugins_dir();
10499 for track in self.state.lock().tracks.values() {
10500 track.lock().set_session_base_dir(self.session_dir.clone());
10501 }
10502 }
10503 Action::MarkHistorySavePoint => {
10504 self.history.mark_save_point();
10505 self.notify_clients(Ok(Action::HistoryState {
10506 dirty: self.history.is_dirty(),
10507 }))
10508 .await;
10509 }
10510 Action::ClearHistory => {
10511 self.history.clear();
10512 self.history.mark_save_point();
10513 }
10514 Action::BeginSessionRestore => {
10515 self.history_suspended = true;
10516 self.history.clear();
10517 }
10518 Action::EndSessionRestore => {
10519 self.history.clear();
10520 self.history_suspended = false;
10521 self.preload_track_clips_spawn();
10522 }
10523 Action::Quit => {
10524 self.handle_quit(a.clone()).await;
10525 return;
10526 }
10527 Action::AddTrack {
10528 ref name,
10529 audio_ins,
10530 midi_ins,
10531 audio_outs,
10532 midi_outs,
10533 folder,
10534 } => {
10535 self.handle_add_track(
10536 name.clone(),
10537 audio_ins,
10538 midi_ins,
10539 audio_outs,
10540 midi_outs,
10541 folder,
10542 )
10543 .await;
10544 }
10545 Action::TrackAddAudioInput(..) => {
10546 if Self::box_bool(self.handle_track_add_audio_input(a.clone())).await {
10547 return;
10548 }
10549 }
10550 Action::TrackAddAudioOutput(..) => {
10551 if Self::box_bool(self.handle_track_add_audio_output(a.clone())).await {
10552 return;
10553 }
10554 }
10555 Action::TrackRemoveAudioInput(..) => {
10556 if Self::box_bool(self.handle_track_remove_audio_input(a.clone())).await {
10557 return;
10558 }
10559 }
10560 Action::TrackRemoveAudioOutput(..) => {
10561 if Self::box_bool(self.handle_track_remove_audio_output(a.clone())).await {
10562 return;
10563 }
10564 }
10565 Action::RenameTrack { .. } => {
10566 if Self::box_bool(self.handle_rename_track(a.clone())).await {
10567 return;
10568 }
10569 }
10570 Action::RemoveTrack(ref name) => {
10571 self.handle_remove_track(name.clone(), record_history).await;
10572 inverse_actions = None;
10573 }
10574 Action::TrackLevel(ref name, level) => {
10575 if name == "hw:out" {
10576 self.hw_out_level_db = level;
10577 } else if let Some(track) = self.state.lock().tracks.get(name) {
10578 track.lock().set_level(level);
10579 }
10580 }
10581 Action::TrackBalance(ref name, balance) => {
10582 if name == "hw:out" {
10583 self.hw_out_balance = balance.clamp(-1.0, 1.0);
10584 } else if let Some(track) = self.state.lock().tracks.get(name) {
10585 track.lock().set_balance(balance);
10586 }
10587 }
10588 Action::TrackAutomationLevel(ref name, level) => {
10589 tracing::debug!(%name, level, "engine received TrackAutomationLevel");
10590 if name == "hw:out" {
10591 self.hw_out_level_db = level;
10592 } else if let Some(track) = self.state.lock().tracks.get(name) {
10593 track.lock().set_level(level);
10594 }
10595 }
10596 Action::TrackAutomationBalance(ref name, balance) => {
10597 if name == "hw:out" {
10598 self.hw_out_balance = balance.clamp(-1.0, 1.0);
10599 } else if let Some(track) = self.state.lock().tracks.get(name) {
10600 track.lock().set_balance(balance);
10601 }
10602 }
10603 Action::TrackMidiCc { .. } => {
10604 if Self::box_bool(self.handle_track_midi_cc(a.clone())).await {
10605 return;
10606 }
10607 }
10608 Action::RequestMeterSnapshot => {
10609 self.notify_clients(Ok(Action::MeterSnapshot {
10610 hw_out_db: self.latest_hw_out_meter_db.clone(),
10611 track_meters: self.latest_track_meter_snapshot.clone(),
10612 }))
10613 .await;
10614 return;
10615 }
10616 Action::TrackMeters { .. } => {}
10617 Action::MeterSnapshot { .. } => {}
10618 Action::TrackToggleArm(..) => {
10619 if Self::box_bool(self.handle_track_toggle_arm(a.clone())).await {
10620 return;
10621 }
10622 }
10623 Action::TrackToggleMute(ref name) => {
10624 if name == "hw:out" {
10625 self.hw_out_muted = !self.hw_out_muted;
10626 } else if let Some(track) = self.state.lock().tracks.get(name) {
10627 track.lock().mute();
10628 }
10629 }
10630 Action::TrackTogglePhase(ref name) => {
10631 if let Some(track) = self.state.lock().tracks.get(name) {
10632 track.lock().invert_phase();
10633 }
10634 }
10635 Action::TrackToggleSolo(ref name) => {
10636 if name == "hw:out" {
10637 return;
10638 }
10639 if let Some(track) = self.state.lock().tracks.get(name) {
10640 track.lock().solo();
10641 }
10642 }
10643 Action::TrackToggleMaster(ref name) => {
10644 if let Some(track) = self.state.lock().tracks.get(name) {
10645 track.lock().toggle_master();
10646 }
10647 }
10648 Action::TrackToggleInputMonitor {
10649 ref track_name,
10650 lane,
10651 } => {
10652 if let Some(track) = self.state.lock().tracks.get(track_name) {
10653 track.lock().toggle_input_monitor(lane);
10654 }
10655 }
10656 Action::TrackToggleDiskMonitor {
10657 ref track_name,
10658 lane,
10659 } => {
10660 if let Some(track) = self.state.lock().tracks.get(track_name) {
10661 track.lock().toggle_disk_monitor(lane);
10662 }
10663 }
10664 Action::TrackToggleMidiInputMonitor {
10665 ref track_name,
10666 lane,
10667 } => {
10668 if let Some(track) = self.state.lock().tracks.get(track_name) {
10669 track.lock().toggle_midi_input_monitor(lane);
10670 }
10671 }
10672 Action::TrackToggleMidiDiskMonitor {
10673 ref track_name,
10674 lane,
10675 } => {
10676 if let Some(track) = self.state.lock().tracks.get(track_name) {
10677 track.lock().toggle_midi_disk_monitor(lane);
10678 }
10679 }
10680 Action::TrackSetColor {
10681 ref track_name,
10682 color,
10683 } => {
10684 if let Some(track) = self.state.lock().tracks.get(track_name) {
10685 track.lock().color = color;
10686 }
10687 }
10688 Action::TrackArmMidiLearn {
10689 ref track_name,
10690 target,
10691 } => {
10692 if let Err(e) = self.track_handle_or_err(track_name) {
10693 self.notify_clients(Err(e)).await;
10694 return;
10695 }
10696 self.pending_midi_learn = Some((track_name.clone(), target, None));
10697 }
10698 Action::GlobalArmMidiLearn { target } => {
10699 self.pending_global_midi_learn = Some(target);
10700 }
10701 Action::SessionArmMidiLearn { ref target } => {
10702 self.pending_session_midi_learn = Some(target.clone());
10703 }
10704 Action::TrackSetMidiLearnBinding { .. } => {
10705 if Self::box_bool(self.handle_track_set_midi_learn_binding(a.clone())).await {
10706 return;
10707 }
10708 }
10709 Action::SetGlobalMidiLearnBinding { .. } => {
10710 if Self::box_bool(self.handle_set_global_midi_learn_binding(a.clone())).await {
10711 return;
10712 }
10713 }
10714 Action::SetSessionMidiLearnBinding { .. } => {
10715 if Self::box_bool(self.handle_set_session_midi_learn_binding(a.clone())).await {
10716 return;
10717 }
10718 }
10719 Action::TrackSetFolder { .. } => {
10720 if Self::box_bool(self.handle_track_set_folder(a.clone())).await {
10721 return;
10722 }
10723 }
10724 Action::TrackSetParent {
10725 ref track_name,
10726 ref parent_name,
10727 } => {
10728 self.handle_track_set_parent(track_name.as_str(), parent_name.as_deref())
10729 .await;
10730 }
10731 Action::TrackToggleFolder { .. } => {
10732 if Self::box_bool(self.handle_track_toggle_folder(a.clone())).await {
10733 return;
10734 }
10735 }
10736 Action::TrackSetMidiLaneChannel { .. } => {
10737 if Self::box_bool(self.handle_track_set_midi_lane_channel(a.clone())).await {
10738 return;
10739 }
10740 }
10741 Action::TrackSetFrozen { .. } => {
10742 if Self::box_bool(self.handle_track_set_frozen(a.clone())).await {
10743 return;
10744 }
10745 }
10746 Action::TrackSetSessionSlot { .. } => {
10747 if Self::box_bool(self.handle_track_set_session_slot(a.clone())).await {
10748 return;
10749 }
10750 }
10751 Action::TrackSetSessionSlotPlayEnabled { .. } => {
10752 if self
10753 .handle_track_set_session_slot_play_enabled(a.clone())
10754 .await
10755 {
10756 return;
10757 }
10758 }
10759 Action::TrackOfflineBounce { .. } => {
10760 self.handle_track_offline_bounce(action_to_process).await;
10761 return;
10762 }
10763 Action::TrackOfflineBounceCancel { .. } => {}
10764 Action::TrackOfflineBounceCancelAll => {}
10765 Action::TrackOfflineBounceCanceled { .. } => {}
10766 Action::TrackOfflineBounceProgress { .. } => {}
10767 Action::PianoKey {
10768 ref track_name,
10769 note,
10770 velocity,
10771 on,
10772 } => {
10773 if let Some(track) = self.state.lock().tracks.get(track_name) {
10774 let status = if on { 0x90 } else { 0x80 };
10775 let event = MidiEvent::new(0, vec![status, note.min(127), velocity.min(127)]);
10776 track.lock().push_hw_midi_events(&[event]);
10777 }
10778 }
10779 Action::ModifyMidiNotes { .. }
10780 | Action::ModifyMidiControllers { .. }
10781 | Action::DeleteMidiControllers { .. }
10782 | Action::InsertMidiControllers { .. }
10783 | Action::DeleteMidiNotes { .. }
10784 | Action::InsertMidiNotes { .. } => {
10785 if let Err(e) = self.apply_midi_edit_action(&action_to_process) {
10786 self.notify_clients(Err(e)).await;
10787 return;
10788 }
10789 }
10790 Action::SetMidiSysExEvents { .. } => {
10791 if let Err(e) = self.apply_midi_edit_action(&action_to_process) {
10792 self.notify_clients(Err(e)).await;
10793 return;
10794 }
10795 }
10796 Action::TrackClearDefaultPassthrough { .. } => {
10797 if Self::box_bool(self.handle_track_clear_default_passthrough(a.clone())).await {
10798 return;
10799 }
10800 }
10801 Action::TrackClearPlugins { .. } => {
10802 if Self::box_bool(self.handle_track_clear_plugins(a.clone())).await {
10803 return;
10804 }
10805 }
10806 #[cfg(all(unix, not(target_os = "macos")))]
10807 Action::ClipSetLv2PluginState { ref track_name, .. } => {
10808 self.notify_clients(Err(format!(
10809 "Track '{}': clip LV2 plugin state changes are not supported",
10810 track_name
10811 )))
10812 .await;
10813 }
10814 Action::TrackGetClapNoteNames { .. } => {
10815 if Self::box_bool(self.handle_track_get_clap_note_names(a.clone())).await {
10816 return;
10817 }
10818 }
10819 Action::TrackGetPluginGraph { .. } => {
10820 if Self::box_bool(self.handle_track_get_plugin_graph(a.clone())).await {
10821 return;
10822 }
10823 }
10824 Action::TrackPluginGraph { .. } => {}
10825 Action::TrackConnectPluginAudio { .. } => {
10826 if Self::box_bool(self.handle_track_connect_plugin_audio(a.clone())).await {
10827 return;
10828 }
10829 }
10830 Action::TrackConnectPluginMidi { .. } => {
10831 if Self::box_bool(self.handle_track_connect_plugin_midi(a.clone())).await {
10832 return;
10833 }
10834 }
10835 Action::TrackDisconnectPluginAudio { .. } => {
10836 if Self::box_bool(self.handle_track_disconnect_plugin_audio(a.clone())).await {
10837 return;
10838 }
10839 }
10840 Action::TrackDisconnectPluginMidi { .. } => {
10841 if Self::box_bool(self.handle_track_disconnect_plugin_midi(a.clone())).await {
10842 return;
10843 }
10844 }
10845 Action::TrackConnectAudio { .. } => {
10846 if Self::box_bool(self.handle_track_connect_audio(a.clone())).await {
10847 return;
10848 }
10849 }
10850 Action::TrackDisconnectAudio { .. } => {
10851 if Self::box_bool(self.handle_track_disconnect_audio(a.clone())).await {
10852 return;
10853 }
10854 }
10855 Action::TrackConnectMidi { .. } => {
10856 if Self::box_bool(self.handle_track_connect_midi(a.clone())).await {
10857 return;
10858 }
10859 }
10860 Action::TrackDisconnectMidi { .. } => {
10861 if Self::box_bool(self.handle_track_disconnect_midi(a.clone())).await {
10862 return;
10863 }
10864 }
10865 #[cfg(all(unix, not(target_os = "macos")))]
10866 Action::ListLv2Plugins => {
10867 if Self::box_bool(self.handle_list_lv2_plugins(a.clone())).await {
10868 return;
10869 }
10870 }
10871 #[cfg(all(unix, not(target_os = "macos")))]
10872 Action::Lv2Plugins(_) => {}
10873 #[cfg(all(unix, not(target_os = "macos")))]
10874 Action::Lv2PluginsUnavailable { .. } => {}
10875 Action::ListVst3Plugins => {
10876 if Self::box_bool(self.handle_list_vst3_plugins(a.clone())).await {
10877 return;
10878 }
10879 }
10880 Action::Vst3Plugins(_) => {}
10881 Action::Vst3PluginsUnavailable { .. } => {}
10882 Action::ListClapPlugins => {
10883 if Self::box_bool(self.handle_list_clap_plugins(a.clone())).await {
10884 return;
10885 }
10886 }
10887 Action::ListClapPluginsWithCapabilities => {
10888 if self
10889 .handle_list_clap_plugins_with_capabilities(a.clone())
10890 .await
10891 {
10892 return;
10893 }
10894 }
10895 Action::ClapPlugins(_) => {}
10896 Action::ClapPluginsUnavailable { .. } => {}
10897 Action::TrackLoadClapPlugin {
10898 ref track_name,
10899 ref plugin_id,
10900 instance_id,
10901 } => {
10902 if self
10903 .handle_track_load_clap_plugin(
10904 track_name.as_str(),
10905 plugin_id.as_str(),
10906 instance_id,
10907 )
10908 .await
10909 {
10910 return;
10911 }
10912 }
10913 Action::TrackUnloadClapPlugin {
10914 ref track_name,
10915 ref plugin_id,
10916 } => {
10917 if self
10918 .handle_track_unload_clap_plugin(track_name.as_str(), plugin_id.as_str())
10919 .await
10920 {
10921 return;
10922 }
10923 }
10924 Action::TrackUnloadClapPluginInstance {
10925 ref track_name,
10926 instance_id,
10927 } => {
10928 if self
10929 .handle_track_unload_clap_plugin_instance(track_name.as_str(), instance_id)
10930 .await
10931 {
10932 return;
10933 }
10934 }
10935 Action::TrackShowClapGui { .. } => {
10936 if Self::box_bool(self.handle_track_show_clap_gui(a.clone())).await {
10937 return;
10938 }
10939 }
10940 Action::TrackLoadVst3Plugin {
10941 ref track_name,
10942 ref plugin_id,
10943 instance_id,
10944 } => {
10945 if self
10946 .handle_track_load_vst3_plugin(
10947 track_name.as_str(),
10948 plugin_id.as_str(),
10949 instance_id,
10950 )
10951 .await
10952 {
10953 return;
10954 }
10955 }
10956 Action::TrackUnloadVst3Plugin {
10957 ref track_name,
10958 ref plugin_id,
10959 } => {
10960 if self
10961 .handle_track_unload_vst3_plugin(track_name.as_str(), plugin_id.as_str())
10962 .await
10963 {
10964 return;
10965 }
10966 }
10967 Action::TrackUnloadVst3PluginInstance {
10968 ref track_name,
10969 instance_id,
10970 } => {
10971 if self
10972 .handle_track_unload_vst3_plugin_instance(track_name.as_str(), instance_id)
10973 .await
10974 {
10975 return;
10976 }
10977 }
10978 Action::TrackShowVst3Gui { .. } => {
10979 if Self::box_bool(self.handle_track_show_vst3_gui(a.clone())).await {
10980 return;
10981 }
10982 }
10983 #[cfg(all(unix, not(target_os = "macos")))]
10984 Action::TrackLoadLv2Plugin {
10985 ref track_name,
10986 ref plugin_uri,
10987 instance_id,
10988 } => {
10989 if self
10990 .handle_track_load_lv2_plugin(
10991 track_name.as_str(),
10992 plugin_uri.as_str(),
10993 instance_id,
10994 )
10995 .await
10996 {
10997 return;
10998 }
10999 }
11000 #[cfg(all(unix, not(target_os = "macos")))]
11001 Action::TrackUnloadLv2Plugin {
11002 ref track_name,
11003 ref plugin_uri,
11004 } => {
11005 if self
11006 .handle_track_unload_lv2_plugin(track_name.as_str(), plugin_uri.as_str())
11007 .await
11008 {
11009 return;
11010 }
11011 }
11012 #[cfg(all(unix, not(target_os = "macos")))]
11013 Action::TrackUnloadLv2PluginInstance {
11014 ref track_name,
11015 instance_id,
11016 } => {
11017 if self
11018 .handle_track_unload_lv2_plugin_instance(track_name.as_str(), instance_id)
11019 .await
11020 {
11021 return;
11022 }
11023 }
11024 #[cfg(all(unix, not(target_os = "macos")))]
11025 Action::TrackShowLv2Gui { .. } => {
11026 if Self::box_bool(self.handle_track_show_lv2_gui(a.clone())).await {
11027 return;
11028 }
11029 }
11030 Action::TrackSetPluginResourceDir { .. } => {
11031 if Self::box_bool(self.handle_track_set_plugin_resource_dir(a.clone())).await {
11032 return;
11033 }
11034 }
11035 Action::TrackClapFileReferences { .. } => {
11036 if Self::box_bool(self.handle_track_clap_file_references(a.clone())).await {
11037 return;
11038 }
11039 }
11040 Action::TrackUpdateClapFileReference { .. } => {
11041 if self
11042 .handle_track_update_clap_file_reference(a.clone())
11043 .await
11044 {
11045 return;
11046 }
11047 }
11048 Action::ClipSetPluginResourceDir { .. } => {
11049 if Self::box_bool(self.handle_clip_set_plugin_resource_dir(a.clone())).await {
11050 return;
11051 }
11052 }
11053 Action::ClipClapFileReferences { .. } => {
11054 if Self::box_bool(self.handle_clip_clap_file_references(a.clone())).await {
11055 return;
11056 }
11057 }
11058 Action::ClipUpdateClapFileReference { .. } => {
11059 if Self::box_bool(self.handle_clip_update_clap_file_reference(a.clone())).await {
11060 return;
11061 }
11062 }
11063 Action::TrackSetClapParameter { .. } => {
11064 if Self::box_bool(self.handle_track_set_clap_parameter(a.clone())).await {
11065 return;
11066 }
11067 }
11068 Action::ClipSetClapParameter { .. } => {
11069 if Self::box_bool(self.handle_clip_set_clap_parameter(a.clone())).await {
11070 return;
11071 }
11072 }
11073 Action::TrackSetClapParameterAt { .. } => {
11074 if Self::box_bool(self.handle_track_set_clap_parameter_at(a.clone())).await {
11075 return;
11076 }
11077 }
11078 Action::TrackBeginClapParameterEdit { .. } => {
11079 if Self::box_bool(self.handle_track_begin_clap_parameter_edit(a.clone())).await {
11080 return;
11081 }
11082 }
11083 Action::TrackEndClapParameterEdit { .. } => {
11084 if Self::box_bool(self.handle_track_end_clap_parameter_edit(a.clone())).await {
11085 return;
11086 }
11087 }
11088 Action::TrackGetClapParameters { .. } => {
11089 if Self::box_bool(self.handle_track_get_clap_parameters(a.clone())).await {
11090 return;
11091 }
11092 }
11093 Action::TrackClapParameters { .. } => {}
11094 Action::TrackClapSnapshotState { .. } => {
11095 if Self::box_bool(self.handle_track_clap_snapshot_state(a.clone())).await {
11096 return;
11097 }
11098 }
11099 Action::ClipClapSnapshotState { .. } => {
11100 if Self::box_bool(self.handle_clip_clap_snapshot_state(a.clone())).await {
11101 return;
11102 }
11103 }
11104 Action::TrackClapStateSnapshot { .. } => {}
11105 Action::ClipClapStateSnapshot { .. } => {}
11106 Action::TrackClapStateDirty { .. } => {}
11107 Action::ClipClapStateDirty { .. } => {}
11108 Action::TrackClapRestoreState { .. } => {
11109 if Self::box_bool(self.handle_track_clap_restore_state(a.clone())).await {
11110 return;
11111 }
11112 }
11113 Action::ClipClapRestoreState { .. } => {
11114 if Self::box_bool(self.handle_clip_clap_restore_state(a.clone())).await {
11115 return;
11116 }
11117 }
11118 Action::TrackSnapshotAllClapStates { .. } => {
11119 if Self::box_bool(self.handle_track_snapshot_all_clap_states(a.clone())).await {
11120 return;
11121 }
11122 }
11123 Action::TrackSnapshotAllClapStatesDone { .. } => {}
11124 Action::TrackGetVst3Graph { .. } => {
11125 if Self::box_bool(self.handle_track_get_vst3_graph(a.clone())).await {
11126 return;
11127 }
11128 }
11129 Action::TrackVst3Graph { .. } => {}
11130 Action::TrackSetVst3Parameter { .. } => {
11131 if Self::box_bool(self.handle_track_set_vst3_parameter(a.clone())).await {
11132 return;
11133 }
11134 }
11135 Action::TrackSetPluginBypassed { .. } => {
11136 if Self::box_bool(self.handle_track_set_plugin_bypassed(a.clone())).await {
11137 return;
11138 }
11139 }
11140 Action::TrackGetVst3Parameters { .. } => {
11141 if Self::box_bool(self.handle_track_get_vst3_parameters(a.clone())).await {
11142 return;
11143 }
11144 }
11145 Action::TrackVst3Parameters { .. } => {}
11146 #[cfg(all(unix, not(target_os = "macos")))]
11147 Action::TrackGetLv2PluginControls { .. } => {
11148 if Self::box_bool(self.handle_track_get_lv2_plugin_controls(a.clone())).await {
11149 return;
11150 }
11151 }
11152 #[cfg(all(unix, not(target_os = "macos")))]
11153 Action::TrackLv2SnapshotState { .. } => {
11154 if Self::box_bool(self.handle_track_lv2_snapshot_state(a.clone())).await {
11155 return;
11156 }
11157 }
11158 #[cfg(all(unix, not(target_os = "macos")))]
11159 Action::ClipLv2SnapshotState { .. } => {
11160 if Self::box_bool(self.handle_clip_lv2_snapshot_state(a.clone())).await {
11161 return;
11162 }
11163 }
11164 Action::TrackVst3SnapshotState { .. } => {
11165 if Self::box_bool(self.handle_track_vst3_snapshot_state(a.clone())).await {
11166 return;
11167 }
11168 }
11169 Action::ClipVst3SnapshotState { .. } => {
11170 if Self::box_bool(self.handle_clip_vst3_snapshot_state(a.clone())).await {
11171 return;
11172 }
11173 }
11174 Action::TrackVst3StateSnapshot { .. } => {}
11175 Action::ClipVst3StateSnapshot { .. } => {}
11176 Action::TrackVst3RestoreState { .. } => {
11177 if Self::box_bool(self.handle_track_vst3_restore_state(a.clone())).await {
11178 return;
11179 }
11180 }
11181 Action::TrackConnectVst3Audio { .. } => {
11182 if Self::box_bool(self.handle_track_connect_vst3_audio(a.clone())).await {
11183 return;
11184 }
11185 }
11186 Action::TrackDisconnectVst3Audio { .. } => {
11187 if Self::box_bool(self.handle_track_disconnect_vst3_audio(a.clone())).await {
11188 return;
11189 }
11190 }
11191 Action::ClipMove { .. } => {
11192 self.handle_clip_move(a.clone()).await;
11193 }
11194 Action::AddClip { .. } => {
11195 if Self::box_bool(self.handle_add_clip(a.clone())).await {
11196 return;
11197 }
11198 }
11199 Action::AddGroupedClip { .. } => {
11200 if Self::box_bool(self.handle_add_grouped_clip(a.clone())).await {
11201 return;
11202 }
11203 }
11204 Action::RemoveClip {
11205 ref track_name,
11206 kind,
11207 ref clip_indices,
11208 } => {
11209 self.remove_clips_from_track(track_name, kind, clip_indices);
11210 }
11211 Action::RenameClip {
11212 ref track_name,
11213 kind,
11214 clip_index,
11215 ref new_name,
11216 } => {
11217 self.rename_clip_references(track_name, kind, clip_index, new_name);
11218 }
11219 Action::SetClipSourceName {
11220 ref track_name,
11221 kind,
11222 clip_index,
11223 ref name,
11224 } => {
11225 self.set_clip_source_name(track_name, clip_index, kind, name.clone());
11226 }
11227 Action::SetClipFade { .. } => {
11228 if Self::box_bool(self.handle_set_clip_fade(a.clone())).await {
11229 return;
11230 }
11231 }
11232 Action::SetClipBounds {
11233 ref track_name,
11234 clip_index,
11235 kind,
11236 start,
11237 length,
11238 offset,
11239 } => {
11240 self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
11241 }
11242 Action::SyncClipBounds {
11243 ref track_name,
11244 clip_index,
11245 kind,
11246 start,
11247 length,
11248 offset,
11249 } => {
11250 self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
11251 }
11252 Action::SetClipMuted {
11253 ref track_name,
11254 clip_index,
11255 kind,
11256 muted,
11257 } => {
11258 self.set_clip_muted(track_name, clip_index, kind, muted);
11259 }
11260 Action::SetClipPluginGraphJson {
11261 ref track_name,
11262 clip_index,
11263 ref plugin_graph_json,
11264 } => {
11265 self.set_clip_plugin_graph_json(track_name, clip_index, plugin_graph_json.clone());
11266 }
11267 Action::SetClipPitchCorrection { .. } => {
11268 if Self::box_bool(self.handle_set_clip_pitch_correction(a.clone())).await {
11269 return;
11270 }
11271 }
11272 Action::Connect {
11273 ref from_track,
11274 from_port,
11275 ref to_track,
11276 to_port,
11277 kind,
11278 } => {
11279 self.handle_connect(
11280 from_track.as_str(),
11281 from_port,
11282 to_track.as_str(),
11283 to_port,
11284 kind,
11285 )
11286 .await;
11287 }
11288 Action::Disconnect { .. } => {
11289 self.handle_disconnect(a.clone()).await;
11290 }
11291 Action::OpenAudioDevice { .. } => {
11292 let (done, updated) = self.handle_open_audio_device(a.clone()).await;
11293 if done {
11294 return;
11295 }
11296 if let Some(action) = updated {
11297 action_to_process = action;
11298 }
11299 }
11300 Action::JackAddAudioInputPort => {
11301 if Self::box_bool(self.handle_jack_add_audio_input_port(a.clone())).await {
11302 return;
11303 }
11304 }
11305 Action::JackRemoveAudioInputPort(_removed_port) => {
11306 if self
11307 .handle_jack_remove_audio_input_port(_removed_port, a.clone())
11308 .await
11309 {
11310 return;
11311 }
11312 }
11313 Action::JackAddAudioOutputPort => {
11314 if Self::box_bool(self.handle_jack_add_audio_output_port(a.clone())).await {
11315 return;
11316 }
11317 }
11318 Action::JackRemoveAudioOutputPort(_removed_port) => {
11319 if self
11320 .handle_jack_remove_audio_output_port(_removed_port, a.clone())
11321 .await
11322 {
11323 return;
11324 }
11325 }
11326 Action::OpenMidiInputDevice(ref device) => {
11327 let midi_hub = self.midi_hub.lock();
11328 if let Err(e) = midi_hub.open_input(device) {
11329 self.notify_clients(Err(e)).await;
11330 return;
11331 }
11332 }
11333 Action::OpenMidiOutputDevice(ref device) => {
11334 let midi_hub = self.midi_hub.lock();
11335 if let Err(e) = midi_hub.open_output(device) {
11336 self.notify_clients(Err(e)).await;
11337 return;
11338 }
11339 }
11340 Action::RequestSessionDiagnostics => {
11341 self.handle_request_session_diagnostics().await;
11342 }
11343 Action::RequestMidiLearnMappingsReport => {
11344 self.handle_request_midi_learn_mappings_report().await;
11345 }
11346 Action::ClearAllMidiLearnBindings => {
11347 if Self::box_bool(self.handle_clear_all_midi_learn_bindings(a.clone())).await {
11348 return;
11349 }
11350 }
11351 #[cfg(all(unix, not(target_os = "macos")))]
11352 Action::TrackLv2PluginControls { .. } => {}
11353 #[cfg(all(unix, not(target_os = "macos")))]
11354 Action::ClipLv2PluginControls { .. } => {}
11355 #[cfg(all(unix, not(target_os = "macos")))]
11356 Action::TrackLv2StateSnapshot { .. } => {}
11357 #[cfg(all(unix, not(target_os = "macos")))]
11358 Action::ClipLv2StateSnapshot { .. } => {}
11359 #[cfg(all(unix, not(target_os = "macos")))]
11360 Action::TrackLv2Midnam { .. } => {}
11361 Action::TrackClapNoteNames { .. } => {}
11362 Action::SessionDiagnosticsReport { .. } => {}
11363 Action::MidiLearnMappingsReport { .. } => {}
11364 Action::HWInfo { .. } => {}
11365 Action::HistoryState { .. } => {}
11366 Action::Undo => {}
11367 Action::Redo => {}
11368 Action::ApplyGroupedActions(_) => {}
11369 _ => {}
11370 }
11371
11372 if let Some(inverse) = inverse_actions {
11373 if let Some(group) = self.history_group.as_mut() {
11374 group.forward_actions.push(action_to_process.clone());
11375 group.inverse_actions.splice(0..0, inverse);
11376 } else {
11377 self.history.record(UndoEntry {
11378 forward_actions: vec![action_to_process.clone()],
11379 inverse_actions: inverse,
11380 });
11381 }
11382 }
11383
11384 self.notify_clients(Ok(action_to_process)).await;
11385 }
11386 pub async fn work(&mut self) {
11387 while let Some(message) = self.rx.recv().await {
11388 match message {
11389 Message::Ready(id) => self.push_ready_worker(id),
11390 Message::Finished {
11391 worker_id,
11392 task,
11393 output_linear,
11394 process_epoch,
11395 parameter_updates,
11396 } => {
11397 tracing::debug!(
11398 "engine received Finished from worker {} for task {:?} (epoch {} vs {})",
11399 worker_id,
11400 task,
11401 process_epoch,
11402 self.track_process_epoch
11403 );
11404 self.push_ready_worker(worker_id);
11405 let task_key = Self::task_key(&task);
11406 self.task_processing_started_at.remove(&task_key);
11407 if process_epoch != self.track_process_epoch {
11408 if let Some(track) = self
11409 .state
11410 .lock()
11411 .tracks
11412 .get(&Self::task_track_name(&task))
11413 .cloned()
11414 {
11415 let t = track.lock();
11416 t.audio.finished = false;
11417 t.audio.processing = false;
11418 }
11419 continue;
11420 }
11421 self.cycle_tasks_running
11422 .retain(|t| Self::task_key(t) != task_key);
11423 self.cycle_tasks_finished.push(task.clone());
11424 let track_name = Self::task_track_name(&task);
11425 let peak = output_linear.iter().copied().fold(0.0_f32, |a, b| a.max(b));
11426 tracing::debug!(
11427 "Finished task for '{}' epoch={} output_peak={}",
11428 track_name,
11429 process_epoch,
11430 peak
11431 );
11432 self.track_meter_linear_by_track
11433 .insert(track_name.clone(), output_linear);
11434 for action in parameter_updates {
11435 self.notify_clients(Ok(action)).await;
11436 }
11437 self.force_stalled_task_completions();
11438 let all_finished = self.send_tasks().await;
11439 tracing::debug!(
11440 "engine after Finished for {}: all_finished={}",
11441 track_name,
11442 all_finished
11443 );
11444 if all_finished {
11445 self.on_all_tracks_finished().await;
11446 }
11447 }
11448 Message::Channel(s) => {
11449 self.clients.push(s);
11450 }
11451
11452 Message::Request(a) => {
11453 self.dispatch_request(a).await;
11454 }
11455 Message::OscRequest { action, reply_to } => {
11456 self.osc_reply_target = Some(reply_to);
11457 self.dispatch_request(action).await;
11458 self.osc_reply_target = None;
11459 }
11460 Message::OfflineBounceFinished { result } => {
11461 if let Ok(Action::TrackOfflineBounce { track_name, .. }) = &result {
11462 self.offline_bounce_jobs.remove(track_name);
11463 }
11464 self.notify_clients(result).await;
11465 if self.offline_bounce_jobs.is_empty() {
11466 while let Some(next) = self.pending_requests.pop_front() {
11467 self.handle_request(next).await;
11468 }
11469 }
11470 }
11471 Message::HWFinished => {
11472 if !self.awaiting_hwfinished {
11473 tracing::debug!("HWFinished ignored (not awaiting)");
11474 continue;
11475 }
11476 tracing::debug!("HWFinished handling; playing={}", self.playing);
11477 self.handling_hwfinished = true;
11478 self.awaiting_hwfinished = false;
11479 #[cfg(unix)]
11480 {
11481 if let Some(jack) = &self.jack_runtime {
11482 if !self.pending_hw_midi_out_events.is_empty() {
11483 let out_events =
11484 std::mem::take(&mut self.pending_hw_midi_out_events);
11485 jack.lock().write_events(&out_events);
11486 }
11487 let mut in_events = vec![];
11488 jack.lock().read_events_into(&mut in_events);
11489 if !in_events.is_empty() {
11490 self.pending_hw_midi_events.extend(in_events);
11491 }
11492 }
11493 }
11494 #[cfg(unix)]
11495 if self.jack_runtime.is_some() {
11496 self.sync_from_jack_transport().await;
11497 }
11498 while let Some(a) = self.pending_requests.pop_front() {
11499 self.handle_request(a).await;
11500 }
11501 self.apply_mute_solo_policy();
11502 self.append_recorded_cycle();
11503 self.flush_completed_recordings().await;
11504 let hw_in_routes = self.midi_hw_in_routes.clone();
11505 let pending_hw_in_by_device = self.pending_hw_midi_events_by_device.clone();
11506 let mut reconfigured_tracks = Vec::new();
11507 for (track_name, track) in self.state.lock().tracks.iter() {
11508 let track_lock = track.lock();
11509 if self.jack_runtime_is_some() {
11510 if !self.pending_hw_midi_events.is_empty() {
11511 track_lock.push_hw_midi_events(&self.pending_hw_midi_events);
11512 }
11513 } else {
11514 for route in hw_in_routes.iter().filter(|r| &r.to_track == track_name) {
11515 if let Some(events) = pending_hw_in_by_device.get(&route.device) {
11516 track_lock.push_hw_midi_events_to_port(route.to_port, events);
11517 }
11518 }
11519 }
11520 if track_lock.setup() {
11521 reconfigured_tracks.push(track_name.clone());
11522 }
11523 }
11524 self.publish_track_meters().await;
11525 self.publish_session_runtime_reports().await;
11526 self.publish_clap_state_dirty().await;
11527 for track_name in reconfigured_tracks {
11528 let track = self.state.lock().tracks.get(&track_name).cloned();
11529 if let Some(track) = track {
11530 let (plugins, connections, connectable_connections) = {
11531 let track_lock = track.lock();
11532 (
11533 track_lock.plugin_graph_plugins(false),
11534 track_lock.plugin_graph_connections(),
11535 track_lock.connectable_connections(),
11536 )
11537 };
11538 self.notify_clients(Ok(Action::TrackPluginGraph {
11539 track_name: track_name.clone(),
11540 plugins,
11541 connections,
11542 connectable_connections,
11543 }))
11544 .await;
11545 }
11546 }
11547 self.pending_hw_midi_events.clear();
11548 self.pending_hw_midi_events_by_device.clear();
11549 if self.transport_running {
11550 if self.transport_panic_flush_pending {
11551 self.transport_panic_flush_pending = false;
11552 } else if self.transport_restart_pending {
11553 self.transport_restart_pending = false;
11554 } else {
11555 let next = self
11556 .transport_sample
11557 .saturating_add(self.current_cycle_samples());
11558 let normalized = self.normalize_transport_sample(next);
11559 let wrapped = normalized != next;
11560 self.transport_sample = normalized;
11561 if wrapped {
11562 if self.notified_loop_wrap_sample == Some(self.transport_sample) {
11563 self.notified_loop_wrap_sample = None;
11564 } else {
11565 self.notify_clients(Ok(Action::TransportPosition(
11566 self.transport_sample,
11567 )))
11568 .await;
11569 }
11570 }
11571 }
11572 }
11573 if self.session_clip_playback_enabled && self.playing {
11574 self.session_transport_sample = self
11575 .session_transport_sample
11576 .saturating_add(self.current_cycle_samples());
11577 }
11578 {
11579 let echoes = self.apply_modulators(self.active_transport_sample());
11580 for action in echoes {
11581 self.notify_clients(Ok(action)).await;
11582 }
11583 }
11584 self.invalidate_track_cycle_state();
11585 let all_finished = self.send_tasks().await;
11586 tracing::debug!(
11587 "HWFinished send_tasks finished={} hw_worker={}",
11588 all_finished,
11589 self.hw_worker.is_some()
11590 );
11591 if all_finished && self.hw_worker.is_some() {
11592 self.request_hw_cycle().await;
11593 }
11594 #[cfg(unix)]
11595 {
11596 if self.jack_runtime.is_some() {
11597 self.awaiting_hwfinished = true;
11598 }
11599 }
11600 self.handling_hwfinished = false;
11601 }
11602 Message::HWMidiEvents(events) => {
11603 for hw_event in events {
11604 let thru_targets: Vec<String> = self
11605 .midi_hw_thru_routes
11606 .iter()
11607 .filter(|route| route.from_device == hw_event.device)
11608 .map(|route| route.to_device.clone())
11609 .collect();
11610 for device in thru_targets {
11611 self.pending_hw_midi_out_events_by_device.push(HwMidiEvent {
11612 device,
11613 event: hw_event.event.clone(),
11614 });
11615 }
11616 if hw_event.event.data.len() >= 3 {
11617 let status = hw_event.event.data[0];
11618 if status & 0xF0 == 0xB0 {
11619 let channel = status & 0x0F;
11620 let cc = hw_event.event.data[1];
11621 let value = hw_event.event.data[2];
11622 self.handle_incoming_hw_cc(&hw_event.device, channel, cc, value)
11623 .await;
11624 }
11625 if self.step_recording_enabled && status & 0xF0 == 0x90 {
11626 let channel = status & 0x0F;
11627 let pitch = hw_event.event.data[1];
11628 let velocity = hw_event.event.data[2];
11629 if velocity > 0 {
11630 self.notify_clients(Ok(Action::StepRecordMidiNote {
11631 device: hw_event.device.clone(),
11632 channel,
11633 pitch,
11634 velocity,
11635 }))
11636 .await;
11637 }
11638 }
11639 }
11640 self.pending_hw_midi_events_by_device
11641 .entry(hw_event.device)
11642 .or_default()
11643 .push(hw_event.event);
11644 }
11645 }
11646 _ => {}
11647 }
11648 }
11649 }
11650
11651 fn collect_hw_midi_output_events(&self) -> Vec<MidiEvent> {
11652 let mut events = vec![];
11653 for track in self.state.lock().tracks.values() {
11654 events.extend(
11655 track
11656 .lock()
11657 .take_hw_midi_out_events()
11658 .into_iter()
11659 .map(|evt| evt.event),
11660 );
11661 }
11662 events.sort_by_key(|a| a.frame);
11663 events
11664 }
11665
11666 fn collect_hw_midi_output_events_by_device(&mut self) -> Vec<HwMidiEvent> {
11667 let mut events = Vec::<HwMidiEvent>::new();
11668 let routes = self.midi_hw_out_routes.clone();
11669 let mut events_by_track = HashMap::<String, Vec<crate::track::HwMidiOutEvent>>::new();
11670 {
11671 let state = self.state.lock();
11672 for route in &routes {
11673 if events_by_track.contains_key(&route.from_track) {
11674 continue;
11675 }
11676 let Some(track) = state.tracks.get(&route.from_track) else {
11677 continue;
11678 };
11679 events_by_track.insert(
11680 route.from_track.clone(),
11681 track.lock().take_hw_midi_out_events(),
11682 );
11683 }
11684 }
11685
11686 for route in routes {
11687 let Some(track_events) = events_by_track.get(&route.from_track) else {
11688 continue;
11689 };
11690 for hw_event in track_events
11691 .iter()
11692 .filter(|evt| evt.port == route.from_port)
11693 {
11694 self.update_active_hw_notes_for_track(
11695 &route.from_track,
11696 &route.device,
11697 &hw_event.event.data,
11698 );
11699 events.push(HwMidiEvent {
11700 device: route.device.clone(),
11701 event: hw_event.event.clone(),
11702 });
11703 }
11704 }
11705 events.sort_by(|a, b| {
11706 a.event
11707 .frame
11708 .cmp(&b.event.frame)
11709 .then_with(|| a.device.cmp(&b.device))
11710 });
11711 events
11712 }
11713}
11714
11715#[cfg(test)]
11716mod tests {
11717 use super::*;
11718 use crate::mutex::UnsafeMutex;
11719 use tokio::sync::mpsc::channel;
11720 use tokio::time::{Duration as TokioDuration, timeout};
11721
11722 #[test]
11723 #[cfg(unix)]
11724 fn jack_transport_sync_decision_starts_and_syncs_position_on_external_play() {
11725 let decision = Engine::jack_transport_sync_decision(false, 128, true, 256, 64);
11726
11727 assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Start));
11728 assert_eq!(decision.position_sync, Some(256));
11729 }
11730
11731 #[test]
11732 #[cfg(unix)]
11733 fn jack_transport_sync_decision_stops_and_syncs_position_on_external_stop() {
11734 let decision = Engine::jack_transport_sync_decision(true, 512, false, 96, 64);
11735
11736 assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Stop));
11737 assert_eq!(decision.position_sync, Some(96));
11738 }
11739
11740 #[test]
11741 #[cfg(unix)]
11742 fn jack_transport_sync_decision_ignores_small_rolling_drift() {
11743 let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1040, 64);
11744
11745 assert_eq!(decision.play_sync, None);
11746 assert_eq!(decision.position_sync, None);
11747 }
11748
11749 #[test]
11750 #[cfg(unix)]
11751 fn jack_transport_sync_decision_syncs_large_rolling_jump() {
11752 let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1200, 64);
11753
11754 assert_eq!(decision.play_sync, None);
11755 assert_eq!(decision.position_sync, Some(1200));
11756 }
11757
11758 #[test]
11759 #[cfg(unix)]
11760 fn jack_transport_sync_decision_syncs_locate_while_stopped() {
11761 let decision = Engine::jack_transport_sync_decision(false, 400, false, 900, 64);
11762
11763 assert_eq!(decision.play_sync, None);
11764 assert_eq!(decision.position_sync, Some(900));
11765 }
11766
11767 fn make_engine_with_client() -> (Engine, tokio::sync::mpsc::Receiver<Message>) {
11768 let (engine_tx, engine_rx) = channel(16);
11769 let mut engine = Engine::new(engine_rx, engine_tx);
11770 let (client_tx, client_rx) = channel(16);
11771 engine.clients.push(client_tx);
11772 (engine, client_rx)
11773 }
11774
11775 fn insert_track(engine: &mut Engine, track: Track) {
11776 engine.state.lock().tracks.insert(
11777 track.name.clone(),
11778 Arc::new(UnsafeMutex::new(Box::new(track))),
11779 );
11780 }
11781
11782 fn osc_packet(address: &str) -> Vec<u8> {
11783 fn push_padded_osc_string(packet: &mut Vec<u8>, value: &str) {
11784 packet.extend_from_slice(value.as_bytes());
11785 packet.push(0);
11786 while !packet.len().is_multiple_of(4) {
11787 packet.push(0);
11788 }
11789 }
11790
11791 let mut packet = Vec::new();
11792 push_padded_osc_string(&mut packet, address);
11793 push_padded_osc_string(&mut packet, ",");
11794 packet
11795 }
11796
11797 #[tokio::test]
11798 async fn set_osc_enabled_starts_and_stops_server() {
11799 let (mut engine, _client_rx) = make_engine_with_client();
11800
11801 engine
11802 .set_osc_enabled_with(true, |tx| OscServer::start_on_addr(tx, "127.0.0.1:0"))
11803 .expect("start osc server on ephemeral port");
11804 assert!(engine.osc_server.is_some());
11805
11806 engine
11807 .set_osc_enabled_with(false, OscServer::start)
11808 .expect("stop osc server");
11809 assert!(engine.osc_server.is_none());
11810 }
11811
11812 #[tokio::test]
11813 async fn set_tempo_map_is_recorded_and_undone() {
11814 let (mut engine, _client_rx) = make_engine_with_client();
11815 let original_tempo_points = engine.tempo_points.clone();
11816 let original_time_signature_points = engine.time_signature_points.clone();
11817
11818 let new_tempo_points = vec![crate::message::TempoPoint {
11819 sample: 0,
11820 bpm: 140.0,
11821 }];
11822 let new_time_signature_points = vec![crate::message::TimeSignaturePoint {
11823 sample: 0,
11824 numerator: 3,
11825 denominator: 4,
11826 }];
11827
11828 engine
11829 .handle_request(Action::SetTempoMap {
11830 tempo_points: new_tempo_points.clone(),
11831 time_signature_points: new_time_signature_points.clone(),
11832 })
11833 .await;
11834
11835 assert_eq!(engine.tempo_points, new_tempo_points);
11836 assert_eq!(engine.time_signature_points, new_time_signature_points);
11837 assert_eq!(engine.tempo_bpm, 140.0);
11838 assert_eq!(engine.tsig_num, 3);
11839 assert_eq!(engine.tsig_denom, 4);
11840 assert!(engine.history.is_dirty());
11841
11842 engine.handle_request(Action::Undo).await;
11843
11844 assert_eq!(engine.tempo_points, original_tempo_points);
11845 assert_eq!(engine.time_signature_points, original_time_signature_points);
11846 assert_eq!(engine.tempo_bpm, 120.0);
11847 assert_eq!(engine.tsig_num, 4);
11848 assert_eq!(engine.tsig_denom, 4);
11849 }
11850
11851 #[tokio::test]
11852 async fn osc_server_forwards_transport_packets_to_engine_channel() {
11853 let (tx, mut rx) = channel(4);
11854 let mut server =
11855 OscServer::start_on_addr(tx, "127.0.0.1:0").expect("start osc test server");
11856 let socket = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind sender socket");
11857 let packet = osc_packet("/transport/play");
11858 socket
11859 .send_to(&packet, server.listen_addr())
11860 .expect("send osc packet");
11861
11862 let message = timeout(TokioDuration::from_secs(1), rx.recv())
11863 .await
11864 .expect("packet delivery timeout")
11865 .expect("osc message");
11866 match message {
11867 Message::OscRequest {
11868 action: Action::Play,
11869 ..
11870 } => {}
11871 other => panic!("unexpected osc message: {other:?}"),
11872 }
11873
11874 server.stop();
11875 }
11876
11877 #[tokio::test]
11878 async fn track_offline_bounce_rejects_zero_length_requests() {
11879 let (mut engine, mut client_rx) = make_engine_with_client();
11880 insert_track(
11881 &mut engine,
11882 Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
11883 );
11884
11885 engine
11886 .handle_request(Action::TrackOfflineBounce {
11887 track_name: "track".to_string(),
11888 output_path: "/tmp/out.wav".to_string(),
11889 start_sample: 0,
11890 length_samples: 0,
11891 automation_lanes: vec![],
11892 apply_fader: false,
11893 })
11894 .await;
11895
11896 match client_rx.recv().await.expect("response") {
11897 Message::Response(Err(err)) => {
11898 assert!(err.contains("has no renderable content for offline bounce"));
11899 }
11900 other => panic!("unexpected message: {other:?}"),
11901 }
11902 }
11903
11904 #[tokio::test]
11905 async fn track_offline_bounce_rejects_when_same_track_is_active() {
11906 let (mut engine, mut client_rx) = make_engine_with_client();
11907 engine.offline_bounce_jobs.insert(
11908 "other".to_string(),
11909 OfflineBounceJob {
11910 cancel: Arc::new(AtomicBool::new(false)),
11911 },
11912 );
11913
11914 engine
11915 .handle_request(Action::TrackOfflineBounce {
11916 track_name: "other".to_string(),
11917 output_path: "/tmp/out.wav".to_string(),
11918 start_sample: 0,
11919 length_samples: 128,
11920 automation_lanes: vec![],
11921 apply_fader: false,
11922 })
11923 .await;
11924
11925 match client_rx.recv().await.expect("response") {
11926 Message::Response(Err(err)) => {
11927 assert!(err.contains("already in progress"));
11928 }
11929 other => panic!("unexpected message: {other:?}"),
11930 }
11931 }
11932
11933 #[tokio::test]
11934 async fn track_offline_bounce_allows_different_track_concurrently() {
11935 let (mut engine, _client_rx) = make_engine_with_client();
11936 insert_track(
11937 &mut engine,
11938 Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
11939 );
11940 engine.offline_bounce_jobs.insert(
11941 "other".to_string(),
11942 OfflineBounceJob {
11943 cancel: Arc::new(AtomicBool::new(false)),
11944 },
11945 );
11946
11947 engine
11948 .handle_request(Action::TrackOfflineBounce {
11949 track_name: "track".to_string(),
11950 output_path: "/tmp/out.wav".to_string(),
11951 start_sample: 0,
11952 length_samples: 128,
11953 automation_lanes: vec![],
11954 apply_fader: false,
11955 })
11956 .await;
11957
11958 assert!(engine.offline_bounce_jobs.contains_key("other"));
11959 assert_eq!(engine.pending_requests.len(), 1);
11960 }
11961
11962 #[tokio::test]
11963 async fn reject_if_track_frozen_sends_error_and_blocks_operation() {
11964 let (mut engine, mut client_rx) = make_engine_with_client();
11965 let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
11966 track.set_frozen(true);
11967 insert_track(&mut engine, track);
11968
11969 let rejected = engine
11970 .reject_if_track_frozen("track", "arming/disarming")
11971 .await;
11972
11973 assert!(rejected);
11974 match client_rx.recv().await.expect("response") {
11975 Message::Response(Err(err)) => {
11976 assert_eq!(err, "Track 'track' is frozen; arming/disarming is blocked");
11977 }
11978 other => panic!("unexpected message: {other:?}"),
11979 }
11980 }
11981
11982 #[tokio::test]
11983 async fn undo_restores_original_clip_bounds_after_stretch_style_group() {
11984 let (mut engine, _client_rx) = make_engine_with_client();
11985 let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
11986 let mut clip = AudioClip::new("audio/original.wav".to_string(), 100, 220);
11987 clip.offset = 12;
11988 clip.fade_in_samples = 20;
11989 clip.fade_out_samples = 30;
11990 track.audio.clips.push(clip);
11991 insert_track(&mut engine, track);
11992
11993 engine.handle_request(Action::BeginHistoryGroup).await;
11994 engine
11995 .handle_request(Action::SetClipBounds {
11996 track_name: "track".to_string(),
11997 clip_index: 0,
11998 kind: Kind::Audio,
11999 start: 120,
12000 length: 180,
12001 offset: 0,
12002 })
12003 .await;
12004 engine
12005 .handle_request(Action::SetClipSourceName {
12006 track_name: "track".to_string(),
12007 clip_index: 0,
12008 kind: Kind::Audio,
12009 name: "audio/stretched.wav".to_string(),
12010 })
12011 .await;
12012 engine
12013 .handle_request(Action::SetClipFade {
12014 track_name: "track".to_string(),
12015 clip_index: 0,
12016 kind: Kind::Audio,
12017 fade_enabled: true,
12018 fade_in_samples: 12,
12019 fade_out_samples: 12,
12020 })
12021 .await;
12022 engine.handle_request(Action::EndHistoryGroup).await;
12023
12024 engine.handle_request(Action::Undo).await;
12025
12026 let state = engine.state.lock();
12027 let track = state.tracks.get("track").expect("track exists").lock();
12028 let clip = track.audio.clips.first().expect("clip exists");
12029 assert_eq!(clip.name, "audio/original.wav");
12030 assert_eq!(clip.start, 100);
12031 assert_eq!(clip.end, 220);
12032 assert_eq!(clip.end.saturating_sub(clip.start), 120);
12033 assert_eq!(clip.offset, 12);
12034 }
12035
12036 #[tokio::test]
12037 async fn track_offline_bounce_queues_when_no_worker_is_ready() {
12038 let (mut engine, _client_rx) = make_engine_with_client();
12039 insert_track(
12040 &mut engine,
12041 Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
12042 );
12043
12044 engine
12045 .handle_request(Action::TrackOfflineBounce {
12046 track_name: "track".to_string(),
12047 output_path: "/tmp/out.wav".to_string(),
12048 start_sample: 0,
12049 length_samples: 128,
12050 automation_lanes: vec![],
12051 apply_fader: false,
12052 })
12053 .await;
12054
12055 assert!(engine.offline_bounce_jobs.is_empty());
12056 assert_eq!(engine.pending_requests.len(), 1);
12057 assert!(matches!(
12058 engine.pending_requests.front(),
12059 Some(Action::TrackOfflineBounce { track_name, length_samples, .. })
12060 if track_name == "track" && *length_samples == 128
12061 ));
12062 }
12063
12064 #[tokio::test]
12065 async fn track_offline_bounce_returns_missing_track_error() {
12066 let (mut engine, mut client_rx) = make_engine_with_client();
12067
12068 engine
12069 .handle_request(Action::TrackOfflineBounce {
12070 track_name: "missing".to_string(),
12071 output_path: "/tmp/out.wav".to_string(),
12072 start_sample: 0,
12073 length_samples: 128,
12074 automation_lanes: vec![],
12075 apply_fader: false,
12076 })
12077 .await;
12078
12079 match client_rx.recv().await.expect("response") {
12080 Message::Response(Err(err)) => {
12081 assert_eq!(err, "Track not found: missing");
12082 }
12083 other => panic!("unexpected message: {other:?}"),
12084 }
12085 }
12086
12087 #[tokio::test]
12088 async fn track_offline_bounce_clears_job_when_worker_send_fails() {
12089 let (mut engine, mut client_rx) = make_engine_with_client();
12090 insert_track(
12091 &mut engine,
12092 Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
12093 );
12094 let (worker_tx, worker_rx) = channel(1);
12095 drop(worker_rx);
12096 engine
12097 .workers
12098 .push(WorkerData::new(worker_tx, tokio::spawn(async {})));
12099 engine.ready_workers.push(0);
12100
12101 engine
12102 .handle_request(Action::TrackOfflineBounce {
12103 track_name: "track".to_string(),
12104 output_path: "/tmp/out.wav".to_string(),
12105 start_sample: 0,
12106 length_samples: 128,
12107 automation_lanes: vec![],
12108 apply_fader: false,
12109 })
12110 .await;
12111
12112 assert!(engine.offline_bounce_jobs.is_empty());
12113 match client_rx.recv().await.expect("response") {
12114 Message::Response(Err(err)) => {
12115 assert!(err.contains("Failed to schedule offline bounce"));
12116 }
12117 other => panic!("unexpected message: {other:?}"),
12118 }
12119 }
12120
12121 #[tokio::test]
12122 async fn play_stop_play_keeps_clip_output_audible() {
12123 use crate::audio::clip::AudioClip;
12124 use crate::audio_codec::write_wav_f32;
12125
12126 let (engine_tx, engine_rx) = channel(16);
12127 let mut engine = Engine::new(engine_rx, engine_tx);
12128 let state = engine.state();
12129 let (client_tx, mut client_rx) = channel(16);
12130 engine.clients.push(client_tx);
12131 engine.init().await;
12132
12133 let tmp_dir = std::env::temp_dir().join("maolan_play_stop_play_test");
12134 let _ = std::fs::create_dir_all(&tmp_dir);
12135 let wav_path = tmp_dir.join("tone.wav");
12136 let sample_rate = 48_000u32;
12137 let clip_samples = sample_rate as usize;
12138 let mut samples = Vec::with_capacity(clip_samples);
12139 for i in 0..clip_samples {
12140 let phase = i as f32 / sample_rate as f32 * 2.0 * std::f32::consts::PI * 440.0;
12141 samples.push(phase.sin() * 0.5);
12142 }
12143 write_wav_f32(&wav_path, &samples, 1, sample_rate).expect("write wav");
12144
12145 let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 1024, sample_rate as f64);
12146 let mut clip = AudioClip::new(wav_path.to_string_lossy().to_string(), 0, clip_samples);
12147 clip.fade_enabled = false;
12148 track.audio.clips.push(clip);
12149 track.session_base_dir = Some(tmp_dir.clone());
12150 insert_track(&mut engine, track);
12151
12152 let tx = engine.tx.clone();
12153 let work_handle = tokio::spawn(async move {
12154 engine.work().await;
12155 });
12156
12157 tokio::time::sleep(TokioDuration::from_millis(100)).await;
12159
12160 async fn drain_responses(
12161 client_rx: &mut tokio::sync::mpsc::Receiver<Message>,
12162 count: usize,
12163 ) {
12164 for _ in 0..count {
12165 let _ = tokio::time::timeout(TokioDuration::from_secs(2), client_rx.recv()).await;
12166 }
12167 }
12168
12169 async fn wait_for_track_processed(
12170 client_rx: &mut tokio::sync::mpsc::Receiver<Message>,
12171 state: &Arc<UnsafeMutex<State>>,
12172 ) -> bool {
12173 let deadline = Instant::now() + Duration::from_secs(5);
12174 while Instant::now() < deadline {
12175 let msg =
12176 tokio::time::timeout(TokioDuration::from_millis(100), client_rx.recv()).await;
12177 if let Ok(Some(Message::Response(Ok(Action::TransportPosition(_)))))
12178 | Ok(Some(Message::Response(Ok(Action::Play)))) = msg
12179 {
12180 let track_deadline = Instant::now() + Duration::from_secs(5);
12181 while Instant::now() < track_deadline {
12182 if state
12183 .lock()
12184 .tracks
12185 .get("track")
12186 .map(|t| t.lock().audio.finished)
12187 .unwrap_or(false)
12188 {
12189 return true;
12190 }
12191 tokio::time::sleep(TokioDuration::from_millis(10)).await;
12192 }
12193 }
12194 }
12195 false
12196 }
12197
12198 tx.send(Message::Request(Action::SetClipPlaybackEnabled(true)))
12199 .await
12200 .unwrap();
12201 tx.send(Message::Request(Action::Play)).await.unwrap();
12202 assert!(
12203 wait_for_track_processed(&mut client_rx, &state).await,
12204 "track did not process on first play"
12205 );
12206 let first_peak = {
12207 let state = state.lock();
12208 let track = state.tracks.get("track").expect("track").lock();
12209 let input = track.audio.ins[0].buffer.lock();
12210 crate::simd::peak_abs(input)
12211 };
12212 assert!(
12213 first_peak > 0.001,
12214 "expected audible input on first play, got {first_peak}"
12215 );
12216
12217 tx.send(Message::Request(Action::SetClipPlaybackEnabled(true)))
12218 .await
12219 .unwrap();
12220 tx.send(Message::Request(Action::Stop)).await.unwrap();
12221 drain_responses(&mut client_rx, 2).await;
12222
12223 tx.send(Message::Request(Action::SetClipPlaybackEnabled(true)))
12224 .await
12225 .unwrap();
12226 tx.send(Message::Request(Action::Play)).await.unwrap();
12227 assert!(
12228 wait_for_track_processed(&mut client_rx, &state).await,
12229 "track did not process on second play"
12230 );
12231 let second_peak = {
12232 let state = state.lock();
12233 let track = state.tracks.get("track").expect("track").lock();
12234 let input = track.audio.ins[0].buffer.lock();
12235 crate::simd::peak_abs(input)
12236 };
12237 assert!(
12238 second_peak > 0.001,
12239 "expected audible input on second play after stop, got {second_peak}"
12240 );
12241
12242 let _ = tx.send(Message::Request(Action::Quit)).await;
12243 tokio::time::sleep(TokioDuration::from_millis(200)).await;
12244 work_handle.abort();
12245 let _ = std::fs::remove_dir_all(&tmp_dir);
12246 }
12247
12248 #[test]
12249 fn modulator_sets_track_volume() {
12250 let (mut engine, _client_rx) = make_engine_with_client();
12251 let track = Track::new("vol-track".to_string(), 0, 2, 0, 0, 128, 48_000.0);
12252 insert_track(&mut engine, track);
12253
12254 engine.modulators = vec![crate::modulator::Modulator {
12255 id: 1,
12256 name: "LFO".to_string(),
12257 shape: crate::modulator::ModulatorShape::Sine,
12258 rate: crate::modulator::ModulatorRate::Hz(1.0),
12259 phase: 0.0,
12260 enabled: true,
12261 targets: vec![crate::modulator::ModulatorTarget::TrackVolume {
12262 track_name: "vol-track".to_string(),
12263 min: -90.0,
12264 max: 20.0,
12265 }],
12266 }];
12267
12268 let echoes = engine.apply_modulators(12_000);
12270 let track = engine.state.lock().tracks["vol-track"].lock();
12271 assert!(
12272 (track.level() - 20.0).abs() < 0.01,
12273 "expected 20 dB, got {}",
12274 track.level()
12275 );
12276 assert!(
12277 echoes
12278 .iter()
12279 .any(|a| matches!(a, Action::TrackAutomationLevel(name, _) if name == "vol-track"))
12280 );
12281 }
12282
12283 #[test]
12284 fn modulator_sets_track_balance() {
12285 let (mut engine, _client_rx) = make_engine_with_client();
12286 let track = Track::new("pan-track".to_string(), 0, 2, 0, 0, 128, 48_000.0);
12287 insert_track(&mut engine, track);
12288
12289 engine.modulators = vec![crate::modulator::Modulator {
12290 id: 1,
12291 name: "LFO".to_string(),
12292 shape: crate::modulator::ModulatorShape::Sine,
12293 rate: crate::modulator::ModulatorRate::Hz(1.0),
12294 phase: 0.0,
12295 enabled: true,
12296 targets: vec![crate::modulator::ModulatorTarget::TrackBalance {
12297 track_name: "pan-track".to_string(),
12298 min: -1.0,
12299 max: 1.0,
12300 }],
12301 }];
12302
12303 let echoes = engine.apply_modulators(12_000);
12305 let track = engine.state.lock().tracks["pan-track"].lock();
12306 assert!(
12307 (track.balance - 1.0).abs() < 0.01,
12308 "expected balance 1.0, got {}",
12309 track.balance
12310 );
12311 assert!(
12312 echoes.iter().any(
12313 |a| matches!(a, Action::TrackAutomationBalance(name, _) if name == "pan-track")
12314 )
12315 );
12316 }
12317
12318 #[tokio::test]
12319 async fn track_set_parent_wires_folder_input_to_child_input_and_child_output_to_folder_output()
12320 {
12321 let (mut engine, mut client_rx) = make_engine_with_client();
12322 let folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
12323 let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
12324 insert_track(&mut engine, folder);
12325 insert_track(&mut engine, child);
12326
12327 engine
12328 .handle_request_inner(
12329 Action::TrackSetParent {
12330 track_name: "child".to_string(),
12331 parent_name: Some("folder".to_string()),
12332 },
12333 false,
12334 )
12335 .await;
12336
12337 while let Ok(Some(_)) =
12339 tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
12340 {}
12341
12342 let state = engine.state.lock();
12343 let folder = state.tracks.get("folder").unwrap().lock();
12344 let child = state.tracks.get("child").unwrap().lock();
12345
12346 assert!(folder.child_tracks.iter().any(|c| c.lock().name == "child"));
12347 assert_eq!(child.parent_track.as_deref(), Some("folder"));
12348
12349 for (i, (parent_in, child_in)) in folder.audio.ins.iter().zip(&child.audio.ins).enumerate()
12351 {
12352 assert!(
12353 child_in
12354 .connections
12355 .lock()
12356 .iter()
12357 .any(|c| Arc::ptr_eq(c, parent_in)),
12358 "folder input {i} is not routed to child input {i}"
12359 );
12360 assert!(
12361 !parent_in
12362 .connections
12363 .lock()
12364 .iter()
12365 .any(|c| Arc::ptr_eq(c, child_in)),
12366 "folder input {i} should not read from child input {i}"
12367 );
12368 }
12369
12370 for (i, (child_out, parent_out)) in
12372 child.audio.outs.iter().zip(&folder.audio.outs).enumerate()
12373 {
12374 assert!(
12375 parent_out
12376 .connections
12377 .lock()
12378 .iter()
12379 .any(|c| Arc::ptr_eq(c, child_out)),
12380 "child output {i} is not routed to folder output {i}"
12381 );
12382 }
12383
12384 for (i, child_out) in child.audio.outs.iter().enumerate() {
12386 assert!(
12387 child_out.connections.lock().iter().any(|c| {
12388 child
12389 .audio
12390 .ins
12391 .get(i)
12392 .is_some_and(|inp| Arc::ptr_eq(c, inp))
12393 }),
12394 "child output {i} is not connected to child input {i}"
12395 );
12396 }
12397 }
12398
12399 #[tokio::test]
12400 async fn track_set_parent_to_none_restores_root_passthrough() {
12401 let (mut engine, mut client_rx) = make_engine_with_client();
12402 let folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
12403 let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
12404 insert_track(&mut engine, folder);
12405 insert_track(&mut engine, child);
12406
12407 engine
12408 .handle_request_inner(
12409 Action::TrackSetParent {
12410 track_name: "child".to_string(),
12411 parent_name: Some("folder".to_string()),
12412 },
12413 false,
12414 )
12415 .await;
12416 engine
12417 .handle_request_inner(
12418 Action::TrackSetParent {
12419 track_name: "child".to_string(),
12420 parent_name: None,
12421 },
12422 false,
12423 )
12424 .await;
12425
12426 while let Ok(Some(_)) =
12427 tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
12428 {}
12429
12430 let state = engine.state.lock();
12431 let folder = state.tracks.get("folder").unwrap().lock();
12432 let child = state.tracks.get("child").unwrap().lock();
12433
12434 assert!(folder.child_tracks.is_empty());
12435 assert!(child.parent_track.is_none());
12436
12437 for (i, child_out) in child.audio.outs.iter().enumerate() {
12438 assert!(
12439 child_out.connections.lock().iter().any(|c| {
12440 child
12441 .audio
12442 .ins
12443 .get(i)
12444 .is_some_and(|inp| Arc::ptr_eq(c, inp))
12445 }),
12446 "child output {i} should be connected to child input {i} after moving to root"
12447 );
12448 }
12449 }
12450
12451 #[tokio::test]
12452 async fn track_set_parent_wires_folder_midi_to_child_midi() {
12453 let (mut engine, mut client_rx) = make_engine_with_client();
12454 let folder = Track::new_folder("folder".to_string(), 0, 0, 1, 1, 64, 48_000.0);
12455 let child = Track::new("child".to_string(), 0, 0, 1, 1, 64, 48_000.0);
12456 insert_track(&mut engine, folder);
12457 insert_track(&mut engine, child);
12458
12459 engine
12460 .handle_request_inner(
12461 Action::TrackSetParent {
12462 track_name: "child".to_string(),
12463 parent_name: Some("folder".to_string()),
12464 },
12465 false,
12466 )
12467 .await;
12468
12469 while let Ok(Some(_)) =
12470 tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
12471 {}
12472
12473 let state = engine.state.lock();
12474 let folder = state.tracks.get("folder").unwrap().lock();
12475 let child = state.tracks.get("child").unwrap().lock();
12476
12477 let folder_midi_in = &folder.midi.ins[0];
12478 let child_midi_in = &child.midi.ins[0];
12479 assert!(
12480 child_midi_in
12481 .lock()
12482 .connections
12483 .iter()
12484 .any(|c| Arc::ptr_eq(c, folder_midi_in)),
12485 "folder MIDI input should be routed to child MIDI input"
12486 );
12487
12488 let child_midi_out = &child.midi.outs[0];
12489 let folder_midi_out = &folder.midi.outs[0];
12490 assert!(
12491 child_midi_out
12492 .lock()
12493 .connections
12494 .iter()
12495 .any(|c| Arc::ptr_eq(c, folder_midi_out)),
12496 "child MIDI output should be routed to folder MIDI output"
12497 );
12498 }
12499
12500 #[test]
12501 fn nested_folder_expands_in_task_graph() {
12502 let (mut engine, _client_rx) = make_engine_with_client();
12503 let outer = Track::new_folder("outer".to_string(), 2, 2, 0, 0, 64, 48_000.0);
12504 let inner = Track::new_folder("inner".to_string(), 2, 2, 0, 0, 64, 48_000.0);
12505 let leaf = Track::new("leaf".to_string(), 2, 2, 0, 0, 64, 48_000.0);
12506 insert_track(&mut engine, outer);
12507 insert_track(&mut engine, inner);
12508 insert_track(&mut engine, leaf);
12509
12510 {
12511 let state = engine.state.lock();
12512 let outer = state.tracks.get("outer").unwrap().clone();
12513 let inner = state.tracks.get("inner").unwrap().clone();
12514 let leaf = state.tracks.get("leaf").unwrap().clone();
12515 outer.lock().child_tracks.push(inner.clone());
12516 inner.lock().child_tracks.push(leaf.clone());
12517 inner.lock().parent_track = Some("outer".to_string());
12518 leaf.lock().parent_track = Some("inner".to_string());
12519 }
12520
12521 let (tasks, deps) = engine.build_task_graph();
12522 let names: Vec<String> = tasks
12523 .iter()
12524 .map(|t| match t {
12525 ProcessTask::Track(t) => format!("track:{}", t.lock().name.clone()),
12526 ProcessTask::FolderInput(t) => format!("in:{}", t.lock().name.clone()),
12527 ProcessTask::FolderOutput(t) => format!("out:{}", t.lock().name.clone()),
12528 ProcessTask::Plugin { track, .. } => {
12529 format!("plugin:{}", track.lock().name.clone())
12530 }
12531 })
12532 .collect();
12533
12534 let expected = vec![
12535 "in:outer",
12536 "in:inner",
12537 "track:leaf",
12538 "out:inner",
12539 "out:outer",
12540 ];
12541 assert_eq!(names, expected, "task graph should expand nested folders");
12542
12543 for window in tasks.windows(2) {
12545 let prev = &window[0];
12546 let next = &window[1];
12547 let prev_key = Engine::task_key(prev);
12548 let next_key = Engine::task_key(next);
12549 assert!(
12550 deps.get(&next_key).is_some_and(|d| d.contains(&prev_key)),
12551 "{:?} should depend on {:?}",
12552 next,
12553 prev
12554 );
12555 }
12556 }
12557
12558 #[test]
12559 fn child_to_plugin_to_folder_output_task_graph_has_no_cycle() {
12560 use crate::message::ConnectableRef;
12561
12562 let plugin_path = Path::new(env!("CARGO_MANIFEST_DIR"))
12563 .parent()
12564 .unwrap()
12565 .join("daw")
12566 .join("plugin-host")
12567 .join("tests")
12568 .join("test_passthrough.clap");
12569 if !plugin_path.exists() {
12570 return;
12571 }
12572 if crate::plugins::ipc::find_plugin_host_binary().is_none() {
12573 return;
12574 }
12575
12576 let (mut engine, _client_rx) = make_engine_with_client();
12577 let mut folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
12578 let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
12579
12580 folder
12581 .load_clap_plugin(
12582 &format!("{}::com.maolan.test.passthrough", plugin_path.display()),
12583 None,
12584 )
12585 .expect("should load CLAP plugin on folder");
12586 folder.clap_plugins[0].processor.lock().setup_audio_ports();
12587 let plugin_id = folder.clap_plugins[0].id;
12588
12589 insert_track(&mut engine, folder);
12590 insert_track(&mut engine, child);
12591
12592 {
12593 let state = engine.state.lock();
12594 let folder = state.tracks.get("folder").unwrap().clone();
12595 let child = state.tracks.get("child").unwrap().clone();
12596 folder.lock().child_tracks.push(child.clone());
12597 child.lock().parent_track = Some("folder".to_string());
12598
12599 folder
12600 .lock()
12601 .connect_audio_connectable(
12602 ConnectableRef::ChildTrack("child".to_string()),
12603 0,
12604 ConnectableRef::ClapPlugin(plugin_id),
12605 0,
12606 )
12607 .expect("connect child L to plugin L");
12608 folder
12609 .lock()
12610 .connect_audio_connectable(
12611 ConnectableRef::ChildTrack("child".to_string()),
12612 1,
12613 ConnectableRef::ClapPlugin(plugin_id),
12614 1,
12615 )
12616 .expect("connect child R to plugin R");
12617 folder
12618 .lock()
12619 .connect_audio_connectable(
12620 ConnectableRef::ClapPlugin(plugin_id),
12621 0,
12622 ConnectableRef::TrackOutput,
12623 0,
12624 )
12625 .expect("connect plugin L to folder output L");
12626 folder
12627 .lock()
12628 .connect_audio_connectable(
12629 ConnectableRef::ClapPlugin(plugin_id),
12630 1,
12631 ConnectableRef::TrackOutput,
12632 1,
12633 )
12634 .expect("connect plugin R to folder output R");
12635 }
12636
12637 let (tasks, deps) = engine.build_task_graph();
12638
12639 let folder_in_key = tasks
12640 .iter()
12641 .find(|t| matches!(t, ProcessTask::FolderInput(t) if t.lock().name == "folder"))
12642 .map(Engine::task_key)
12643 .expect("folder input task");
12644 let child_key = tasks
12645 .iter()
12646 .find(|t| matches!(t, ProcessTask::Track(t) if t.lock().name == "child"))
12647 .map(Engine::task_key)
12648 .expect("child task");
12649 let plugin_key = tasks
12650 .iter()
12651 .find(|t| {
12652 matches!(
12653 t,
12654 ProcessTask::Plugin {
12655 track,
12656 kind: PluginKind::Clap,
12657 index: 0,
12658 } if track.lock().name == "folder"
12659 )
12660 })
12661 .map(Engine::task_key)
12662 .expect("plugin task");
12663 let folder_out_key = tasks
12664 .iter()
12665 .find(|t| matches!(t, ProcessTask::FolderOutput(t) if t.lock().name == "folder"))
12666 .map(Engine::task_key)
12667 .expect("folder output task");
12668
12669 assert!(
12670 deps.get(&child_key)
12671 .is_some_and(|d| d.contains(&folder_in_key)),
12672 "child task should depend on folder input"
12673 );
12674 assert!(
12675 deps.get(&plugin_key)
12676 .is_some_and(|d| d.contains(&folder_in_key) && d.contains(&child_key)),
12677 "plugin task should depend on folder input and child"
12678 );
12679 assert!(
12680 deps.get(&folder_out_key).is_some_and(|d| {
12681 d.contains(&folder_in_key) && d.contains(&plugin_key) && d.contains(&child_key)
12682 }),
12683 "folder output should depend on folder input, plugin, and child"
12684 );
12685
12686 fn has_cycle(deps: &HashMap<String, Vec<String>>) -> bool {
12687 let mut state: HashMap<String, u8> = HashMap::new();
12688 fn visit(
12689 node: &str,
12690 deps: &HashMap<String, Vec<String>>,
12691 state: &mut HashMap<String, u8>,
12692 ) -> bool {
12693 match state.get(node).copied() {
12694 Some(1) => return true,
12695 Some(2) => return false,
12696 _ => {}
12697 }
12698 state.insert(node.to_string(), 1);
12699 for next in deps.get(node).into_iter().flatten() {
12700 if visit(next, deps, state) {
12701 return true;
12702 }
12703 }
12704 state.insert(node.to_string(), 2);
12705 false
12706 }
12707 for node in deps.keys() {
12708 if visit(node, deps, &mut state) {
12709 return true;
12710 }
12711 }
12712 false
12713 }
12714
12715 assert!(
12716 !has_cycle(&deps),
12717 "task graph should not contain a cycle when a plugin reads from a child track"
12718 );
12719 }
12720
12721 #[tokio::test]
12722 async fn track_set_parent_wires_child_io_to_folder_even_after_addtrack() {
12723 let (mut engine, mut client_rx) = make_engine_with_client();
12724 let folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
12725 let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
12726 insert_track(&mut engine, folder);
12727 insert_track(&mut engine, child);
12728
12729 engine
12730 .handle_request_inner(
12731 Action::TrackSetParent {
12732 track_name: "child".to_string(),
12733 parent_name: Some("folder".to_string()),
12734 },
12735 false,
12736 )
12737 .await;
12738
12739 while let Ok(Some(_)) =
12740 tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
12741 {}
12742
12743 let state = engine.state.lock();
12744 let folder = state.tracks.get("folder").unwrap().lock();
12745 let child = state.tracks.get("child").unwrap().lock();
12746
12747 for (i, (parent_in, child_in)) in folder.audio.ins.iter().zip(&child.audio.ins).enumerate()
12749 {
12750 assert!(
12751 child_in
12752 .connections
12753 .lock()
12754 .iter()
12755 .any(|c| Arc::ptr_eq(c, parent_in)),
12756 "folder input {i} is not routed to child input {i}"
12757 );
12758 }
12759
12760 for (i, (child_out, parent_out)) in
12762 child.audio.outs.iter().zip(&folder.audio.outs).enumerate()
12763 {
12764 assert!(
12765 parent_out
12766 .connections
12767 .lock()
12768 .iter()
12769 .any(|c| Arc::ptr_eq(c, child_out)),
12770 "child output {i} is not routed to folder output {i}"
12771 );
12772 }
12773 }
12774
12775 #[tokio::test]
12776 async fn folder_child_audio_passes_through() {
12777 let (mut engine, mut client_rx) = make_engine_with_client();
12778 let folder = Track::new_folder("folder".to_string(), 1, 1, 0, 0, 64, 48_000.0);
12779 let child = Track::new("child".to_string(), 1, 1, 0, 0, 64, 48_000.0);
12780 insert_track(&mut engine, folder);
12781 insert_track(&mut engine, child);
12782
12783 engine
12784 .handle_request_inner(
12785 Action::TrackSetParent {
12786 track_name: "child".to_string(),
12787 parent_name: Some("folder".to_string()),
12788 },
12789 false,
12790 )
12791 .await;
12792 while let Ok(Some(_)) =
12793 tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
12794 {}
12795
12796 {
12797 let state = engine.state.lock();
12798 let folder = state.tracks.get("folder").unwrap().clone();
12799 let child = state.tracks.get("child").unwrap().clone();
12800
12801 folder.lock().input_monitor = vec![true];
12802 child.lock().input_monitor = vec![true];
12803
12804 let source = Arc::new(crate::audio::io::AudioIO::new(64));
12806 for sample in source.buffer.lock().iter_mut() {
12807 *sample = 0.75;
12808 }
12809 crate::audio::io::AudioIO::connect(&source, &folder.lock().audio.ins[0]);
12810
12811 folder.lock().process_folder_input();
12812 child.lock().process();
12813 folder.lock().process_folder_output();
12814
12815 let output = folder.lock().audio.outs[0].buffer.lock();
12816 assert!(
12817 output.iter().any(|s| (*s - 0.75).abs() < 1e-5),
12818 "folder output should contain the child-processed folder input signal, got {:?}",
12819 output.iter().take(8).collect::<Vec<_>>()
12820 );
12821 }
12822 }
12823
12824 #[tokio::test]
12825 async fn remove_folder_track_deletes_descendants_recursively() {
12826 let (mut engine, mut client_rx) = make_engine_with_client();
12827 let folder = Track::new_folder("folder".to_string(), 1, 1, 0, 0, 64, 48_000.0);
12828 let child = Track::new_folder("child".to_string(), 1, 1, 0, 0, 64, 48_000.0);
12829 let grandchild = Track::new("grandchild".to_string(), 1, 1, 0, 0, 64, 48_000.0);
12830 insert_track(&mut engine, folder);
12831 insert_track(&mut engine, child);
12832 insert_track(&mut engine, grandchild);
12833
12834 engine
12835 .handle_request(Action::TrackSetParent {
12836 track_name: "child".to_string(),
12837 parent_name: Some("folder".to_string()),
12838 })
12839 .await;
12840 engine
12841 .handle_request(Action::TrackSetParent {
12842 track_name: "grandchild".to_string(),
12843 parent_name: Some("child".to_string()),
12844 })
12845 .await;
12846
12847 while let Ok(Some(_)) =
12849 tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
12850 {}
12851
12852 engine
12853 .handle_request(Action::RemoveTrack("folder".to_string()))
12854 .await;
12855
12856 {
12857 let state = engine.state.lock();
12858 assert!(
12859 !state.tracks.contains_key("folder"),
12860 "folder should have been removed"
12861 );
12862 assert!(
12863 !state.tracks.contains_key("child"),
12864 "child should have been removed"
12865 );
12866 assert!(
12867 !state.tracks.contains_key("grandchild"),
12868 "grandchild should have been removed"
12869 );
12870 }
12871
12872 let mut removed_names = Vec::new();
12873 for _ in 0..3 {
12874 let msg = tokio::time::timeout(TokioDuration::from_millis(100), client_rx.recv()).await;
12875 if let Ok(Some(Message::Response(Ok(Action::RemoveTrack(name))))) = msg {
12876 removed_names.push(name);
12877 }
12878 }
12879 assert_eq!(
12880 removed_names,
12881 vec!["grandchild", "child", "folder"],
12882 "descendants should be removed before the folder and clients notified"
12883 );
12884 }
12885
12886 #[tokio::test]
12887 async fn track_set_folder_rejects_master_track() {
12888 let (mut engine, mut client_rx) = make_engine_with_client();
12889 let mut track = Track::new("master".to_string(), 2, 2, 0, 0, 64, 48_000.0);
12890 track.is_master = true;
12891 insert_track(&mut engine, track);
12892
12893 engine
12894 .handle_request_inner(
12895 Action::TrackSetFolder {
12896 track_name: "master".to_string(),
12897 is_folder: true,
12898 },
12899 false,
12900 )
12901 .await;
12902
12903 {
12904 let state = engine.state.lock();
12905 assert!(!state.tracks.get("master").unwrap().lock().is_folder);
12906 }
12907
12908 let msg = tokio::time::timeout(TokioDuration::from_millis(100), client_rx.recv()).await;
12909 assert!(
12910 matches!(msg, Ok(Some(Message::Response(Err(_))))),
12911 "master track folder conversion should report an error"
12912 );
12913 }
12914
12915 #[tokio::test]
12916 async fn track_toggle_master_ignored_for_folder_track() {
12917 let (mut engine, mut client_rx) = make_engine_with_client();
12918 let folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
12919 insert_track(&mut engine, folder);
12920
12921 engine
12922 .handle_request_inner(Action::TrackToggleMaster("folder".to_string()), false)
12923 .await;
12924
12925 {
12926 let state = engine.state.lock();
12927 assert!(!state.tracks.get("folder").unwrap().lock().is_master);
12928 }
12929
12930 let msg = tokio::time::timeout(TokioDuration::from_millis(100), client_rx.recv()).await;
12931 assert!(
12932 matches!(
12933 msg,
12934 Ok(Some(Message::Response(Ok(Action::TrackToggleMaster(ref name)))))
12935 if name == "folder"
12936 ),
12937 "folder track master toggle should still be echoed to clients: {msg:?}"
12938 );
12939 }
12940}