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