1use super::*;
2#[cfg(target_os = "linux")]
3use crate::hw::alsa::{HwDriver, HwOptions, MidiHub};
4#[cfg(target_os = "macos")]
5use crate::hw::coreaudio::{HwDriver, HwOptions, MidiHub};
6#[cfg(target_os = "windows")]
7use crate::hw::options::HwOptions;
8#[cfg(target_os = "openbsd")]
9use crate::hw::sndio::{HwDriver, HwOptions, MidiHub};
10#[cfg(target_os = "windows")]
11use crate::hw::wasapi::{self, HwDriver, MidiHub};
12#[cfg(target_os = "linux")]
13use crate::workers::alsa_worker::HwWorker;
14#[cfg(target_os = "macos")]
15use crate::workers::coreaudio_worker::HwWorker;
16#[cfg(target_os = "openbsd")]
17use crate::workers::sndio_worker::HwWorker;
18#[cfg(target_os = "windows")]
19use crate::workers::wasapi_worker::HwWorker;
20use crate::{
21 audio::clip::AudioClip,
22 audio::io::AudioIO,
23 history::{UndoEntry, create_inverse_actions, should_record},
24 kind::Kind,
25 message::Action,
26 midi::clip::MIDIClip,
27 midi::io::{MIDIIO, MidiEvent},
28 routing,
29 track::Track,
30};
31use std::sync::Arc;
32use std::sync::atomic::Ordering;
33
34impl Engine {
35 pub(crate) fn is_track_frozen(&self, track_name: &str) -> bool {
36 self.state
37 .lock()
38 .tracks
39 .get(track_name)
40 .map(|track| track.lock().frozen())
41 .unwrap_or(false)
42 }
43
44 pub(crate) async fn reject_if_track_frozen(
45 &mut self,
46 track_name: &str,
47 operation: &str,
48 ) -> bool {
49 if self.is_track_frozen(track_name) {
50 self.notify_clients(Err(format!(
51 "Track '{track_name}' is frozen; {operation} is blocked"
52 )))
53 .await;
54 true
55 } else {
56 false
57 }
58 }
59
60 #[cfg(unix)]
61 pub(crate) fn audio_ports_connected(source: &Arc<AudioIO>, target: &Arc<AudioIO>) -> bool {
62 source
63 .connections()
64 .iter()
65 .any(|conn| Arc::ptr_eq(conn, target))
66 }
67
68 pub(crate) fn resolve_audio_route_ports(
69 &self,
70 from_track: &str,
71 from_port: usize,
72 to_track: &str,
73 to_port: usize,
74 ) -> (Option<Arc<AudioIO>>, Option<Arc<AudioIO>>) {
75 let state = self.state.lock();
76 let from_is_child_of_to = state
77 .tracks
78 .get(from_track)
79 .and_then(|t| t.lock().parent_track.clone())
80 .as_deref()
81 == Some(to_track);
82 let to_is_child_of_from = state
83 .tracks
84 .get(to_track)
85 .and_then(|t| t.lock().parent_track.clone())
86 .as_deref()
87 == Some(from_track);
88
89 let from_audio_io = if from_track == "hw:in" {
90 self.hw_input_audio_port(from_port)
91 } else {
92 state.tracks.get(from_track).and_then(|t| {
93 let t = t.lock();
94 if t.is_folder {
95 if to_is_child_of_from {
96 t.audio.ins.get(from_port).cloned()
98 } else {
99 t.audio.outs.get(from_port).cloned()
101 }
102 } else {
103 t.audio.outs.get(from_port).cloned()
104 }
105 })
106 };
107 let to_audio_io = if to_track == "hw:out" {
108 self.hw_output_audio_port(to_port)
109 } else {
110 state.tracks.get(to_track).and_then(|t| {
111 let t = t.lock();
112 if t.is_folder {
113 if from_is_child_of_to {
114 t.audio.outs.get(to_port).cloned()
116 } else {
117 t.audio.ins.get(to_port).cloned()
119 }
120 } else {
121 t.audio.ins.get(to_port).cloned()
122 }
123 })
124 };
125 (from_audio_io, to_audio_io)
126 }
127
128 pub(crate) async fn disconnect_audio_route_and_notify(
129 &mut self,
130 action: Action,
131 ) -> Result<(), String> {
132 let Action::Disconnect {
133 from_track,
134 from_port,
135 to_track,
136 to_port,
137 kind,
138 } = &action
139 else {
140 return Err("disconnect_audio_route_and_notify requires Disconnect action".to_string());
141 };
142 if *kind != Kind::Audio {
143 return Err("disconnect_audio_route_and_notify only supports audio routes".to_string());
144 }
145 let (from_audio_io, to_audio_io) =
146 self.resolve_audio_route_ports(from_track, *from_port, to_track, *to_port);
147 match (from_audio_io, to_audio_io) {
148 (Some(source), Some(target)) => {
149 crate::audio::io::AudioIO::disconnect(&source, &target)
150 .map_err(|e| format!("Disconnect failed: {e}"))?;
151 self.notify_clients(Ok(action)).await;
152 Ok(())
153 }
154 _ => Err(format!(
155 "Disconnect failed: Port not found ({} -> {})",
156 from_track, to_track
157 )),
158 }
159 }
160
161 #[cfg(unix)]
162 pub(crate) fn disconnect_actions_for_removed_hw_input(
163 &self,
164 removed_port: usize,
165 removed_io: &Arc<AudioIO>,
166 ) -> Vec<Action> {
167 let mut actions = Vec::new();
168 {
169 let state = self.state.lock();
170 for (track_name, track) in &state.tracks {
171 let track = track.lock();
172 for (to_port, target) in track.audio.ins.iter().enumerate() {
173 if Self::audio_ports_connected(removed_io, target) {
174 actions.push(Action::Disconnect {
175 from_track: "hw:in".to_string(),
176 from_port: removed_port,
177 to_track: track_name.clone(),
178 to_port,
179 kind: Kind::Audio,
180 });
181 }
182 }
183 }
184 }
185 for (to_port, target) in self.all_hw_output_audio_ports().into_iter().enumerate() {
186 if Self::audio_ports_connected(removed_io, &target) {
187 actions.push(Action::Disconnect {
188 from_track: "hw:in".to_string(),
189 from_port: removed_port,
190 to_track: "hw:out".to_string(),
191 to_port,
192 kind: Kind::Audio,
193 });
194 }
195 }
196 actions
197 }
198
199 #[cfg(unix)]
200 pub(crate) fn disconnect_actions_for_removed_hw_output(
201 &self,
202 removed_port: usize,
203 removed_io: &Arc<AudioIO>,
204 ) -> Vec<Action> {
205 let mut actions = Vec::new();
206 {
207 let state = self.state.lock();
208 for (track_name, track) in &state.tracks {
209 let track = track.lock();
210 for (from_port, source) in track.audio.outs.iter().enumerate() {
211 if Self::audio_ports_connected(source, removed_io) {
212 actions.push(Action::Disconnect {
213 from_track: track_name.clone(),
214 from_port,
215 to_track: "hw:out".to_string(),
216 to_port: removed_port,
217 kind: Kind::Audio,
218 });
219 }
220 }
221 }
222 }
223 #[cfg(unix)]
224 if let Some(jack) = &self.jack_runtime {
225 for (from_port, source) in jack.audio_ins().into_iter().enumerate() {
226 if Self::audio_ports_connected(&source, removed_io) {
227 actions.push(Action::Disconnect {
228 from_track: "hw:in".to_string(),
229 from_port,
230 to_track: "hw:out".to_string(),
231 to_port: removed_port,
232 kind: Kind::Audio,
233 });
234 }
235 }
236 }
237 actions
238 }
239
240 #[cfg(unix)]
241 pub(crate) fn reindex_notifications_for_removed_hw_input(
242 &self,
243 removed_port: usize,
244 ) -> Vec<Action> {
245 let mut actions = Vec::new();
246 #[cfg(unix)]
247 if let Some(jack) = &self.jack_runtime {
248 for from_port in (removed_port + 1)..jack.input_channels() {
249 let Some(source) = jack.input_audio_port(from_port) else {
250 continue;
251 };
252 {
253 let state = self.state.lock();
254 for (track_name, track) in &state.tracks {
255 let track = track.lock();
256 for (to_port, target) in track.audio.ins.iter().enumerate() {
257 if Self::audio_ports_connected(&source, target) {
258 actions.push(Action::Disconnect {
259 from_track: "hw:in".to_string(),
260 from_port,
261 to_track: track_name.clone(),
262 to_port,
263 kind: Kind::Audio,
264 });
265 actions.push(Action::Connect {
266 from_track: "hw:in".to_string(),
267 from_port: from_port - 1,
268 to_track: track_name.clone(),
269 to_port,
270 kind: Kind::Audio,
271 });
272 }
273 }
274 }
275 }
276 for (to_port, target) in self.all_hw_output_audio_ports().into_iter().enumerate() {
277 if Self::audio_ports_connected(&source, &target) {
278 actions.push(Action::Disconnect {
279 from_track: "hw:in".to_string(),
280 from_port,
281 to_track: "hw:out".to_string(),
282 to_port,
283 kind: Kind::Audio,
284 });
285 actions.push(Action::Connect {
286 from_track: "hw:in".to_string(),
287 from_port: from_port - 1,
288 to_track: "hw:out".to_string(),
289 to_port,
290 kind: Kind::Audio,
291 });
292 }
293 }
294 }
295 }
296 actions
297 }
298
299 #[cfg(unix)]
300 pub(crate) fn reindex_notifications_for_removed_hw_output(
301 &self,
302 removed_port: usize,
303 ) -> Vec<Action> {
304 let mut actions = Vec::new();
305 #[cfg(unix)]
306 if let Some(jack) = &self.jack_runtime {
307 for to_port in (removed_port + 1)..jack.output_channels() {
308 let Some(target) = jack.output_audio_port(to_port) else {
309 continue;
310 };
311 {
312 let state = self.state.lock();
313 for (track_name, track) in &state.tracks {
314 let track = track.lock();
315 for (from_port, source) in track.audio.outs.iter().enumerate() {
316 if Self::audio_ports_connected(source, &target) {
317 actions.push(Action::Disconnect {
318 from_track: track_name.clone(),
319 from_port,
320 to_track: "hw:out".to_string(),
321 to_port,
322 kind: Kind::Audio,
323 });
324 actions.push(Action::Connect {
325 from_track: track_name.clone(),
326 from_port,
327 to_track: "hw:out".to_string(),
328 to_port: to_port - 1,
329 kind: Kind::Audio,
330 });
331 }
332 }
333 }
334 }
335 for (from_port, source) in jack.audio_ins().into_iter().enumerate() {
336 if Self::audio_ports_connected(&source, &target) {
337 actions.push(Action::Disconnect {
338 from_track: "hw:in".to_string(),
339 from_port,
340 to_track: "hw:out".to_string(),
341 to_port,
342 kind: Kind::Audio,
343 });
344 actions.push(Action::Connect {
345 from_track: "hw:in".to_string(),
346 from_port,
347 to_track: "hw:out".to_string(),
348 to_port: to_port - 1,
349 kind: Kind::Audio,
350 });
351 }
352 }
353 }
354 }
355 actions
356 }
357
358 pub(crate) fn upstream_audio_track_names(
359 &self,
360 seeds: &std::collections::HashSet<String>,
361 ) -> std::collections::HashSet<String> {
362 let state = self.state.lock();
363 let mut output_to_track: std::collections::HashMap<
364 *const crate::audio::io::AudioIO,
365 String,
366 > = std::collections::HashMap::new();
367 for (name, track) in &state.tracks {
368 let t = track.lock();
369 for out in &t.audio.outs {
370 output_to_track.insert(std::sync::Arc::as_ptr(out), name.clone());
371 }
372 }
373 let mut upstream = std::collections::HashSet::new();
374 let mut to_process: Vec<String> = seeds.iter().cloned().collect();
375 let mut processed = std::collections::HashSet::new();
376 while let Some(target_name) = to_process.pop() {
377 if !processed.insert(target_name.clone()) {
378 continue;
379 }
380 if let Some(target_track) = state.tracks.get(&target_name) {
381 let tt = target_track.lock();
382 for input in &tt.audio.ins {
383 for conn in input.connections().iter() {
384 let conn_ptr = std::sync::Arc::as_ptr(conn);
385 if let Some(source_name) = output_to_track.get(&conn_ptr)
386 && source_name != &target_name
387 && !seeds.contains(source_name)
388 {
389 upstream.insert(source_name.clone());
390 to_process.push(source_name.clone());
391 }
392 }
393 }
394 }
395 }
396 upstream
397 }
398
399 pub(crate) fn is_track_in_soloed_folder(
400 &self,
401 track: &crate::track::TrackData,
402 tracks: &std::collections::HashMap<String, crate::state::TrackHandle>,
403 ) -> bool {
404 let mut current = track.parent_track.clone();
405 while let Some(parent_name) = current {
406 if let Some(parent) = tracks.get(&parent_name) {
407 let p = parent.lock();
408 if p.soloed() {
409 return true;
410 }
411 current = p.parent_track.clone();
412 } else {
413 break;
414 }
415 }
416 false
417 }
418
419 pub(crate) fn folder_has_soloed_descendant(
420 &self,
421 folder_name: &str,
422 tracks: &std::collections::HashMap<String, crate::state::TrackHandle>,
423 ) -> bool {
424 for track in tracks.values() {
425 let t = track.lock();
426 if !t.soloed() {
427 continue;
428 }
429 let mut current = t.parent_track.clone();
430 while let Some(parent_name) = current {
431 if parent_name == folder_name {
432 return true;
433 }
434 if let Some(parent) = tracks.get(&parent_name) {
435 current = parent.lock().parent_track.clone();
436 } else {
437 break;
438 }
439 }
440 }
441 false
442 }
443
444 pub(crate) fn refresh_realtime_infection(&self) {
445 let state = self.state.lock();
446 let live_seeds: std::collections::HashSet<String> = state
447 .tracks
448 .iter()
449 .filter_map(|(name, track)| {
450 let t = track.lock();
451 if t.armed() && t.input_monitor().iter().any(|&m| m) {
452 Some(name.clone())
453 } else {
454 None
455 }
456 })
457 .collect();
458 let mut output_owner: std::collections::HashMap<*const crate::audio::io::AudioIO, String> =
459 std::collections::HashMap::new();
460 for (name, track) in state.tracks.iter() {
461 let t = track.lock();
462 for out in &t.audio.outs {
463 output_owner.insert(std::sync::Arc::as_ptr(out), name.clone());
464 }
465 }
466
467 let mut infected = live_seeds.clone();
468 let mut mixed_nodes = std::collections::HashSet::new();
469 loop {
470 let mut changed = false;
471 for (name, track) in state.tracks.iter() {
472 let t = track.lock();
473 let mut upstream_owners = std::collections::HashSet::new();
474 for input in &t.audio.ins {
475 for conn in input.connections().iter() {
476 if let Some(owner) = output_owner.get(&std::sync::Arc::as_ptr(conn)) {
477 upstream_owners.insert(owner.clone());
478 }
479 }
480 }
481 if upstream_owners.is_empty() {
482 continue;
483 }
484 let has_realtime = upstream_owners
485 .iter()
486 .any(|owner| infected.contains(owner) || live_seeds.contains(owner));
487 let has_playback = upstream_owners
488 .iter()
489 .any(|owner| !infected.contains(owner) && !live_seeds.contains(owner));
490 if has_realtime && has_playback {
491 mixed_nodes.insert(name.clone());
492 }
493 if has_realtime && infected.insert(name.clone()) {
494 changed = true;
495 }
496 }
497 if !changed {
498 break;
499 }
500 }
501
502 for (name, track) in state.tracks.iter() {
503 let forced = infected.contains(name) && !live_seeds.contains(name);
504 let mut t = track.lock();
505 t.set_shared_realtime_mixed(mixed_nodes.contains(name));
506 t.set_force_realtime_domain(forced);
507 }
508 }
509
510 pub(crate) fn apply_mute_solo_policy(&mut self) {
511 let mut newly_disabled_tracks: Vec<String> = Vec::new();
512 {
513 let tracks = &self.state.lock().tracks;
514 let soloed: std::collections::HashSet<String> = tracks
515 .iter()
516 .filter_map(|(name, t)| {
517 if t.lock().soloed() {
518 Some(name.clone())
519 } else {
520 None
521 }
522 })
523 .collect();
524 let any_soloed = !soloed.is_empty();
525 let upstream = if any_soloed {
526 self.upstream_audio_track_names(&soloed)
527 } else {
528 std::collections::HashSet::new()
529 };
530 for track in tracks.values() {
531 let t = track.lock();
532 let was_enabled = t.output_enabled();
533 let in_soloed_folder = self.is_track_in_soloed_folder(&t, tracks);
534 let folder_with_soloed_child =
535 t.is_folder && self.folder_has_soloed_descendant(&t.name, tracks);
536 let enabled = if t.is_master() {
537 !t.muted()
538 } else if any_soloed {
539 (t.soloed()
540 || upstream.contains(&t.name)
541 || in_soloed_folder
542 || folder_with_soloed_child)
543 && !t.muted()
544 } else {
545 !t.muted()
546 };
547 t.set_output_enabled(enabled);
548 if was_enabled && !enabled {
549 newly_disabled_tracks.push(t.name.clone());
550 }
551 }
552 }
553 let mut note_off_events = Vec::new();
554 for track_name in newly_disabled_tracks {
555 note_off_events.extend(self.note_off_events_for_track(&track_name));
556 }
557 if !note_off_events.is_empty() {
558 self.pending_hw_midi_out_events_by_device
559 .extend(note_off_events);
560 }
561 }
562
563 pub(crate) fn track_handle_by_name(
564 &self,
565 track_name: &str,
566 ) -> Option<crate::state::TrackHandle> {
567 self.state.lock().tracks.get(track_name).cloned()
568 }
569
570 pub(crate) fn track_handle_or_err(
571 &self,
572 track_name: &str,
573 ) -> Result<crate::state::TrackHandle, String> {
574 self.track_handle_by_name(track_name)
575 .ok_or_else(|| format!("Track not found: {track_name}"))
576 }
577
578 pub(crate) fn add_clip_to_track(&self, request: ClipAddRequest<'_>) {
579 if let Some(track) = self.state.lock().tracks.get(request.track_name) {
580 let mut track = track.lock();
581 if track.is_master() || track.is_folder {
582 return;
583 }
584 match request.kind {
585 Kind::Audio => {
586 let mut clip = AudioClip::new(
587 request.name.to_string(),
588 request.start,
589 request.start.saturating_add(request.length.max(1)),
590 );
591 clip.id = request.clip_id.to_string();
592 clip.offset = request.offset;
593 let max_lane = track.audio.ins.len().saturating_sub(1);
594 clip.input_channel = request.input_channel.min(max_lane);
595 clip.muted = request.muted;
596 clip.peaks_file = request.peaks_file;
597 clip.fade_enabled = request.fade_enabled;
598 clip.fade_in_samples = request.fade_in_samples;
599 clip.fade_out_samples = request.fade_out_samples;
600 clip.pitch_correction_preview_name = request.preview_name;
601 clip.pitch_correction_source_name = request.source_name;
602 clip.pitch_correction_source_offset = request.source_offset;
603 clip.pitch_correction_source_length = request.source_length;
604 clip.pitch_correction_points = request.pitch_correction_points;
605 clip.pitch_correction_frame_likeness = request.pitch_correction_frame_likeness;
606 clip.pitch_correction_inertia_ms = request.pitch_correction_inertia_ms;
607 clip.pitch_correction_formant_compensation =
608 request.pitch_correction_formant_compensation;
609 clip.plugin_graph_json = request.plugin_graph_json;
610 track.audio.push_clip(clip);
611 #[cfg(unix)]
612 track.rt.clip_pitch_shifters.clear();
613 }
614 Kind::MIDI => {
615 let mut clip = MIDIClip::new(
616 request.name.to_string(),
617 request.start,
618 request.start.saturating_add(request.length.max(1)),
619 );
620 clip.id = request.clip_id.to_string();
621 clip.offset = request.offset;
622 let max_lane = track.midi.ins.len().saturating_sub(1);
623 clip.input_channel = request.input_channel.min(max_lane);
624 clip.muted = request.muted;
625 track.midi.push_clip(clip);
626 }
627 }
628 }
629 }
630
631 pub(crate) fn audio_clip_from_data(data: &crate::message::AudioClipData) -> AudioClip {
632 let mut clip = AudioClip::new(
633 data.name.clone(),
634 data.start,
635 data.start.saturating_add(data.length.max(1)),
636 );
637 clip.id = data.id.clone();
638 clip.offset = data.offset;
639 clip.input_channel = data.input_channel;
640 clip.muted = data.muted;
641 clip.peaks_file = data.peaks_file.clone();
642 clip.fade_enabled = data.fade_enabled;
643 clip.fade_in_samples = data.fade_in_samples;
644 clip.fade_out_samples = data.fade_out_samples;
645 clip.pitch_correction_preview_name = data.preview_name.clone();
646 clip.pitch_correction_source_name = data.source_name.clone();
647 clip.pitch_correction_source_offset = data.source_offset;
648 clip.pitch_correction_source_length = data.source_length;
649 clip.pitch_correction_points = data.pitch_correction_points.clone();
650 clip.pitch_correction_frame_likeness = data.pitch_correction_frame_likeness;
651 clip.pitch_correction_inertia_ms = data.pitch_correction_inertia_ms;
652 clip.pitch_correction_formant_compensation = data.pitch_correction_formant_compensation;
653 clip.plugin_graph_json = data.plugin_graph_json.clone();
654 clip.grouped_clips = data
655 .grouped_clips
656 .iter()
657 .map(Self::audio_clip_from_data)
658 .collect();
659 for child in &mut clip.grouped_clips {
660 child.fade_enabled = false;
661 child.fade_in_samples = 0;
662 child.fade_out_samples = 0;
663 }
664 clip
665 }
666
667 pub(crate) fn midi_clip_from_data(data: &crate::message::MidiClipData) -> MIDIClip {
668 let mut clip = MIDIClip::new(
669 data.name.clone(),
670 data.start,
671 data.start.saturating_add(data.length.max(1)),
672 );
673 clip.id = data.id.clone();
674 clip.offset = data.offset;
675 clip.input_channel = data.input_channel;
676 clip.muted = data.muted;
677 clip.grouped_clips = data
678 .grouped_clips
679 .iter()
680 .map(Self::midi_clip_from_data)
681 .collect();
682 clip
683 }
684
685 pub(crate) fn add_grouped_clip_to_track(
686 &self,
687 track_name: &str,
688 kind: Kind,
689 audio_clip: Option<crate::message::AudioClipData>,
690 midi_clip: Option<crate::message::MidiClipData>,
691 ) {
692 if let Some(track) = self.state.lock().tracks.get(track_name) {
693 let mut track = track.lock();
694 if track.is_master() {
695 return;
696 }
697 match kind {
698 Kind::Audio => {
699 if let Some(mut clip) = audio_clip.map(|clip| Self::audio_clip_from_data(&clip))
700 {
701 let max_lane = track.audio.ins.len().saturating_sub(1);
702 clip.input_channel = clip.input_channel.min(max_lane);
703 track.audio.push_clip(clip);
704 #[cfg(unix)]
705 track.rt.clip_pitch_shifters.clear();
706 }
707 }
708 Kind::MIDI => {
709 if let Some(mut clip) = midi_clip.map(|clip| Self::midi_clip_from_data(&clip)) {
710 let max_lane = track.midi.ins.len().saturating_sub(1);
711 clip.input_channel = clip.input_channel.min(max_lane);
712 track.midi.push_clip(clip);
713 }
714 }
715 }
716 }
717 }
718
719 pub(crate) fn remove_clips_from_track(
720 &self,
721 track_name: &str,
722 kind: Kind,
723 clip_indices: &[usize],
724 ) {
725 if let Some(track) = self.state.lock().tracks.get(track_name) {
726 let mut track = track.lock();
727 let mut indices = clip_indices.to_vec();
728 indices.sort_unstable();
729 indices.dedup();
730 match kind {
731 Kind::Audio => {
732 for idx in indices.into_iter().rev() {
733 if idx < track.audio.clips().len() {
734 track.audio.remove_clip(idx);
735 }
736 }
737 #[cfg(unix)]
738 track.rt.clip_pitch_shifters.clear();
739 }
740 Kind::MIDI => {
741 for idx in indices.into_iter().rev() {
742 if idx < track.midi.clips().len() {
743 track.midi.remove_clip(idx);
744 }
745 }
746 }
747 }
748 }
749 }
750
751 pub(crate) fn rename_clip_references(
752 &self,
753 track_name: &str,
754 kind: Kind,
755 clip_index: usize,
756 new_name: &str,
757 ) {
758 let Some(track) = self.state.lock().tracks.get(track_name).cloned() else {
759 return;
760 };
761 let track = track.lock();
762 let old_name = match kind {
763 Kind::Audio => {
764 if clip_index >= track.audio.clips().len() {
765 return;
766 }
767 track.audio.clips().get(clip_index).unwrap().name.clone()
768 }
769 Kind::MIDI => {
770 if clip_index >= track.midi.clips().len() {
771 return;
772 }
773 track.midi.clips().get(clip_index).unwrap().name.clone()
774 }
775 };
776
777 let new_file_name = match kind {
778 Kind::Audio => format!("audio/{}.wav", new_name),
779 Kind::MIDI => {
780 let ext = std::path::Path::new(&old_name)
781 .extension()
782 .and_then(|e| e.to_str())
783 .map(|s| s.to_ascii_lowercase())
784 .filter(|e| e == "mid" || e == "midi")
785 .unwrap_or_else(|| "mid".to_string());
786 format!("midi/{}.{}", new_name, ext)
787 }
788 };
789 let _ = track;
790
791 for other_track in self.state.lock().tracks.values() {
792 let other_track = other_track.lock();
793 match kind {
794 Kind::Audio => {
795 let clips = other_track.audio.clips();
796 for idx in 0..clips.len() {
797 other_track.audio.update_clip(idx, |clip| {
798 if clip.name == old_name {
799 clip.name = new_file_name.clone();
800 }
801 if clip.pitch_correction_source_name.as_deref()
802 == Some(old_name.as_str())
803 {
804 clip.pitch_correction_source_name = Some(new_file_name.clone());
805 }
806 });
807 }
808 }
809 Kind::MIDI => {
810 let clips = other_track.midi.clips();
811 for idx in 0..clips.len() {
812 other_track.midi.update_clip(idx, |clip| {
813 if clip.name == old_name {
814 clip.name = new_file_name.clone();
815 }
816 });
817 }
818 }
819 }
820 }
821 }
822
823 pub(crate) fn set_clip_fade(
824 &self,
825 track_name: &str,
826 clip_index: usize,
827 kind: Kind,
828 fade_enabled: bool,
829 fade_in_samples: usize,
830 fade_out_samples: usize,
831 ) {
832 let Some(track) = self.state.lock().tracks.get(track_name).cloned() else {
833 return;
834 };
835 let track = track.lock();
836 match kind {
837 Kind::Audio => {
838 track.audio.update_clip(clip_index, |clip| {
839 clip.fade_enabled = fade_enabled;
840 clip.fade_in_samples = fade_in_samples;
841 clip.fade_out_samples = fade_out_samples;
842 });
843 }
844 Kind::MIDI => {}
845 }
846 }
847
848 pub(crate) fn set_clip_bounds(
849 &self,
850 track_name: &str,
851 clip_index: usize,
852 kind: Kind,
853 start: usize,
854 length: usize,
855 offset: usize,
856 ) {
857 let Some(track) = self.state.lock().tracks.get(track_name).cloned() else {
858 return;
859 };
860 let mut track = track.lock();
861 match kind {
862 Kind::Audio => {
863 track.audio.update_clip(clip_index, |clip| {
864 clip.start = start;
865 clip.end = start.saturating_add(length.max(1));
866 clip.offset = offset;
867 clip.pitch_correction_preview_name = None;
868 clip.pitch_correction_source_name = None;
869 clip.pitch_correction_source_offset = None;
870 clip.pitch_correction_source_length = None;
871 clip.pitch_correction_points.clear();
872 clip.pitch_correction_frame_likeness = None;
873 clip.pitch_correction_inertia_ms = None;
874 clip.pitch_correction_formant_compensation = None;
875 });
876 #[cfg(unix)]
877 track.rt.clip_pitch_shifters.clear();
878 }
879 Kind::MIDI => {
880 track.midi.update_clip(clip_index, |clip| {
881 clip.start = start;
882 clip.end = start.saturating_add(length.max(1));
883 clip.offset = offset;
884 });
885 }
886 }
887 }
888
889 pub(crate) fn set_clip_source_name(
890 &self,
891 track_name: &str,
892 clip_index: usize,
893 kind: Kind,
894 name: String,
895 ) {
896 let Some(track) = self.state.lock().tracks.get(track_name).cloned() else {
897 return;
898 };
899 let mut track = track.lock();
900 match kind {
901 Kind::Audio => {
902 track.audio.update_clip(clip_index, |clip| {
903 clip.name = name;
904 });
905 #[cfg(unix)]
906 track.rt.clip_pitch_shifters.clear();
907 }
908 Kind::MIDI => {
909 track.midi.update_clip(clip_index, |clip| {
910 clip.name = name;
911 });
912 }
913 }
914 }
915
916 pub(crate) fn set_clip_muted(
917 &self,
918 track_name: &str,
919 clip_index: usize,
920 kind: Kind,
921 muted: bool,
922 ) {
923 let Some(track) = self.state.lock().tracks.get(track_name).cloned() else {
924 return;
925 };
926 let track = track.lock();
927 match kind {
928 Kind::Audio => {
929 track.audio.update_clip(clip_index, |clip| {
930 clip.muted = muted;
931 });
932 }
933 Kind::MIDI => {
934 track.midi.update_clip(clip_index, |clip| {
935 clip.muted = muted;
936 });
937 }
938 }
939 }
940
941 #[allow(clippy::too_many_arguments)]
942 pub(crate) fn set_clip_pitch_correction(
943 &self,
944 track_name: &str,
945 clip_index: usize,
946 preview_name: Option<String>,
947 source_name: Option<String>,
948 source_offset: Option<usize>,
949 source_length: Option<usize>,
950 pitch_correction_points: Vec<crate::message::PitchCorrectionPointData>,
951 pitch_correction_frame_likeness: Option<f32>,
952 pitch_correction_inertia_ms: Option<u16>,
953 pitch_correction_formant_compensation: Option<bool>,
954 ) {
955 if let Some(track) = self.state.lock().tracks.get(track_name) {
956 let mut track = track.lock();
957 track.audio.update_clip(clip_index, |clip| {
958 clip.pitch_correction_preview_name = preview_name;
959 clip.pitch_correction_source_name = source_name;
960 clip.pitch_correction_source_offset = source_offset;
961 clip.pitch_correction_source_length = source_length;
962 clip.pitch_correction_points = pitch_correction_points;
963 clip.pitch_correction_frame_likeness = pitch_correction_frame_likeness;
964 clip.pitch_correction_inertia_ms = pitch_correction_inertia_ms;
965 clip.pitch_correction_formant_compensation = pitch_correction_formant_compensation;
966 });
967 #[cfg(unix)]
968 track.rt.clip_pitch_shifters.clear();
969 }
970 }
971
972 pub fn check_if_leads_to_kind(
973 &self,
974 kind: Kind,
975 current_track_name: &str,
976 target_track_name: &str,
977 ) -> bool {
978 routing::would_create_cycle(
979 &target_track_name.to_string(),
980 ¤t_track_name.to_string(),
981 |track_name| self.connected_neighbors(kind, track_name),
982 )
983 }
984
985 pub(crate) fn connected_neighbors(&self, kind: Kind, current_track_name: &str) -> Vec<String> {
986 let state = self.state.lock();
987 let mut found_neighbors = Vec::new();
988
989 if let Some(current_track_handle) = state.tracks.get(current_track_name) {
990 let current_track = current_track_handle.lock();
991
992 match kind {
993 Kind::Audio => {
994 for out_port in ¤t_track.audio.outs {
995 let conns = out_port.connections();
996 for conn in conns.iter() {
997 for (name, next_track_handle) in &state.tracks {
998 let next_track = next_track_handle.lock();
999 let is_connected = next_track
1000 .audio
1001 .ins
1002 .iter()
1003 .any(|ins_port| Arc::ptr_eq(ins_port, conn));
1004
1005 if is_connected {
1006 found_neighbors.push(name.clone());
1007 }
1008 }
1009 }
1010 }
1011 }
1012 Kind::MIDI => {
1013 for out_port in ¤t_track.midi.outs {
1014 let conns = out_port.connections();
1015 for conn in conns.iter() {
1016 for (name, next_track_handle) in &state.tracks {
1017 let next_track = next_track_handle.lock();
1018 let is_connected = next_track
1019 .midi
1020 .ins
1021 .iter()
1022 .any(|ins_port| Arc::ptr_eq(ins_port, conn));
1023
1024 if is_connected {
1025 found_neighbors.push(name.clone());
1026 }
1027 }
1028 }
1029 }
1030 }
1031 }
1032 }
1033 found_neighbors
1034 }
1035
1036 pub(crate) fn find_audio_io_owner(
1037 &self,
1038 state: &crate::state::State,
1039 io: &std::sync::Arc<crate::audio::io::AudioIO>,
1040 ) -> Option<(String, usize)> {
1041 for (name, track) in &state.tracks {
1042 let t = track.lock();
1043 for (i, out) in t.audio.outs.iter().enumerate() {
1044 if std::sync::Arc::ptr_eq(out, io) {
1045 return Some((name.clone(), i));
1046 }
1047 }
1048 for (i, inp) in t.audio.ins.iter().enumerate() {
1049 if std::sync::Arc::ptr_eq(inp, io) {
1050 return Some((name.clone(), i));
1051 }
1052 }
1053 }
1054 None
1055 }
1056
1057 pub(crate) fn find_midi_io_owner(
1058 &self,
1059 state: &crate::state::State,
1060 io: &std::sync::Arc<crate::midi::io::MIDIIO>,
1061 ) -> Option<(String, usize, bool)> {
1062 for (name, track) in &state.tracks {
1063 let t = track.lock();
1064 for (i, out) in t.midi.outs.iter().enumerate() {
1065 if std::sync::Arc::ptr_eq(out, io) {
1066 return Some((name.clone(), i, false));
1067 }
1068 }
1069 for (i, inp) in t.midi.ins.iter().enumerate() {
1070 if std::sync::Arc::ptr_eq(inp, io) {
1071 return Some((name.clone(), i, true));
1072 }
1073 }
1074 }
1075 None
1076 }
1077
1078 pub(crate) fn collect_descendant_track_names(&self, name: &str, out: &mut Vec<String>) {
1079 let child_arcs: Vec<crate::state::TrackHandle> = {
1082 let state = self.state.lock();
1083 if let Some(track) = state.tracks.get(name) {
1084 track.lock().child_tracks.clone()
1085 } else {
1086 Vec::new()
1087 }
1088 };
1089 for child in child_arcs {
1090 let child_name = { child.lock().name.clone() };
1091 self.collect_descendant_track_names(&child_name, out);
1092 out.push(child_name);
1093 }
1094 }
1095
1096 pub(crate) async fn remove_single_track(&mut self, name: &str) {
1097 let children: Vec<crate::state::TrackHandle> = {
1098 let state = self.state.lock();
1099 if let Some(removed) = state.tracks.get(name).cloned() {
1100 removed.lock().child_tracks.clone()
1101 } else {
1102 Vec::new()
1103 }
1104 };
1105 let parent_name: Option<String> = {
1106 let state = self.state.lock();
1107 state
1108 .tracks
1109 .get(name)
1110 .map(|t| t.lock().parent_track.clone())
1111 .unwrap_or(None)
1112 };
1113 if let Some(parent_name) = parent_name {
1114 let state = self.state.lock();
1115 if let Some(parent) = state.tracks.get(&parent_name).cloned() {
1116 let mut parent = parent.lock();
1117 parent.child_tracks.retain(|c| c.lock().name != *name);
1118 }
1119 }
1120 if let Some(removed_track) = self.state.lock().tracks.get(name).cloned() {
1121 for child in children {
1122 let removed = removed_track.lock();
1123 child.lock().disconnect_from_parent(&removed);
1124 child.lock().parent_track = None;
1125 }
1126 }
1127 self.state.lock().tracks.remove(name);
1128 self.audio_recordings.remove(name);
1129 self.midi_recordings.remove(name);
1130 self.midi_hw_in_routes.retain(|r| r.to_track != *name);
1131 self.midi_hw_out_routes.retain(|r| r.from_track != *name);
1132 if self
1133 .pending_midi_learn
1134 .as_ref()
1135 .is_some_and(|(track_name, _, _)| track_name == name)
1136 {
1137 self.pending_midi_learn = None;
1138 }
1139 }
1140
1141 pub(crate) fn prepare_inverse_actions(
1142 &self,
1143 action_to_process: &Action,
1144 record_history: bool,
1145 suppress_timing_history: bool,
1146 ) -> Option<Vec<Action>> {
1147 let mut extra_inverse_actions: Vec<Action> = Vec::new();
1148 if record_history
1149 && !self.history_suspended
1150 && let Action::RemoveTrack(track_name) = action_to_process
1151 {
1152 for route in self
1153 .midi_hw_in_routes
1154 .iter()
1155 .filter(|route| &route.to_track == track_name)
1156 {
1157 extra_inverse_actions.push(Action::Connect {
1158 from_track: format!("midi:hw:in:{}", route.device),
1159 from_port: 0,
1160 to_track: route.to_track.clone(),
1161 to_port: route.to_port,
1162 kind: Kind::MIDI,
1163 });
1164 }
1165 for route in self
1166 .midi_hw_out_routes
1167 .iter()
1168 .filter(|route| &route.from_track == track_name)
1169 {
1170 extra_inverse_actions.push(Action::Connect {
1171 from_track: route.from_track.clone(),
1172 from_port: route.from_port,
1173 to_track: format!("midi:hw:out:{}", route.device),
1174 to_port: 0,
1175 kind: Kind::MIDI,
1176 });
1177 }
1178 }
1179 if record_history
1180 && !self.history_suspended
1181 && matches!(action_to_process, Action::ClearAllMidiLearnBindings)
1182 {
1183 if let Some(binding) = self.global_midi_learn_play_pause.clone() {
1184 extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
1185 target: crate::message::GlobalMidiLearnTarget::PlayPause,
1186 binding: Some(binding),
1187 });
1188 }
1189 if let Some(binding) = self.global_midi_learn_stop.clone() {
1190 extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
1191 target: crate::message::GlobalMidiLearnTarget::Stop,
1192 binding: Some(binding),
1193 });
1194 }
1195 if let Some(binding) = self.global_midi_learn_record_toggle.clone() {
1196 extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
1197 target: crate::message::GlobalMidiLearnTarget::RecordToggle,
1198 binding: Some(binding),
1199 });
1200 }
1201 for (key, binding) in self.session_midi_learn_slots.clone() {
1202 extra_inverse_actions.push(Action::SetSessionMidiLearnBinding {
1203 target: crate::message::SessionMidiLearnTarget::Slot {
1204 track_name: key.0,
1205 scene_index: key.1,
1206 },
1207 binding: Some(binding),
1208 });
1209 }
1210 for (scene_index, binding) in self.session_midi_learn_scenes.clone() {
1211 extra_inverse_actions.push(Action::SetSessionMidiLearnBinding {
1212 target: crate::message::SessionMidiLearnTarget::Scene(scene_index),
1213 binding: Some(binding),
1214 });
1215 }
1216 for (track_name, binding) in self.session_midi_learn_stop_track.clone() {
1217 extra_inverse_actions.push(Action::SetSessionMidiLearnBinding {
1218 target: crate::message::SessionMidiLearnTarget::StopTrack(track_name),
1219 binding: Some(binding),
1220 });
1221 }
1222 if let Some(binding) = self.session_midi_learn_stop_all.clone() {
1223 extra_inverse_actions.push(Action::SetSessionMidiLearnBinding {
1224 target: crate::message::SessionMidiLearnTarget::StopAll,
1225 binding: Some(binding),
1226 });
1227 }
1228 }
1229 let mut inverse_actions = if record_history
1230 && !suppress_timing_history
1231 && should_record(action_to_process)
1232 && !self.history_suspended
1233 {
1234 create_inverse_actions(action_to_process, self.state.as_ref()).map(|mut actions| {
1235 actions.extend(extra_inverse_actions);
1236 actions
1237 })
1238 } else {
1239 None
1240 };
1241 if record_history && !suppress_timing_history && !self.history_suspended {
1242 match action_to_process {
1243 Action::SetTempo(_) => {
1244 inverse_actions = Some(vec![Action::SetTempo(self.tempo_bpm)]);
1245 }
1246 Action::SetLoopEnabled(_) => {
1247 inverse_actions = Some(vec![Action::SetLoopEnabled(self.loop_enabled)]);
1248 }
1249 Action::SetLoopRange(_) => {
1250 inverse_actions = Some(vec![
1251 Action::SetLoopRange(self.loop_range_samples),
1252 Action::SetLoopEnabled(self.loop_enabled),
1253 ]);
1254 }
1255 Action::SetPunchEnabled(_) => {
1256 inverse_actions = Some(vec![Action::SetPunchEnabled(self.punch_enabled)]);
1257 }
1258 Action::SetPunchRange(_) => {
1259 inverse_actions = Some(vec![
1260 Action::SetPunchRange(self.punch_range_samples),
1261 Action::SetPunchEnabled(self.punch_enabled),
1262 ]);
1263 }
1264 Action::SetMetronomeEnabled(_) => {
1265 inverse_actions =
1266 Some(vec![Action::SetMetronomeEnabled(self.metronome_enabled)]);
1267 }
1268 Action::SetTimeSignature { .. } => {
1269 inverse_actions = Some(vec![Action::SetTimeSignature {
1270 numerator: self.tsig_num,
1271 denominator: self.tsig_denom,
1272 }]);
1273 }
1274 Action::SetTempoMap { .. } => {
1275 inverse_actions = Some(vec![Action::SetTempoMap {
1276 tempo_points: self.tempo_points.clone(),
1277 time_signature_points: self.time_signature_points.clone(),
1278 }]);
1279 }
1280 Action::SetClipPlaybackEnabled(_) => {
1281 inverse_actions = Some(vec![Action::SetClipPlaybackEnabled(
1282 self.clip_playback_enabled,
1283 )]);
1284 }
1285 Action::SetRecordEnabled(_) => {
1286 inverse_actions = Some(vec![Action::SetRecordEnabled(self.record_enabled)]);
1287 }
1288 Action::SetGlobalMidiLearnBinding { target, .. } => {
1289 let binding = match target {
1290 crate::message::GlobalMidiLearnTarget::PlayPause => {
1291 self.global_midi_learn_play_pause.clone()
1292 }
1293 crate::message::GlobalMidiLearnTarget::Stop => {
1294 self.global_midi_learn_stop.clone()
1295 }
1296 crate::message::GlobalMidiLearnTarget::RecordToggle => {
1297 self.global_midi_learn_record_toggle.clone()
1298 }
1299 };
1300 inverse_actions = Some(vec![Action::SetGlobalMidiLearnBinding {
1301 target: *target,
1302 binding,
1303 }]);
1304 }
1305 Action::SetModulators(_) => {
1306 inverse_actions = Some(vec![Action::SetModulators(self.modulators.clone())]);
1307 }
1308 Action::SetSessionMidiLearnBinding { target, .. } => {
1309 let binding = match target {
1310 crate::message::SessionMidiLearnTarget::Slot {
1311 track_name,
1312 scene_index,
1313 } => self
1314 .session_midi_learn_slots
1315 .get(&(track_name.clone(), *scene_index))
1316 .cloned(),
1317 crate::message::SessionMidiLearnTarget::Scene(scene_index) => {
1318 self.session_midi_learn_scenes.get(scene_index).cloned()
1319 }
1320 crate::message::SessionMidiLearnTarget::StopTrack(track_name) => {
1321 self.session_midi_learn_stop_track.get(track_name).cloned()
1322 }
1323 crate::message::SessionMidiLearnTarget::StopAll => {
1324 self.session_midi_learn_stop_all.clone()
1325 }
1326 };
1327 inverse_actions = Some(vec![Action::SetSessionMidiLearnBinding {
1328 target: target.clone(),
1329 binding,
1330 }]);
1331 }
1332 _ => {}
1333 }
1334 }
1335 inverse_actions
1336 }
1337
1338 pub(crate) async fn handle_add_track(
1339 &mut self,
1340 name: String,
1341 audio_ins: usize,
1342 midi_ins: usize,
1343 audio_outs: usize,
1344 midi_outs: usize,
1345 folder: bool,
1346 ) {
1347 let tracks = &mut self.state.lock().tracks;
1348 if tracks.contains_key(&name) {
1349 self.notify_clients(Err(format!("Track {} already exists", name)))
1350 .await;
1351 return;
1352 }
1353 let maybe_hw = if let Some(info) = self.hw_driver_info {
1354 Some((info.cycle_samples, info.sample_rate as f64))
1355 } else {
1356 #[cfg(unix)]
1357 if let Some(jack) = &self.jack_runtime {
1358 let j = jack;
1359 Some((j.buffer_size, j.sample_rate as f64))
1360 } else {
1361 None
1362 }
1363 #[cfg(not(unix))]
1364 None
1365 };
1366
1367 if let Some((chsamples, sample_rate)) = maybe_hw {
1368 let track = if folder {
1369 Track::new_folder(
1370 name.clone(),
1371 audio_ins,
1372 audio_outs,
1373 midi_ins,
1374 midi_outs,
1375 chsamples,
1376 sample_rate,
1377 )
1378 } else {
1379 Track::new(
1380 name.clone(),
1381 audio_ins,
1382 audio_outs,
1383 midi_ins,
1384 midi_outs,
1385 chsamples,
1386 sample_rate,
1387 )
1388 };
1389 tracks.insert(name.clone(), Arc::new(track));
1390 if let Some(track) = tracks.get(&name) {
1391 let mut t = track.lock();
1392 t.set_clip_playback_enabled(self.clip_playback_enabled);
1393 t.set_transport_timing(self.tempo_bpm, self.tsig_num, self.tsig_denom);
1394 t.set_session_base_dir(self.session_dir.clone());
1395 }
1396 } else {
1397 self.notify_clients(Err(
1398 "Engine needs to open audio device before adding audio track".to_string(),
1399 ))
1400 .await;
1401 }
1402 }
1403
1404 pub(crate) async fn handle_remove_track(&mut self, name: String, record_history: bool) {
1405 let mut descendant_names = Vec::new();
1406 self.collect_descendant_track_names(&name, &mut descendant_names);
1407 let names_to_remove: Vec<String> = descendant_names
1408 .iter()
1409 .cloned()
1410 .chain(std::iter::once(name.clone()))
1411 .collect();
1412
1413 let combined_inverse = if record_history && !self.history_suspended {
1414 let mut inv = Vec::new();
1415 for n in &names_to_remove {
1416 if let Some(mut actions) =
1417 create_inverse_actions(&Action::RemoveTrack(n.clone()), self.state.as_ref())
1418 {
1419 inv.append(&mut actions);
1420 }
1421 for route in self.midi_hw_in_routes.iter().filter(|r| &r.to_track == n) {
1422 inv.push(Action::Connect {
1423 from_track: format!("midi:hw:in:{}", route.device),
1424 from_port: 0,
1425 to_track: route.to_track.clone(),
1426 to_port: route.to_port,
1427 kind: Kind::MIDI,
1428 });
1429 }
1430 for route in self
1431 .midi_hw_out_routes
1432 .iter()
1433 .filter(|r| &r.from_track == n)
1434 {
1435 inv.push(Action::Connect {
1436 from_track: route.from_track.clone(),
1437 from_port: route.from_port,
1438 to_track: format!("midi:hw:out:{}", route.device),
1439 to_port: 0,
1440 kind: Kind::MIDI,
1441 });
1442 }
1443 }
1444
1445 let mut add_tracks = Vec::new();
1449 let mut connections = Vec::new();
1450 let mut rest = Vec::new();
1451 for action in inv {
1452 match action {
1453 Action::AddTrack { .. } => add_tracks.push(action),
1454 Action::Connect { .. } => connections.push(action),
1455 _ => rest.push(action),
1456 }
1457 }
1458 let mut ordered = add_tracks;
1459 ordered.extend(rest);
1460 ordered.extend(connections);
1461 ordered
1462 } else {
1463 Vec::new()
1464 };
1465
1466 for n in &descendant_names {
1467 self.remove_single_track(n).await;
1468 self.notify_clients(Ok(Action::RemoveTrack(n.clone())))
1469 .await;
1470 }
1471 self.remove_single_track(&name).await;
1472
1473 if record_history && !self.history_suspended && !combined_inverse.is_empty() {
1474 self.history.record(UndoEntry {
1475 forward_actions: vec![Action::RemoveTrack(name.clone())],
1476 inverse_actions: combined_inverse,
1477 });
1478 }
1479 }
1480
1481 pub(crate) async fn handle_connect(
1482 &mut self,
1483 from_track: &str,
1484 from_port: usize,
1485 to_track: &str,
1486 to_port: usize,
1487 kind: Kind,
1488 ) {
1489 match kind {
1490 Kind::Audio => {
1491 let (from_audio_io, to_audio_io) =
1492 self.resolve_audio_route_ports(from_track, from_port, to_track, to_port);
1493 match (from_audio_io, to_audio_io) {
1494 (Some(source), Some(target)) => {
1495 if from_track != "hw:in"
1496 && to_track != "hw:out"
1497 && self.check_if_leads_to_kind(Kind::Audio, to_track, from_track)
1498 {
1499 self.notify_clients(Err("Circular routing is not allowed!".into()))
1500 .await;
1501 return;
1502 }
1503 crate::audio::io::AudioIO::connect(&source, &target);
1504 }
1505 (None, _) => {
1506 self.notify_clients(Err(format!(
1507 "Source track '{}' not found",
1508 from_track
1509 )))
1510 .await;
1511 }
1512 (_, None) => {
1513 self.notify_clients(Err(format!(
1514 "Destination track '{}' not found",
1515 to_track
1516 )))
1517 .await;
1518 }
1519 }
1520 }
1521 Kind::MIDI => {
1522 let from_hw_in_device = Self::midi_hw_in_device(from_track);
1523 let to_hw_out_device = Self::midi_hw_out_device(to_track);
1524 let from_is_invalid_hw = Self::midi_hw_out_device(from_track).is_some();
1525 let to_is_invalid_hw = Self::midi_hw_in_device(to_track).is_some();
1526
1527 if from_is_invalid_hw || to_is_invalid_hw {
1528 self.notify_clients(Err(
1529 "Invalid MIDI hardware connection direction".to_string()
1530 ))
1531 .await;
1532 return;
1533 }
1534
1535 if from_hw_in_device.is_none()
1536 && to_hw_out_device.is_none()
1537 && self.check_if_leads_to_kind(Kind::MIDI, to_track, from_track)
1538 {
1539 self.notify_clients(Err("Circular routing is not allowed!".into()))
1540 .await;
1541 return;
1542 }
1543
1544 let state = self.state.lock();
1545 let from_track_handle = state.tracks.get(from_track);
1546 let to_track_handle = state.tracks.get(to_track);
1547
1548 if let (Some(from_device), Some(to_device)) = (from_hw_in_device, to_hw_out_device)
1549 {
1550 let route = MidiHwThruRoute {
1551 from_device: from_device.to_string(),
1552 to_device: to_device.to_string(),
1553 };
1554 if !self.midi_hw_thru_routes.iter().any(|r| r == &route) {
1555 self.midi_hw_thru_routes.push(route);
1556 }
1557 } else if let Some(device) = from_hw_in_device {
1558 if let Some(t_t) = to_track_handle {
1559 if t_t.lock().midi.ins.get(to_port).is_none() {
1560 self.notify_clients(Err(format!(
1561 "MIDI input port {} not found on track '{}'",
1562 to_port, to_track
1563 )))
1564 .await;
1565 return;
1566 }
1567 let route = MidiHwInRoute {
1568 device: device.to_string(),
1569 to_track: to_track.to_string(),
1570 to_port,
1571 };
1572 if !self.midi_hw_in_routes.iter().any(|r| r == &route) {
1573 self.midi_hw_in_routes.push(route);
1574 }
1575 } else {
1576 self.notify_clients(Err(format!(
1577 "MIDI destination track not found: {}",
1578 to_track
1579 )))
1580 .await;
1581 }
1582 } else if let Some(device) = to_hw_out_device {
1583 if let Some(f_t) = from_track_handle {
1584 if f_t.lock().midi.outs.get(from_port).is_none() {
1585 self.notify_clients(Err(format!(
1586 "MIDI output port {} not found on track '{}'",
1587 from_port, from_track
1588 )))
1589 .await;
1590 return;
1591 }
1592 let route = MidiHwOutRoute {
1593 from_track: from_track.to_string(),
1594 from_port,
1595 device: device.to_string(),
1596 };
1597 if !self.midi_hw_out_routes.iter().any(|r| r == &route) {
1598 self.midi_hw_out_routes.push(route);
1599 }
1600 } else {
1601 self.notify_clients(Err(format!(
1602 "MIDI source track not found: {}",
1603 from_track
1604 )))
1605 .await;
1606 }
1607 } else {
1608 match (from_track_handle, to_track_handle) {
1609 (Some(f_t), Some(t_t)) => {
1610 let to_in_res = t_t.lock().midi.ins.get(to_port).cloned();
1611 if let Some(to_in) = to_in_res {
1612 let mut from_track = f_t.lock();
1613 if let Err(e) = from_track.midi.connect_out(from_port, to_in) {
1614 self.notify_clients(Err(e)).await;
1615 return;
1616 }
1617 from_track.invalidate_midi_route_cache();
1618 } else {
1619 self.notify_clients(Err(format!(
1620 "MIDI input port {} not found on track '{}'",
1621 to_port, to_track
1622 )))
1623 .await;
1624 }
1625 }
1626 _ => {
1627 self.notify_clients(Err(format!(
1628 "MIDI tracks not found: {} or {}",
1629 from_track, to_track
1630 )))
1631 .await;
1632 }
1633 }
1634 }
1635 }
1636 };
1637 }
1638
1639 pub(crate) async fn handle_disconnect(&mut self, action: Action) {
1640 let Action::Disconnect {
1641 ref from_track,
1642 from_port,
1643 ref to_track,
1644 to_port,
1645 kind,
1646 } = action
1647 else {
1648 return;
1649 };
1650
1651 if kind == Kind::Audio {
1652 if let Err(e) = self.disconnect_audio_route_and_notify(action.clone()).await {
1653 self.notify_clients(Err(e)).await;
1654 }
1655 } else if kind == Kind::MIDI {
1656 let from_hw_in_device = Self::midi_hw_in_device(from_track);
1657 let to_hw_out_device = Self::midi_hw_out_device(to_track);
1658
1659 if let (Some(from_device), Some(to_device)) = (from_hw_in_device, to_hw_out_device) {
1660 let before = self.midi_hw_thru_routes.len();
1661 self.midi_hw_thru_routes
1662 .retain(|r| !(r.from_device == from_device && r.to_device == to_device));
1663 if self.midi_hw_thru_routes.len() < before {
1664 self.notify_clients(Ok(action.clone())).await;
1665 } else {
1666 self.notify_clients(Err(format!(
1667 "Disconnect failed: MIDI route not found ({} -> {})",
1668 from_track, to_track
1669 )))
1670 .await;
1671 }
1672 return;
1673 }
1674
1675 if let Some(device) = from_hw_in_device {
1676 let before = self.midi_hw_in_routes.len();
1677 self.midi_hw_in_routes.retain(|r| {
1678 !(r.device == device && r.to_track == *to_track && r.to_port == to_port)
1679 });
1680 if self.midi_hw_in_routes.len() < before {
1681 self.notify_clients(Ok(action.clone())).await;
1682 } else {
1683 self.notify_clients(Err(format!(
1684 "Disconnect failed: MIDI route not found ({} -> {})",
1685 from_track, to_track
1686 )))
1687 .await;
1688 }
1689 return;
1690 }
1691
1692 if let Some(device) = to_hw_out_device {
1693 let before = self.midi_hw_out_routes.len();
1694 self.midi_hw_out_routes.retain(|r| {
1695 !(r.from_track == *from_track && r.from_port == from_port && r.device == device)
1696 });
1697 if self.midi_hw_out_routes.len() < before {
1698 self.notify_clients(Ok(action.clone())).await;
1699 } else {
1700 self.notify_clients(Err(format!(
1701 "Disconnect failed: MIDI route not found ({} -> {})",
1702 from_track, to_track
1703 )))
1704 .await;
1705 }
1706 return;
1707 }
1708
1709 let state = self.state.lock();
1710 if let (Some(f_t), Some(t_t)) =
1711 (state.tracks.get(from_track), state.tracks.get(to_track))
1712 && let Some(to_in) = t_t.lock().midi.ins.get(to_port).cloned()
1713 {
1714 let mut from_track = f_t.lock();
1715 if let Err(e) = from_track.midi.disconnect_out(from_port, &to_in) {
1716 self.notify_clients(Err(e)).await;
1717 } else {
1718 from_track.invalidate_midi_route_cache();
1719 self.notify_clients(Ok(action.clone())).await;
1720 }
1721 } else {
1722 self.notify_clients(Err(format!(
1723 "Disconnect failed: MIDI ports not found ({} -> {})",
1724 from_track, to_track
1725 )))
1726 .await;
1727 }
1728 }
1729 }
1730
1731 pub(crate) async fn handle_track_set_parent(
1732 &mut self,
1733 track_name: &str,
1734 parent_name: Option<&str>,
1735 ) {
1736 let track = match self.track_handle_or_err(track_name) {
1737 Ok(track) => track,
1738 Err(e) => {
1739 self.notify_clients(Err(e)).await;
1740 return;
1741 }
1742 };
1743 if parent_name == Some(track_name) {
1744 self.notify_clients(Err("Track cannot be its own parent".to_string()))
1745 .await;
1746 return;
1747 }
1748
1749 if let Some(parent_name) = parent_name {
1751 let state = self.state.lock();
1752 let parent = state.tracks.get(parent_name);
1753 if parent.is_none() {
1754 self.notify_clients(Err(format!(
1755 "Parent track '{}' does not exist",
1756 parent_name
1757 )))
1758 .await;
1759 return;
1760 }
1761 if !parent.unwrap().lock().is_folder {
1762 self.notify_clients(Err(format!("Track '{}' is not a folder", parent_name)))
1763 .await;
1764 return;
1765 }
1766 }
1767
1768 {
1770 let old_parent_name = track.lock().parent_track.clone();
1771 if let Some(old_parent_name) = old_parent_name {
1772 let state = self.state.lock();
1773 if let (Some(parent_arc), Some(child_arc)) = (
1774 state.tracks.get(&old_parent_name).cloned(),
1775 state.tracks.get(track_name).cloned(),
1776 ) {
1777 {
1778 let mut parent = parent_arc.lock();
1779 parent.child_tracks.retain(|c| c.lock().name != *track_name);
1780 }
1781 {
1782 let mut child = child_arc.lock();
1783 let parent = parent_arc.lock();
1784 child.disconnect_from_parent(&parent);
1785 }
1786 }
1787 }
1788 }
1789
1790 let mut disconnect_actions = Vec::new();
1791
1792 {
1794 let state = self.state.lock();
1795 let hw_inputs = self.all_hw_input_audio_ports();
1796 let hw_outputs = self.all_hw_output_audio_ports();
1797 if let Some(child_arc) = state.tracks.get(track_name).cloned() {
1798 let child = child_arc.lock();
1799 for (port_idx, inp) in child.audio.ins.iter().enumerate() {
1800 let sources = inp.connections();
1801 for src in sources.iter() {
1802 let _ = AudioIO::disconnect(src, inp);
1803 if let Some((src_name, src_port)) =
1804 self.find_audio_io_owner(self.state.as_ref(), src)
1805 {
1806 disconnect_actions.push(Action::Disconnect {
1807 from_track: src_name,
1808 from_port: src_port,
1809 to_track: track_name.to_string(),
1810 to_port: port_idx,
1811 kind: Kind::Audio,
1812 });
1813 } else if let Some(src_port) = hw_inputs
1814 .iter()
1815 .position(|hw_in| std::sync::Arc::ptr_eq(hw_in, src))
1816 {
1817 disconnect_actions.push(Action::Disconnect {
1818 from_track: "hw:in".to_string(),
1819 from_port: src_port,
1820 to_track: track_name.to_string(),
1821 to_port: port_idx,
1822 kind: Kind::Audio,
1823 });
1824 }
1825 }
1826 }
1827 for (port_idx, out) in child.audio.outs.iter().enumerate() {
1828 let targets = out.connections();
1829 for tgt in targets.iter() {
1830 let _ = AudioIO::disconnect(out, tgt);
1831 if let Some((tgt_name, tgt_port)) =
1832 self.find_audio_io_owner(self.state.as_ref(), tgt)
1833 {
1834 disconnect_actions.push(Action::Disconnect {
1835 from_track: track_name.to_string(),
1836 from_port: port_idx,
1837 to_track: tgt_name,
1838 to_port: tgt_port,
1839 kind: Kind::Audio,
1840 });
1841 } else if let Some(tgt_port) = hw_outputs
1842 .iter()
1843 .position(|hw_out| std::sync::Arc::ptr_eq(hw_out, tgt))
1844 {
1845 disconnect_actions.push(Action::Disconnect {
1846 from_track: track_name.to_string(),
1847 from_port: port_idx,
1848 to_track: "hw:out".to_string(),
1849 to_port: tgt_port,
1850 kind: Kind::Audio,
1851 });
1852 }
1853 }
1854 }
1855
1856 for route in self
1858 .midi_hw_in_routes
1859 .iter()
1860 .filter(|r| r.to_track == track_name)
1861 {
1862 disconnect_actions.push(Action::Disconnect {
1863 from_track: format!("midi:hw:in:{}", route.device),
1864 from_port: 0,
1865 to_track: track_name.to_string(),
1866 to_port: route.to_port,
1867 kind: Kind::MIDI,
1868 });
1869 }
1870 self.midi_hw_in_routes.retain(|r| r.to_track != track_name);
1871
1872 for route in self
1873 .midi_hw_out_routes
1874 .iter()
1875 .filter(|r| r.from_track == track_name)
1876 {
1877 disconnect_actions.push(Action::Disconnect {
1878 from_track: track_name.to_string(),
1879 from_port: route.from_port,
1880 to_track: format!("midi:hw:out:{}", route.device),
1881 to_port: 0,
1882 kind: Kind::MIDI,
1883 });
1884 }
1885 self.midi_hw_out_routes
1886 .retain(|r| r.from_track != track_name);
1887
1888 for (port_idx, out) in child.midi.outs.iter().enumerate() {
1890 let targets = out.connections();
1891 for tgt in targets {
1892 if let Some((tgt_name, tgt_port, _)) =
1893 self.find_midi_io_owner(self.state.as_ref(), &tgt)
1894 {
1895 let _ = MIDIIO::disconnect(out, &tgt);
1896 disconnect_actions.push(Action::Disconnect {
1897 from_track: track_name.to_string(),
1898 from_port: port_idx,
1899 to_track: tgt_name,
1900 to_port: tgt_port,
1901 kind: Kind::MIDI,
1902 });
1903 }
1904 }
1905 }
1906 }
1907
1908 let child_input_arcs: Vec<_> =
1910 if let Some(child_arc) = state.tracks.get(track_name).cloned() {
1911 let child = child_arc.lock();
1912 child.midi.ins.clone()
1913 } else {
1914 Vec::new()
1915 };
1916 for (other_name, other_track) in &state.tracks {
1917 if other_name.as_str() == track_name {
1918 continue;
1919 }
1920 let other = other_track.lock();
1921 for (out_port, out) in other.midi.outs.iter().enumerate() {
1922 let targets = out.connections();
1923 for tgt in targets {
1924 if let Some(to_port) = child_input_arcs
1925 .iter()
1926 .position(|inp| std::sync::Arc::ptr_eq(inp, &tgt))
1927 {
1928 let _ = MIDIIO::disconnect(out, &tgt);
1929 disconnect_actions.push(Action::Disconnect {
1930 from_track: other_name.clone(),
1931 from_port: out_port,
1932 to_track: track_name.to_string(),
1933 to_port,
1934 kind: Kind::MIDI,
1935 });
1936 }
1937 }
1938 }
1939 }
1940 }
1941
1942 {
1944 track.lock().parent_track = parent_name.map(str::to_string);
1945 }
1946
1947 if let Some(parent_name) = parent_name {
1949 let state = self.state.lock();
1950 if let (Some(parent_arc), Some(child_arc)) = (
1951 state.tracks.get(parent_name).cloned(),
1952 state.tracks.get(track_name).cloned(),
1953 ) {
1954 {
1955 let mut parent = parent_arc.lock();
1956 parent.child_tracks.push(child_arc.clone());
1957 }
1958 {
1959 let mut child = child_arc.lock();
1960 let mut parent = parent_arc.lock();
1961 if parent.audio.ins.len() == child.audio.ins.len() {
1963 for (parent_in, child_in) in
1964 parent.audio.ins.iter().zip(child.audio.ins.iter())
1965 {
1966 Track::connect_directed_audio(parent_in, child_in);
1967 }
1968 }
1969 if parent.audio.outs.len() == child.audio.outs.len() {
1971 for (child_out, parent_out) in
1972 child.audio.outs.iter().zip(parent.audio.outs.iter())
1973 {
1974 AudioIO::connect(child_out, parent_out);
1975 }
1976 }
1977 if parent.midi.ins.len() == child.midi.ins.len() {
1979 for (parent_in, child_in) in
1980 parent.midi.ins.iter().zip(child.midi.ins.iter())
1981 {
1982 child_in.add_connection(parent_in);
1983 }
1984 }
1985 if parent.midi.outs.len() == child.midi.outs.len() {
1987 for (child_out, parent_out) in
1988 child.midi.outs.iter().zip(parent.midi.outs.iter())
1989 {
1990 child_out.add_connection(parent_out);
1991 }
1992 }
1993 child.invalidate_audio_route_cache();
1994 parent.invalidate_audio_route_cache();
1995 child.invalidate_midi_route_cache();
1996 parent.invalidate_midi_route_cache();
1997 }
1998 }
1999 }
2000
2001 {
2004 let state = self.state.lock();
2005 if let Some(child_arc) = state.tracks.get(track_name).cloned() {
2006 let mut child = child_arc.lock();
2007 child.ensure_default_audio_passthrough();
2008 child.ensure_default_midi_passthrough();
2009 }
2010 }
2011
2012 for action in disconnect_actions {
2013 self.notify_clients(Ok(action)).await;
2014 }
2015
2016 self.notify_clients(Ok(Action::TrackSetParent {
2017 track_name: track_name.to_string(),
2018 parent_name: parent_name.map(str::to_string),
2019 }))
2020 .await;
2021 }
2022
2023 pub(crate) async fn handle_clip_move(&mut self, action: Action) {
2024 let Action::ClipMove {
2025 ref kind,
2026 ref from,
2027 ref to,
2028 copy,
2029 } = action
2030 else {
2031 return;
2032 };
2033 if let Some(from_track_handle) = self.state.lock().tracks.get(&from.track_name)
2034 && let Some(to_track_handle) = self.state.lock().tracks.get(&to.track_name)
2035 {
2036 let from_track = from_track_handle.lock();
2037 let to_track = to_track_handle.lock();
2038 match kind {
2039 Kind::Audio => {
2040 if from.clip_index >= from_track.audio.clips().len() {
2041 self.notify_clients(Err(format!(
2042 "Clip index {} is too high, as track {} has only {} clips!",
2043 from.clip_index,
2044 from_track.name.clone(),
2045 from_track.audio.clips().len(),
2046 )))
2047 .await;
2048 return;
2049 }
2050 if from_track.audio.ins.len() != to_track.audio.ins.len() {
2051 self.notify_clients(Err(format!(
2052 "Cannot move/copy audio clip from '{}' ({} inputs) to '{}' ({} inputs)",
2053 from_track.name,
2054 from_track.audio.ins.len(),
2055 to_track.name,
2056 to_track.audio.ins.len()
2057 )))
2058 .await;
2059 return;
2060 }
2061 let Some(clip_copy) = from_track.audio.clips().get(from.clip_index).cloned()
2062 else {
2063 return;
2064 };
2065 if !copy {
2066 from_track.audio.remove_clip(from.clip_index);
2067 }
2068 let mut clip_copy = (*clip_copy).clone();
2069 clip_copy.start = to.sample_offset;
2070 let max_lane = to_track.audio.ins.len().saturating_sub(1);
2071 clip_copy.input_channel = to.input_channel.min(max_lane);
2072 to_track.audio.push_clip(clip_copy);
2073 }
2074 Kind::MIDI => {
2075 if from.clip_index >= from_track.midi.clips().len() {
2076 self.notify_clients(Err(format!(
2077 "Clip index {} is too high, as track {} has only {} clips!",
2078 from.clip_index,
2079 from_track.name.clone(),
2080 from_track.midi.clips().len(),
2081 )))
2082 .await;
2083 return;
2084 }
2085 let Some(clip_copy) = from_track.midi.clips().get(from.clip_index).cloned()
2086 else {
2087 return;
2088 };
2089 if !copy {
2090 from_track.midi.remove_clip(from.clip_index);
2091 }
2092 let mut clip_copy = (*clip_copy).clone();
2093 clip_copy.start = to.sample_offset;
2094 let max_lane = to_track.midi.ins.len().saturating_sub(1);
2095 clip_copy.input_channel = to.input_channel.min(max_lane);
2096 to_track.midi.push_clip(clip_copy);
2097 }
2098 }
2099 }
2100 }
2101
2102 pub(crate) async fn handle_rename_track(&mut self, action: Action) -> bool {
2103 let Action::RenameTrack {
2104 ref old_name,
2105 ref new_name,
2106 } = action
2107 else {
2108 return false;
2109 };
2110
2111 if self.state.lock().tracks.contains_key(new_name) {
2112 self.notify_clients(Err(format!("Track '{}' already exists", new_name)))
2113 .await;
2114 return true;
2115 }
2116
2117 let Some(track) = self.state.lock().tracks.remove(old_name) else {
2118 self.notify_clients(Err(format!("Track '{}' not found", old_name)))
2119 .await;
2120 return true;
2121 };
2122
2123 track.lock().name = new_name.clone();
2124 self.state.lock().tracks.insert(new_name.clone(), track);
2125 for other in self.state.lock().tracks.values() {
2126 let mut other = other.lock();
2127 if other.parent_track.as_deref() == Some(old_name.as_str()) {
2128 other.parent_track = Some(new_name.clone());
2129 }
2130 }
2131
2132 if let Some(recording) = self.audio_recordings.remove(old_name) {
2133 self.audio_recordings.insert(new_name.clone(), recording);
2134 }
2135 if let Some(recording) = self.midi_recordings.remove(old_name) {
2136 self.midi_recordings.insert(new_name.clone(), recording);
2137 }
2138
2139 for route in &mut self.midi_hw_in_routes {
2140 if route.to_track == *old_name {
2141 route.to_track = new_name.clone();
2142 }
2143 }
2144 for route in &mut self.midi_hw_out_routes {
2145 if route.from_track == *old_name {
2146 route.from_track = new_name.clone();
2147 }
2148 }
2149 if let Some((armed_track, target, device)) = self.pending_midi_learn.clone()
2150 && armed_track == *old_name
2151 {
2152 self.pending_midi_learn = Some((new_name.clone(), target, device));
2153 }
2154
2155 self.notify_clients(Ok(Action::RenameTrack {
2156 old_name: old_name.clone(),
2157 new_name: new_name.clone(),
2158 }))
2159 .await;
2160
2161 false
2162 }
2163
2164 pub(crate) async fn handle_add_clip(&mut self, action: Action) -> bool {
2165 let Action::AddClip {
2166 ref clip_id,
2167 ref name,
2168 ref track_name,
2169 start,
2170 length,
2171 offset,
2172 input_channel,
2173 muted,
2174 ref peaks_file,
2175 kind,
2176 fade_enabled,
2177 fade_in_samples,
2178 fade_out_samples,
2179 ref source_name,
2180 source_offset,
2181 source_length,
2182 ref preview_name,
2183 ref pitch_correction_points,
2184 pitch_correction_frame_likeness,
2185 pitch_correction_inertia_ms,
2186 pitch_correction_formant_compensation,
2187 ref plugin_graph_json,
2188 } = action
2189 else {
2190 return false;
2191 };
2192
2193 self.add_clip_to_track(ClipAddRequest {
2194 clip_id,
2195 name,
2196 track_name,
2197 start,
2198 length,
2199 offset,
2200 input_channel,
2201 muted,
2202 peaks_file: peaks_file.clone(),
2203 kind,
2204 fade_enabled,
2205 fade_in_samples,
2206 fade_out_samples,
2207 source_name: source_name.clone(),
2208 source_offset,
2209 source_length,
2210 preview_name: preview_name.clone(),
2211 pitch_correction_points: pitch_correction_points.clone(),
2212 pitch_correction_frame_likeness,
2213 pitch_correction_inertia_ms,
2214 pitch_correction_formant_compensation,
2215 plugin_graph_json: plugin_graph_json.clone(),
2216 });
2217 if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
2218 let track_name = track_name.clone();
2219 tokio::task::spawn_blocking(move || {
2220 track.lock().preload_clips();
2221 tracing::debug!("Preloaded clips for track '{}' after AddClip", track_name);
2222 });
2223 }
2224
2225 false
2226 }
2227
2228 pub(crate) async fn handle_track_set_folder(&mut self, a: Action) -> bool {
2229 let Action::TrackSetFolder {
2230 ref track_name,
2231 is_folder,
2232 } = a
2233 else {
2234 return false;
2235 };
2236
2237 let track = match self.track_handle_or_err(track_name) {
2238 Ok(track) => track,
2239 Err(e) => {
2240 self.notify_clients(Err(e)).await;
2241 return true;
2242 }
2243 };
2244 if is_folder {
2245 let is_master = track.lock().is_master();
2246 if is_master {
2247 self.notify_clients(Err(format!(
2248 "Track '{}' is the master track and cannot be made a folder",
2249 track_name
2250 )))
2251 .await;
2252 return true;
2253 }
2254 }
2255 {
2256 let mut track = track.lock();
2257 track.is_folder = is_folder;
2258 track.ensure_default_audio_passthrough();
2259 track.ensure_default_midi_passthrough();
2260 }
2261 self.notify_clients(Ok(Action::TrackSetFolder {
2262 track_name: track_name.clone(),
2263 is_folder,
2264 }))
2265 .await;
2266
2267 false
2268 }
2269
2270 pub(crate) async fn handle_track_connect_audio(&mut self, a: Action) -> bool {
2271 let Action::TrackConnectAudio {
2272 ref track_name,
2273 ref from,
2274 from_port,
2275 ref to,
2276 to_port,
2277 } = a
2278 else {
2279 return false;
2280 };
2281
2282 if self
2283 .reject_if_track_frozen(track_name, "routing changes")
2284 .await
2285 {
2286 return true;
2287 }
2288 let track = match self.track_handle_or_err(track_name) {
2289 Ok(track) => track,
2290 Err(e) => {
2291 self.notify_clients(Err(e)).await;
2292 return true;
2293 }
2294 };
2295 if let Err(e) =
2296 track
2297 .lock()
2298 .connect_audio_connectable(from.clone(), from_port, to.clone(), to_port)
2299 {
2300 self.notify_clients(Err(e)).await;
2301 return true;
2302 }
2303
2304 false
2305 }
2306
2307 pub(crate) async fn handle_track_disconnect_audio(&mut self, a: Action) -> bool {
2308 let Action::TrackDisconnectAudio {
2309 ref track_name,
2310 ref from,
2311 from_port,
2312 ref to,
2313 to_port,
2314 } = a
2315 else {
2316 return false;
2317 };
2318
2319 if self
2320 .reject_if_track_frozen(track_name, "routing changes")
2321 .await
2322 {
2323 return true;
2324 }
2325 let track = match self.track_handle_or_err(track_name) {
2326 Ok(track) => track,
2327 Err(e) => {
2328 self.notify_clients(Err(e)).await;
2329 return true;
2330 }
2331 };
2332 if let Err(e) =
2333 track
2334 .lock()
2335 .disconnect_audio_connectable(from.clone(), from_port, to.clone(), to_port)
2336 {
2337 self.notify_clients(Err(e)).await;
2338 return true;
2339 }
2340
2341 false
2342 }
2343
2344 pub(crate) async fn handle_track_connect_midi(&mut self, a: Action) -> bool {
2345 let Action::TrackConnectMidi {
2346 ref track_name,
2347 ref from,
2348 from_port,
2349 ref to,
2350 to_port,
2351 } = a
2352 else {
2353 return false;
2354 };
2355
2356 if self
2357 .reject_if_track_frozen(track_name, "routing changes")
2358 .await
2359 {
2360 return true;
2361 }
2362 let track = match self.track_handle_or_err(track_name) {
2363 Ok(track) => track,
2364 Err(e) => {
2365 self.notify_clients(Err(e)).await;
2366 return true;
2367 }
2368 };
2369 if let Err(e) =
2370 track
2371 .lock()
2372 .connect_midi_connectable(from.clone(), from_port, to.clone(), to_port)
2373 {
2374 self.notify_clients(Err(e)).await;
2375 return true;
2376 }
2377
2378 false
2379 }
2380
2381 pub(crate) async fn handle_track_disconnect_midi(&mut self, a: Action) -> bool {
2382 let Action::TrackDisconnectMidi {
2383 ref track_name,
2384 ref from,
2385 from_port,
2386 ref to,
2387 to_port,
2388 } = a
2389 else {
2390 return false;
2391 };
2392
2393 if self
2394 .reject_if_track_frozen(track_name, "routing changes")
2395 .await
2396 {
2397 return true;
2398 }
2399 let track = match self.track_handle_or_err(track_name) {
2400 Ok(track) => track,
2401 Err(e) => {
2402 self.notify_clients(Err(e)).await;
2403 return true;
2404 }
2405 };
2406 if let Err(e) =
2407 track
2408 .lock()
2409 .disconnect_midi_connectable(from.clone(), from_port, to.clone(), to_port)
2410 {
2411 self.notify_clients(Err(e)).await;
2412 return true;
2413 }
2414
2415 false
2416 }
2417
2418 pub(crate) async fn handle_set_clip_pitch_correction(&mut self, a: Action) -> bool {
2419 let Action::SetClipPitchCorrection {
2420 ref track_name,
2421 clip_index,
2422 ref preview_name,
2423 ref source_name,
2424 source_offset,
2425 source_length,
2426 ref pitch_correction_points,
2427 pitch_correction_frame_likeness,
2428 pitch_correction_inertia_ms,
2429 pitch_correction_formant_compensation,
2430 } = a
2431 else {
2432 return false;
2433 };
2434
2435 self.set_clip_pitch_correction(
2436 track_name,
2437 clip_index,
2438 preview_name.clone(),
2439 source_name.clone(),
2440 source_offset,
2441 source_length,
2442 pitch_correction_points.clone(),
2443 pitch_correction_frame_likeness,
2444 pitch_correction_inertia_ms,
2445 pitch_correction_formant_compensation,
2446 );
2447
2448 false
2449 }
2450
2451 pub(crate) async fn handle_track_remove_audio_output(&mut self, a: Action) -> bool {
2452 let Action::TrackRemoveAudioOutput(ref name) = a else {
2453 return false;
2454 };
2455
2456 let track = match self.track_handle_or_err(name) {
2457 Ok(track) => track,
2458 Err(e) => {
2459 self.notify_clients(Err(e)).await;
2460 return true;
2461 }
2462 };
2463 let (hw_outputs, track_inputs) = {
2464 let state = self.state.lock();
2465 let hw_outputs = self.all_hw_output_audio_ports();
2466 let track_inputs = state
2467 .tracks
2468 .iter()
2469 .filter(|(track_name, _)| *track_name != name)
2470 .flat_map(|(_, handle)| handle.lock().audio.ins.clone())
2471 .collect::<Vec<_>>();
2472 (hw_outputs, track_inputs)
2473 };
2474 if let Err(e) = track.lock().remove_audio_output(&hw_outputs, &track_inputs) {
2475 self.notify_clients(Err(e)).await;
2476 return true;
2477 }
2478
2479 false
2480 }
2481
2482 pub(crate) async fn handle_track_toggle_folder(&mut self, a: Action) -> bool {
2483 let Action::TrackToggleFolder { ref track_name } = a else {
2484 return false;
2485 };
2486
2487 let track = match self.track_handle_or_err(track_name) {
2488 Ok(track) => track,
2489 Err(e) => {
2490 self.notify_clients(Err(e)).await;
2491 return true;
2492 }
2493 };
2494 {
2495 let t = track.lock();
2496 t.folder_open.fetch_not(Ordering::Relaxed);
2497 }
2498 self.notify_clients(Ok(Action::TrackToggleFolder {
2499 track_name: track_name.clone(),
2500 }))
2501 .await;
2502
2503 self.notify_clients(Ok(Action::TrackSetFolder {
2504 track_name: track_name.clone(),
2505 is_folder: track.lock().is_folder,
2506 }))
2507 .await;
2508
2509 false
2510 }
2511
2512 pub(crate) async fn handle_add_grouped_clip(&mut self, a: Action) -> bool {
2513 let Action::AddGroupedClip {
2514 ref track_name,
2515 kind,
2516 ref audio_clip,
2517 ref midi_clip,
2518 } = a
2519 else {
2520 return false;
2521 };
2522
2523 self.add_grouped_clip_to_track(track_name, kind, audio_clip.clone(), midi_clip.clone());
2524 if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
2525 let track_name = track_name.clone();
2526 tokio::task::spawn_blocking(move || {
2527 track.lock().preload_clips();
2528 tracing::debug!(
2529 "Preloaded clips for track '{}' after AddGroupedClip",
2530 track_name
2531 );
2532 });
2533 }
2534
2535 false
2536 }
2537
2538 pub(crate) async fn handle_track_add_audio_input(&mut self, a: Action) -> bool {
2539 let Action::TrackAddAudioInput(ref name) = a else {
2540 return false;
2541 };
2542
2543 let track = match self.track_handle_or_err(name) {
2544 Ok(track) => track,
2545 Err(e) => {
2546 self.notify_clients(Err(e)).await;
2547 return true;
2548 }
2549 };
2550 if let Err(e) = track.lock().add_audio_input() {
2551 self.notify_clients(Err(e)).await;
2552 return true;
2553 }
2554
2555 false
2556 }
2557
2558 pub(crate) async fn handle_track_add_audio_output(&mut self, a: Action) -> bool {
2559 let Action::TrackAddAudioOutput(ref name) = a else {
2560 return false;
2561 };
2562
2563 let track = match self.track_handle_or_err(name) {
2564 Ok(track) => track,
2565 Err(e) => {
2566 self.notify_clients(Err(e)).await;
2567 return true;
2568 }
2569 };
2570 if let Err(e) = track.lock().add_audio_output() {
2571 self.notify_clients(Err(e)).await;
2572 return true;
2573 }
2574
2575 false
2576 }
2577
2578 pub(crate) async fn handle_track_remove_audio_input(&mut self, a: Action) -> bool {
2579 let Action::TrackRemoveAudioInput(ref name) = a else {
2580 return false;
2581 };
2582
2583 let track = match self.track_handle_or_err(name) {
2584 Ok(track) => track,
2585 Err(e) => {
2586 self.notify_clients(Err(e)).await;
2587 return true;
2588 }
2589 };
2590 if let Err(e) = track.lock().remove_audio_input() {
2591 self.notify_clients(Err(e)).await;
2592 return true;
2593 }
2594
2595 false
2596 }
2597
2598 pub(crate) async fn handle_track_midi_cc(&mut self, a: Action) -> bool {
2599 let Action::TrackMidiCc {
2600 ref track_name,
2601 channel,
2602 cc,
2603 value,
2604 } = a
2605 else {
2606 return false;
2607 };
2608
2609 if let Some(track) = self.state.lock().tracks.get(track_name) {
2610 track
2611 .lock()
2612 .rt
2613 .pending_automation_midi_events
2614 .push(MidiEvent::new(
2615 0,
2616 vec![0xB0 | channel.min(15), cc.min(127), value.min(127)],
2617 ));
2618 }
2619
2620 false
2621 }
2622
2623 pub(crate) async fn handle_track_toggle_arm(&mut self, a: Action) -> bool {
2624 let Action::TrackToggleArm(ref name) = a else {
2625 return false;
2626 };
2627
2628 if self.reject_if_track_frozen(name, "arming/disarming").await {
2629 return true;
2630 }
2631 if let Some(track) = self.state.lock().tracks.get(name).cloned() {
2632 track.lock().arm();
2633 let armed = track.lock().armed();
2634 if !armed && self.audio_recordings.contains_key(name) {
2635 self.flush_track_recording(name).await;
2636 }
2637 } else {
2638 tracing::warn!(
2639 "TrackToggleArm for '{}' but track not found in engine",
2640 name
2641 );
2642 }
2643
2644 false
2645 }
2646
2647 pub(crate) async fn handle_track_set_midi_lane_channel(&mut self, a: Action) -> bool {
2648 let Action::TrackSetMidiLaneChannel {
2649 ref track_name,
2650 lane,
2651 channel,
2652 } = a
2653 else {
2654 return false;
2655 };
2656
2657 let track = match self.track_handle_or_err(track_name) {
2658 Ok(track) => track,
2659 Err(e) => {
2660 self.notify_clients(Err(e)).await;
2661 return true;
2662 }
2663 };
2664 track.lock().set_midi_lane_channel(lane, channel);
2665
2666 false
2667 }
2668
2669 pub(crate) async fn handle_track_set_frozen(&mut self, a: Action) -> bool {
2670 let Action::TrackSetFrozen {
2671 ref track_name,
2672 frozen,
2673 } = a
2674 else {
2675 return false;
2676 };
2677
2678 let track = match self.track_handle_or_err(track_name) {
2679 Ok(track) => track,
2680 Err(e) => {
2681 self.notify_clients(Err(e)).await;
2682 return true;
2683 }
2684 };
2685 track.lock().set_frozen(frozen);
2686
2687 false
2688 }
2689
2690 pub(crate) async fn handle_track_clear_default_passthrough(&mut self, a: Action) -> bool {
2691 let Action::TrackClearDefaultPassthrough { ref track_name } = a else {
2692 return false;
2693 };
2694
2695 if self
2696 .reject_if_track_frozen(track_name, "plugin graph editing")
2697 .await
2698 {
2699 return true;
2700 }
2701 let track = match self.track_handle_or_err(track_name) {
2702 Ok(track) => track,
2703 Err(e) => {
2704 self.notify_clients(Err(e)).await;
2705 return true;
2706 }
2707 };
2708 track.lock().clear_default_passthrough();
2709
2710 false
2711 }
2712
2713 pub(crate) async fn handle_set_clip_fade(&mut self, a: Action) -> bool {
2714 let Action::SetClipFade {
2715 ref track_name,
2716 clip_index,
2717 kind,
2718 fade_enabled,
2719 fade_in_samples,
2720 fade_out_samples,
2721 } = a
2722 else {
2723 return false;
2724 };
2725
2726 self.set_clip_fade(
2727 track_name,
2728 clip_index,
2729 kind,
2730 fade_enabled,
2731 fade_in_samples,
2732 fade_out_samples,
2733 );
2734
2735 false
2736 }
2737}