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.peaks_file = request.peaks_file;
682 clip.fade_enabled = request.fade_enabled;
683 clip.fade_in_samples = request.fade_in_samples;
684 clip.fade_out_samples = request.fade_out_samples;
685 clip.pitch_correction_preview_name = request.preview_name;
686 clip.pitch_correction_source_name = request.source_name;
687 clip.pitch_correction_source_offset = request.source_offset;
688 clip.pitch_correction_source_length = request.source_length;
689 clip.pitch_correction_points = request.pitch_correction_points;
690 clip.pitch_correction_frame_likeness = request.pitch_correction_frame_likeness;
691 clip.pitch_correction_inertia_ms = request.pitch_correction_inertia_ms;
692 clip.pitch_correction_formant_compensation =
693 request.pitch_correction_formant_compensation;
694 clip.plugin_graph_json = request.plugin_graph_json;
695 Self::push_audio_clip_with_cross_section_fades(&mut track, clip);
696 #[cfg(unix)]
697 track.rt.clip_pitch_shifters.clear();
698 }
699 Kind::MIDI => {
700 let mut clip = MIDIClip::new(
701 request.name.to_string(),
702 request.start,
703 request.start.saturating_add(request.length.max(1)),
704 );
705 clip.id = request.clip_id.to_string();
706 clip.offset = request.offset;
707 let max_lane = track.midi.ins.len().saturating_sub(1);
708 clip.input_channel = request.input_channel.min(max_lane);
709 clip.muted = request.muted;
710 track.midi.push_clip(clip);
711 }
712 }
713 track
714 .rt
715 .session_clip_pool_audio
716 .retain(|clip| clip.id != request.clip_id);
717 track
718 .rt
719 .session_clip_pool_midi
720 .retain(|clip| clip.id != request.clip_id);
721 self.take_clip_from_unused(request.clip_id);
722 }
723 }
724
725 pub(crate) fn audio_clip_from_data(data: &crate::message::AudioClipData) -> AudioClip {
726 let mut clip = AudioClip::new(
727 data.name.clone(),
728 data.start,
729 data.start.saturating_add(data.length.max(1)),
730 );
731 clip.id = data.id.clone();
732 clip.offset = data.offset;
733 clip.input_channel = data.input_channel;
734 clip.muted = data.muted;
735 clip.peaks_file = data.peaks_file.clone();
736 clip.fade_enabled = data.fade_enabled;
737 clip.fade_in_samples = data.fade_in_samples;
738 clip.fade_out_samples = data.fade_out_samples;
739 clip.pitch_correction_preview_name = data.preview_name.clone();
740 clip.pitch_correction_source_name = data.source_name.clone();
741 clip.pitch_correction_source_offset = data.source_offset;
742 clip.pitch_correction_source_length = data.source_length;
743 clip.pitch_correction_points = data.pitch_correction_points.clone();
744 clip.pitch_correction_frame_likeness = data.pitch_correction_frame_likeness;
745 clip.pitch_correction_inertia_ms = data.pitch_correction_inertia_ms;
746 clip.pitch_correction_formant_compensation = data.pitch_correction_formant_compensation;
747 clip.plugin_graph_json = data.plugin_graph_json.clone();
748 clip.grouped_clips = data
749 .grouped_clips
750 .iter()
751 .map(Self::audio_clip_from_data)
752 .collect();
753 for child in &mut clip.grouped_clips {
754 child.fade_enabled = false;
755 child.fade_in_samples = 0;
756 child.fade_out_samples = 0;
757 }
758 clip
759 }
760
761 pub(crate) fn midi_clip_from_data(data: &crate::message::MidiClipData) -> MIDIClip {
762 let mut clip = MIDIClip::new(
763 data.name.clone(),
764 data.start,
765 data.start.saturating_add(data.length.max(1)),
766 );
767 clip.id = data.id.clone();
768 clip.offset = data.offset;
769 clip.input_channel = data.input_channel;
770 clip.muted = data.muted;
771 clip.grouped_clips = data
772 .grouped_clips
773 .iter()
774 .map(Self::midi_clip_from_data)
775 .collect();
776 clip
777 }
778
779 pub(crate) fn add_grouped_clip_to_track(
780 &self,
781 track_name: &str,
782 kind: Kind,
783 audio_clip: Option<crate::message::AudioClipData>,
784 midi_clip: Option<crate::message::MidiClipData>,
785 ) {
786 if let Some(track) = self.state.lock().tracks.get(track_name) {
787 let mut track = track.lock();
788 if track.is_master() {
789 return;
790 }
791 match kind {
792 Kind::Audio => {
793 if let Some(mut clip) = audio_clip.map(|clip| Self::audio_clip_from_data(&clip))
794 {
795 let max_lane = track.audio.ins.len().saturating_sub(1);
796 clip.input_channel = clip.input_channel.min(max_lane);
797 let clip_id = clip.id.clone();
798 Self::push_audio_clip_with_cross_section_fades(&mut track, clip);
799 #[cfg(unix)]
800 track.rt.clip_pitch_shifters.clear();
801 track
802 .rt
803 .session_clip_pool_audio
804 .retain(|clip| clip.id != clip_id);
805 self.take_clip_from_unused(&clip_id);
806 }
807 }
808 Kind::MIDI => {
809 if let Some(mut clip) = midi_clip.map(|clip| Self::midi_clip_from_data(&clip)) {
810 let max_lane = track.midi.ins.len().saturating_sub(1);
811 clip.input_channel = clip.input_channel.min(max_lane);
812 let clip_id = clip.id.clone();
813 track.midi.push_clip(clip);
814 track
815 .rt
816 .session_clip_pool_midi
817 .retain(|clip| clip.id != clip_id);
818 self.take_clip_from_unused(&clip_id);
819 }
820 }
821 }
822 }
823 }
824
825 pub(crate) fn remove_clips_from_track(
826 &self,
827 track_name: &str,
828 kind: Kind,
829 clip_indices: &[usize],
830 ) {
831 if let Some(track) = self.state.lock().tracks.get(track_name) {
832 let track = track.lock();
833 let mut indices = clip_indices.to_vec();
834 indices.sort_unstable();
835 indices.dedup();
836 match kind {
837 Kind::Audio => {
838 for idx in indices.into_iter().rev() {
839 if idx < track.audio.clips().len() {
840 track.audio.remove_clip(idx);
841 }
842 }
843 #[cfg(unix)]
844 {
845 let mut track = track;
846 track.rt.clip_pitch_shifters.clear();
847 }
848 }
849 Kind::MIDI => {
850 for idx in indices.into_iter().rev() {
851 if idx < track.midi.clips().len() {
852 track.midi.remove_clip(idx);
853 }
854 }
855 }
856 }
857 }
858 }
859
860 pub(crate) fn move_clips_to_unused(
861 &self,
862 track_name: &str,
863 kind: Kind,
864 clip_indices: &[usize],
865 ) {
866 if let Some(track) = self.state.lock().tracks.get(track_name) {
867 let mut track = track.lock();
868 let mut indices = clip_indices.to_vec();
869 indices.sort_unstable();
870 indices.dedup();
871 match kind {
872 Kind::Audio => {
873 let mut moved = Vec::new();
874 for idx in indices.into_iter().rev() {
875 if idx < track.audio.clips().len()
876 && let Some(clip) = track.audio.remove_clip(idx)
877 {
878 if track
879 .rt
880 .session_slots
881 .values()
882 .any(|slot| slot.clip_id == clip.id)
883 {
884 track.rt.session_clip_pool_audio.push(clip.clone());
885 }
886 moved.push(audio_clip_to_data(&clip));
887 }
888 }
889 moved.reverse();
890 self.state.lock().unused_audio_clips.extend(moved);
891 #[cfg(unix)]
892 track.rt.clip_pitch_shifters.clear();
893 }
894 Kind::MIDI => {
895 let mut moved = Vec::new();
896 for idx in indices.into_iter().rev() {
897 if idx < track.midi.clips().len()
898 && let Some(clip) = track.midi.remove_clip(idx)
899 {
900 if track
901 .rt
902 .session_slots
903 .values()
904 .any(|slot| slot.clip_id == clip.id)
905 {
906 track.rt.session_clip_pool_midi.push(clip.clone());
907 }
908 moved.push(midi_clip_to_data(&clip));
909 }
910 }
911 moved.reverse();
912 self.state.lock().unused_midi_clips.extend(moved);
913 }
914 }
915 }
916 }
917
918 pub(crate) fn delete_unused_clips(&self, clip_ids: &[String]) {
919 let mut state = self.state.lock();
920 state
921 .unused_audio_clips
922 .retain(|clip| !clip_ids.contains(&clip.id));
923 state
924 .unused_midi_clips
925 .retain(|clip| !clip_ids.contains(&clip.id));
926 for track in state.tracks.values() {
927 let mut track = track.lock();
928 track
929 .rt
930 .session_clip_pool_audio
931 .retain(|clip| !clip_ids.contains(&clip.id));
932 track
933 .rt
934 .session_clip_pool_midi
935 .retain(|clip| !clip_ids.contains(&clip.id));
936 }
937 }
938
939 pub(crate) fn set_unused_clips(
940 &self,
941 audio: Vec<crate::message::AudioClipData>,
942 midi: Vec<crate::message::MidiClipData>,
943 ) {
944 let mut state = self.state.lock();
945 state.unused_audio_clips = audio;
946 state.unused_midi_clips = midi;
947 for track in state.tracks.values() {
951 let mut track = track.lock();
952 let slot_clip_ids: Vec<String> = track
953 .rt
954 .session_slots
955 .values()
956 .map(|slot| slot.clip_id.clone())
957 .collect();
958 for clip_id in slot_clip_ids {
959 let on_track = track.audio.clips().iter().any(|clip| clip.id == clip_id)
960 || track.midi.clips().iter().any(|clip| clip.id == clip_id);
961 let in_pool = track
962 .rt
963 .session_clip_pool_audio
964 .iter()
965 .any(|clip| clip.id == clip_id)
966 || track
967 .rt
968 .session_clip_pool_midi
969 .iter()
970 .any(|clip| clip.id == clip_id);
971 if on_track || in_pool {
972 continue;
973 }
974 if let Some(data) = state
975 .unused_audio_clips
976 .iter()
977 .find(|clip| clip.id == clip_id)
978 {
979 track
980 .rt
981 .session_clip_pool_audio
982 .push(Arc::new(Self::audio_clip_from_data(data)));
983 } else if let Some(data) = state
984 .unused_midi_clips
985 .iter()
986 .find(|clip| clip.id == clip_id)
987 {
988 track
989 .rt
990 .session_clip_pool_midi
991 .push(Arc::new(Self::midi_clip_from_data(data)));
992 }
993 }
994 track.rt.prune_session_clip_pool();
995 }
996 }
997
998 pub(crate) fn take_clip_from_unused(&self, clip_id: &str) {
999 let mut state = self.state.lock();
1000 state.unused_audio_clips.retain(|clip| clip.id != clip_id);
1001 state.unused_midi_clips.retain(|clip| clip.id != clip_id);
1002 }
1003
1004 pub(crate) fn rename_clip_references(
1005 &self,
1006 track_name: &str,
1007 kind: Kind,
1008 clip_index: usize,
1009 new_name: &str,
1010 ) {
1011 let Some(track) = self.state.lock().tracks.get(track_name).cloned() else {
1012 return;
1013 };
1014 let track = track.lock();
1015 let old_name = match kind {
1016 Kind::Audio => {
1017 if clip_index >= track.audio.clips().len() {
1018 return;
1019 }
1020 track.audio.clips().get(clip_index).unwrap().name.clone()
1021 }
1022 Kind::MIDI => {
1023 if clip_index >= track.midi.clips().len() {
1024 return;
1025 }
1026 track.midi.clips().get(clip_index).unwrap().name.clone()
1027 }
1028 };
1029
1030 let new_file_name = match kind {
1031 Kind::Audio => format!("audio/{}.wav", new_name),
1032 Kind::MIDI => {
1033 let ext = std::path::Path::new(&old_name)
1034 .extension()
1035 .and_then(|e| e.to_str())
1036 .map(|s| s.to_ascii_lowercase())
1037 .filter(|e| e == "mid" || e == "midi")
1038 .unwrap_or_else(|| "mid".to_string());
1039 format!("midi/{}.{}", new_name, ext)
1040 }
1041 };
1042 let _ = track;
1043
1044 for other_track in self.state.lock().tracks.values() {
1045 let other_track = other_track.lock();
1046 match kind {
1047 Kind::Audio => {
1048 let clips = other_track.audio.clips();
1049 for idx in 0..clips.len() {
1050 other_track.audio.update_clip(idx, |clip| {
1051 if clip.name == old_name {
1052 clip.name = new_file_name.clone();
1053 }
1054 if clip.pitch_correction_source_name.as_deref()
1055 == Some(old_name.as_str())
1056 {
1057 clip.pitch_correction_source_name = Some(new_file_name.clone());
1058 }
1059 });
1060 }
1061 }
1062 Kind::MIDI => {
1063 let clips = other_track.midi.clips();
1064 for idx in 0..clips.len() {
1065 other_track.midi.update_clip(idx, |clip| {
1066 if clip.name == old_name {
1067 clip.name = new_file_name.clone();
1068 }
1069 });
1070 }
1071 }
1072 }
1073 }
1074 }
1075
1076 pub(crate) fn set_clip_identity(
1077 &self,
1078 track_name: &str,
1079 kind: Kind,
1080 clip_index: usize,
1081 new_id: &str,
1082 new_name: &str,
1083 ) {
1084 let Some(track) = self.state.lock().tracks.get(track_name).cloned() else {
1085 return;
1086 };
1087 let track = track.lock();
1088 match kind {
1089 Kind::Audio => {
1090 track.audio.update_clip(clip_index, |clip| {
1091 clip.id = new_id.to_string();
1092 clip.name = new_name.to_string();
1093 });
1094 #[cfg(unix)]
1095 {
1096 let mut track = track;
1097 track.rt.clip_pitch_shifters.clear();
1098 }
1099 }
1100 Kind::MIDI => {
1101 track.midi.update_clip(clip_index, |clip| {
1102 clip.id = new_id.to_string();
1103 clip.name = new_name.to_string();
1104 });
1105 }
1106 }
1107 }
1108
1109 pub(crate) fn set_clip_fade(
1110 &self,
1111 track_name: &str,
1112 clip_index: usize,
1113 kind: Kind,
1114 fade_enabled: bool,
1115 fade_in_samples: usize,
1116 fade_out_samples: usize,
1117 ) {
1118 let Some(track) = self.state.lock().tracks.get(track_name).cloned() else {
1119 return;
1120 };
1121 let track = track.lock();
1122 match kind {
1123 Kind::Audio => {
1124 track.audio.update_clip(clip_index, |clip| {
1125 clip.fade_enabled = fade_enabled;
1126 clip.fade_in_samples = fade_in_samples;
1127 clip.fade_out_samples = fade_out_samples;
1128 });
1129 }
1130 Kind::MIDI => {}
1131 }
1132 }
1133
1134 pub(crate) fn set_clip_bounds(
1135 &self,
1136 track_name: &str,
1137 clip_index: usize,
1138 kind: Kind,
1139 start: usize,
1140 length: usize,
1141 offset: usize,
1142 ) {
1143 let Some(track) = self.state.lock().tracks.get(track_name).cloned() else {
1144 return;
1145 };
1146 let track = track.lock();
1147 match kind {
1148 Kind::Audio => {
1149 track.audio.update_clip(clip_index, |clip| {
1150 clip.start = start;
1151 clip.end = start.saturating_add(length.max(1));
1152 clip.offset = offset;
1153 clip.pitch_correction_preview_name = None;
1154 clip.pitch_correction_source_name = None;
1155 clip.pitch_correction_source_offset = None;
1156 clip.pitch_correction_source_length = None;
1157 clip.pitch_correction_points.clear();
1158 clip.pitch_correction_frame_likeness = None;
1159 clip.pitch_correction_inertia_ms = None;
1160 clip.pitch_correction_formant_compensation = None;
1161 });
1162 #[cfg(unix)]
1163 {
1164 let mut track = track;
1165 track.rt.clip_pitch_shifters.clear();
1166 }
1167 }
1168 Kind::MIDI => {
1169 track.midi.update_clip(clip_index, |clip| {
1170 clip.start = start;
1171 clip.end = start.saturating_add(length.max(1));
1172 clip.offset = offset;
1173 });
1174 }
1175 }
1176 }
1177
1178 pub(crate) fn set_clip_source_name(
1179 &self,
1180 track_name: &str,
1181 clip_index: usize,
1182 kind: Kind,
1183 name: String,
1184 ) {
1185 let Some(track) = self.state.lock().tracks.get(track_name).cloned() else {
1186 return;
1187 };
1188 let track = track.lock();
1189 match kind {
1190 Kind::Audio => {
1191 track.audio.update_clip(clip_index, |clip| {
1192 clip.name = name;
1193 });
1194 #[cfg(unix)]
1195 {
1196 let mut track = track;
1197 track.rt.clip_pitch_shifters.clear();
1198 }
1199 }
1200 Kind::MIDI => {
1201 track.midi.update_clip(clip_index, |clip| {
1202 clip.name = name;
1203 });
1204 }
1205 }
1206 }
1207
1208 pub(crate) fn set_clip_muted(
1209 &self,
1210 track_name: &str,
1211 clip_index: usize,
1212 kind: Kind,
1213 muted: bool,
1214 ) {
1215 let Some(track) = self.state.lock().tracks.get(track_name).cloned() else {
1216 return;
1217 };
1218 let track = track.lock();
1219 match kind {
1220 Kind::Audio => {
1221 track.audio.update_clip(clip_index, |clip| {
1222 clip.muted = muted;
1223 });
1224 }
1225 Kind::MIDI => {
1226 track.midi.update_clip(clip_index, |clip| {
1227 clip.muted = muted;
1228 });
1229 }
1230 }
1231 }
1232
1233 #[allow(clippy::too_many_arguments)]
1234 pub(crate) fn set_clip_pitch_correction(
1235 &self,
1236 track_name: &str,
1237 clip_index: usize,
1238 preview_name: Option<String>,
1239 source_name: Option<String>,
1240 source_offset: Option<usize>,
1241 source_length: Option<usize>,
1242 pitch_correction_points: Vec<crate::message::PitchCorrectionPointData>,
1243 pitch_correction_frame_likeness: Option<f32>,
1244 pitch_correction_inertia_ms: Option<u16>,
1245 pitch_correction_formant_compensation: Option<bool>,
1246 ) {
1247 if let Some(track) = self.state.lock().tracks.get(track_name) {
1248 let track = track.lock();
1249 track.audio.update_clip(clip_index, |clip| {
1250 clip.pitch_correction_preview_name = preview_name;
1251 clip.pitch_correction_source_name = source_name;
1252 clip.pitch_correction_source_offset = source_offset;
1253 clip.pitch_correction_source_length = source_length;
1254 clip.pitch_correction_points = pitch_correction_points;
1255 clip.pitch_correction_frame_likeness = pitch_correction_frame_likeness;
1256 clip.pitch_correction_inertia_ms = pitch_correction_inertia_ms;
1257 clip.pitch_correction_formant_compensation = pitch_correction_formant_compensation;
1258 });
1259 #[cfg(unix)]
1260 {
1261 let mut track = track;
1262 track.rt.clip_pitch_shifters.clear();
1263 }
1264 }
1265 }
1266
1267 pub fn check_if_leads_to_kind(
1268 &self,
1269 kind: Kind,
1270 current_track_name: &str,
1271 target_track_name: &str,
1272 ) -> bool {
1273 routing::would_create_cycle(
1274 &target_track_name.to_string(),
1275 ¤t_track_name.to_string(),
1276 |track_name| self.connected_neighbors(kind, track_name),
1277 )
1278 }
1279
1280 pub(crate) fn connected_neighbors(&self, kind: Kind, current_track_name: &str) -> Vec<String> {
1281 let state = self.state.lock();
1282 let mut found_neighbors = Vec::new();
1283
1284 if let Some(current_track_handle) = state.tracks.get(current_track_name) {
1285 let current_track = current_track_handle.lock();
1286
1287 match kind {
1288 Kind::Audio => {
1289 for out_port in ¤t_track.audio.outs {
1290 let conns = out_port.connections();
1291 for conn in conns.iter() {
1292 for (name, next_track_handle) in &state.tracks {
1293 let next_track = next_track_handle.lock();
1294 let is_connected = next_track
1295 .audio
1296 .ins
1297 .iter()
1298 .any(|ins_port| Arc::ptr_eq(ins_port, conn));
1299
1300 if is_connected {
1301 found_neighbors.push(name.clone());
1302 }
1303 }
1304 }
1305 }
1306 }
1307 Kind::MIDI => {
1308 for out_port in ¤t_track.midi.outs {
1309 let conns = out_port.connections();
1310 for conn in conns.iter() {
1311 for (name, next_track_handle) in &state.tracks {
1312 let next_track = next_track_handle.lock();
1313 let is_connected = next_track
1314 .midi
1315 .ins
1316 .iter()
1317 .any(|ins_port| Arc::ptr_eq(ins_port, conn));
1318
1319 if is_connected {
1320 found_neighbors.push(name.clone());
1321 }
1322 }
1323 }
1324 }
1325 }
1326 }
1327 }
1328 found_neighbors
1329 }
1330
1331 pub(crate) fn find_audio_io_owner(
1332 &self,
1333 state: &crate::state::State,
1334 io: &std::sync::Arc<crate::audio::io::AudioIO>,
1335 ) -> Option<(String, usize)> {
1336 for (name, track) in &state.tracks {
1337 let t = track.lock();
1338 for (i, out) in t.audio.outs.iter().enumerate() {
1339 if std::sync::Arc::ptr_eq(out, io) {
1340 return Some((name.clone(), i));
1341 }
1342 }
1343 for (i, inp) in t.audio.ins.iter().enumerate() {
1344 if std::sync::Arc::ptr_eq(inp, io) {
1345 return Some((name.clone(), i));
1346 }
1347 }
1348 }
1349 None
1350 }
1351
1352 pub(crate) fn find_midi_io_owner(
1353 &self,
1354 state: &crate::state::State,
1355 io: &std::sync::Arc<crate::midi::io::MIDIIO>,
1356 ) -> Option<(String, usize, bool)> {
1357 for (name, track) in &state.tracks {
1358 let t = track.lock();
1359 for (i, out) in t.midi.outs.iter().enumerate() {
1360 if std::sync::Arc::ptr_eq(out, io) {
1361 return Some((name.clone(), i, false));
1362 }
1363 }
1364 for (i, inp) in t.midi.ins.iter().enumerate() {
1365 if std::sync::Arc::ptr_eq(inp, io) {
1366 return Some((name.clone(), i, true));
1367 }
1368 }
1369 }
1370 None
1371 }
1372
1373 pub(crate) fn collect_descendant_track_names(&self, name: &str, out: &mut Vec<String>) {
1374 let child_arcs: Vec<crate::state::TrackHandle> = {
1377 let state = self.state.lock();
1378 if let Some(track) = state.tracks.get(name) {
1379 track.lock().child_tracks.clone()
1380 } else {
1381 Vec::new()
1382 }
1383 };
1384 for child in child_arcs {
1385 let child_name = { child.lock().name.clone() };
1386 self.collect_descendant_track_names(&child_name, out);
1387 out.push(child_name);
1388 }
1389 }
1390
1391 pub(crate) async fn remove_single_track(&mut self, name: &str) {
1392 let children: Vec<crate::state::TrackHandle> = {
1393 let state = self.state.lock();
1394 if let Some(removed) = state.tracks.get(name).cloned() {
1395 removed.lock().child_tracks.clone()
1396 } else {
1397 Vec::new()
1398 }
1399 };
1400 let parent_name: Option<String> = {
1401 let state = self.state.lock();
1402 state
1403 .tracks
1404 .get(name)
1405 .map(|t| t.lock().parent_track.clone())
1406 .unwrap_or(None)
1407 };
1408 if let Some(parent_name) = parent_name {
1409 let state = self.state.lock();
1410 if let Some(parent) = state.tracks.get(&parent_name).cloned() {
1411 let mut parent = parent.lock();
1412 parent.child_tracks.retain(|c| c.lock().name != *name);
1413 }
1414 }
1415 if let Some(removed_track) = self.state.lock().tracks.get(name).cloned() {
1416 for child in children {
1417 let removed = removed_track.lock();
1418 child.lock().disconnect_from_parent(&removed);
1419 child.lock().parent_track = None;
1420 }
1421 }
1422 self.state.lock().tracks.remove(name);
1423 self.audio_recordings.remove(name);
1424 self.midi_recordings.remove(name);
1425 self.midi_hw_in_routes.retain(|r| r.to_track != *name);
1426 self.midi_hw_out_routes.retain(|r| r.from_track != *name);
1427 if self
1428 .pending_midi_learn
1429 .as_ref()
1430 .is_some_and(|(track_name, _, _)| track_name == name)
1431 {
1432 self.pending_midi_learn = None;
1433 }
1434 }
1435
1436 pub(crate) fn prepare_inverse_actions(
1437 &self,
1438 action_to_process: &Action,
1439 record_history: bool,
1440 suppress_timing_history: bool,
1441 ) -> Option<Vec<Action>> {
1442 let mut extra_inverse_actions: Vec<Action> = Vec::new();
1443 if record_history
1444 && !self.history_suspended
1445 && let Action::RemoveTrack(track_name) = action_to_process
1446 {
1447 for route in self
1448 .midi_hw_in_routes
1449 .iter()
1450 .filter(|route| &route.to_track == track_name)
1451 {
1452 extra_inverse_actions.push(Action::Connect {
1453 from_track: format!("midi:hw:in:{}", route.device),
1454 from_port: 0,
1455 to_track: route.to_track.clone(),
1456 to_port: route.to_port,
1457 kind: Kind::MIDI,
1458 });
1459 }
1460 for route in self
1461 .midi_hw_out_routes
1462 .iter()
1463 .filter(|route| &route.from_track == track_name)
1464 {
1465 extra_inverse_actions.push(Action::Connect {
1466 from_track: route.from_track.clone(),
1467 from_port: route.from_port,
1468 to_track: format!("midi:hw:out:{}", route.device),
1469 to_port: 0,
1470 kind: Kind::MIDI,
1471 });
1472 }
1473 }
1474 if record_history
1475 && !self.history_suspended
1476 && matches!(action_to_process, Action::ClearAllMidiLearnBindings)
1477 {
1478 if let Some(binding) = self.global_midi_learn_play_pause.clone() {
1479 extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
1480 target: crate::message::GlobalMidiLearnTarget::PlayPause,
1481 binding: Some(binding),
1482 });
1483 }
1484 if let Some(binding) = self.global_midi_learn_stop.clone() {
1485 extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
1486 target: crate::message::GlobalMidiLearnTarget::Stop,
1487 binding: Some(binding),
1488 });
1489 }
1490 if let Some(binding) = self.global_midi_learn_record_toggle.clone() {
1491 extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
1492 target: crate::message::GlobalMidiLearnTarget::RecordToggle,
1493 binding: Some(binding),
1494 });
1495 }
1496 for (key, binding) in self.session_midi_learn_slots.clone() {
1497 extra_inverse_actions.push(Action::SetSessionMidiLearnBinding {
1498 target: crate::message::SessionMidiLearnTarget::Slot {
1499 track_name: key.0,
1500 scene_index: key.1,
1501 },
1502 binding: Some(binding),
1503 });
1504 }
1505 for (scene_index, binding) in self.session_midi_learn_scenes.clone() {
1506 extra_inverse_actions.push(Action::SetSessionMidiLearnBinding {
1507 target: crate::message::SessionMidiLearnTarget::Scene(scene_index),
1508 binding: Some(binding),
1509 });
1510 }
1511 for (track_name, binding) in self.session_midi_learn_stop_track.clone() {
1512 extra_inverse_actions.push(Action::SetSessionMidiLearnBinding {
1513 target: crate::message::SessionMidiLearnTarget::StopTrack(track_name),
1514 binding: Some(binding),
1515 });
1516 }
1517 if let Some(binding) = self.session_midi_learn_stop_all.clone() {
1518 extra_inverse_actions.push(Action::SetSessionMidiLearnBinding {
1519 target: crate::message::SessionMidiLearnTarget::StopAll,
1520 binding: Some(binding),
1521 });
1522 }
1523 }
1524 let mut inverse_actions = if record_history
1525 && !suppress_timing_history
1526 && should_record(action_to_process)
1527 && !self.history_suspended
1528 {
1529 create_inverse_actions(action_to_process, self.state.as_ref()).map(|mut actions| {
1530 actions.extend(extra_inverse_actions);
1531 actions
1532 })
1533 } else {
1534 None
1535 };
1536 if record_history && !suppress_timing_history && !self.history_suspended {
1537 match action_to_process {
1538 Action::SetTempo(_) => {
1539 inverse_actions = Some(vec![Action::SetTempo(self.tempo_bpm)]);
1540 }
1541 Action::SetLoopEnabled(_) => {
1542 inverse_actions = Some(vec![Action::SetLoopEnabled(self.loop_enabled)]);
1543 }
1544 Action::SetLoopRange(_) => {
1545 inverse_actions = Some(vec![
1546 Action::SetLoopRange(self.loop_range_samples),
1547 Action::SetLoopEnabled(self.loop_enabled),
1548 ]);
1549 }
1550 Action::SetPunchEnabled(_) => {
1551 inverse_actions = Some(vec![Action::SetPunchEnabled(self.punch_enabled)]);
1552 }
1553 Action::SetPunchRange(_) => {
1554 inverse_actions = Some(vec![
1555 Action::SetPunchRange(self.punch_range_samples),
1556 Action::SetPunchEnabled(self.punch_enabled),
1557 ]);
1558 }
1559 Action::SetMetronomeEnabled(_) => {
1560 inverse_actions =
1561 Some(vec![Action::SetMetronomeEnabled(self.metronome_enabled)]);
1562 }
1563 Action::SetTimeSignature { .. } => {
1564 inverse_actions = Some(vec![Action::SetTimeSignature {
1565 numerator: self.tsig_num,
1566 denominator: self.tsig_denom,
1567 }]);
1568 }
1569 Action::SetTempoMap { .. } => {
1570 inverse_actions = Some(vec![Action::SetTempoMap {
1571 tempo_points: self.tempo_points.clone(),
1572 time_signature_points: self.time_signature_points.clone(),
1573 }]);
1574 }
1575 Action::SetClipPlaybackEnabled(_) => {
1576 inverse_actions = Some(vec![Action::SetClipPlaybackEnabled(
1577 self.clip_playback_enabled,
1578 )]);
1579 }
1580 Action::SetRecordEnabled(_) => {
1581 inverse_actions = Some(vec![Action::SetRecordEnabled(self.record_enabled)]);
1582 }
1583 Action::SetGlobalMidiLearnBinding { target, .. } => {
1584 let binding = match target {
1585 crate::message::GlobalMidiLearnTarget::PlayPause => {
1586 self.global_midi_learn_play_pause.clone()
1587 }
1588 crate::message::GlobalMidiLearnTarget::Stop => {
1589 self.global_midi_learn_stop.clone()
1590 }
1591 crate::message::GlobalMidiLearnTarget::RecordToggle => {
1592 self.global_midi_learn_record_toggle.clone()
1593 }
1594 };
1595 inverse_actions = Some(vec![Action::SetGlobalMidiLearnBinding {
1596 target: *target,
1597 binding,
1598 }]);
1599 }
1600 Action::SetModulators(_) => {
1601 inverse_actions = Some(vec![Action::SetModulators(self.modulators.clone())]);
1602 }
1603 Action::SetSessionMidiLearnBinding { target, .. } => {
1604 let binding = match target {
1605 crate::message::SessionMidiLearnTarget::Slot {
1606 track_name,
1607 scene_index,
1608 } => self
1609 .session_midi_learn_slots
1610 .get(&(track_name.clone(), *scene_index))
1611 .cloned(),
1612 crate::message::SessionMidiLearnTarget::Scene(scene_index) => {
1613 self.session_midi_learn_scenes.get(scene_index).cloned()
1614 }
1615 crate::message::SessionMidiLearnTarget::StopTrack(track_name) => {
1616 self.session_midi_learn_stop_track.get(track_name).cloned()
1617 }
1618 crate::message::SessionMidiLearnTarget::StopAll => {
1619 self.session_midi_learn_stop_all.clone()
1620 }
1621 };
1622 inverse_actions = Some(vec![Action::SetSessionMidiLearnBinding {
1623 target: target.clone(),
1624 binding,
1625 }]);
1626 }
1627 _ => {}
1628 }
1629 }
1630 inverse_actions
1631 }
1632
1633 pub(crate) async fn handle_add_track(
1634 &mut self,
1635 name: String,
1636 audio_ins: usize,
1637 midi_ins: usize,
1638 audio_outs: usize,
1639 midi_outs: usize,
1640 folder: bool,
1641 ) {
1642 let tracks = &mut self.state.lock().tracks;
1643 if tracks.contains_key(&name) {
1644 self.notify_clients(Err(format!("Track {} already exists", name)))
1645 .await;
1646 return;
1647 }
1648 if !folder && !self.history_suspended && audio_ins + midi_ins + audio_outs + midi_outs == 0
1649 {
1650 self.notify_clients(Err(
1651 "Track must have at least one audio or MIDI input/output".to_string(),
1652 ))
1653 .await;
1654 return;
1655 }
1656 let maybe_hw = self
1657 .hw_driver_info
1658 .map(|info| (info.cycle_samples, info.sample_rate as f64));
1659
1660 if let Some((chsamples, sample_rate)) = maybe_hw {
1661 let track = if folder {
1662 Track::new_folder(
1663 name.clone(),
1664 audio_ins,
1665 audio_outs,
1666 midi_ins,
1667 midi_outs,
1668 chsamples,
1669 sample_rate,
1670 )
1671 } else {
1672 Track::new(
1673 name.clone(),
1674 audio_ins,
1675 audio_outs,
1676 midi_ins,
1677 midi_outs,
1678 chsamples,
1679 sample_rate,
1680 )
1681 };
1682 tracks.insert(name.clone(), Arc::new(track));
1683 if let Some(track) = tracks.get(&name) {
1684 let mut t = track.lock();
1685 t.set_clip_playback_enabled(self.clip_playback_enabled);
1686 t.set_transport_timing(self.tempo_bpm, self.tsig_num, self.tsig_denom);
1687 t.set_session_base_dir(self.session_dir.clone());
1688 }
1689 } else {
1690 self.notify_clients(Err(
1691 "Engine needs to open audio device before adding audio track".to_string(),
1692 ))
1693 .await;
1694 }
1695 }
1696
1697 pub(crate) async fn handle_remove_track(&mut self, name: String, record_history: bool) {
1698 let mut descendant_names = Vec::new();
1699 self.collect_descendant_track_names(&name, &mut descendant_names);
1700 let names_to_remove: Vec<String> = descendant_names
1701 .iter()
1702 .cloned()
1703 .chain(std::iter::once(name.clone()))
1704 .collect();
1705
1706 let combined_inverse = if record_history && !self.history_suspended {
1707 let mut inv = Vec::new();
1708 for n in &names_to_remove {
1709 if let Some(mut actions) =
1710 create_inverse_actions(&Action::RemoveTrack(n.clone()), self.state.as_ref())
1711 {
1712 inv.append(&mut actions);
1713 }
1714 for route in self.midi_hw_in_routes.iter().filter(|r| &r.to_track == n) {
1715 inv.push(Action::Connect {
1716 from_track: format!("midi:hw:in:{}", route.device),
1717 from_port: 0,
1718 to_track: route.to_track.clone(),
1719 to_port: route.to_port,
1720 kind: Kind::MIDI,
1721 });
1722 }
1723 for route in self
1724 .midi_hw_out_routes
1725 .iter()
1726 .filter(|r| &r.from_track == n)
1727 {
1728 inv.push(Action::Connect {
1729 from_track: route.from_track.clone(),
1730 from_port: route.from_port,
1731 to_track: format!("midi:hw:out:{}", route.device),
1732 to_port: 0,
1733 kind: Kind::MIDI,
1734 });
1735 }
1736 }
1737
1738 let mut add_tracks = Vec::new();
1742 let mut connections = Vec::new();
1743 let mut rest = Vec::new();
1744 for action in inv {
1745 match action {
1746 Action::AddTrack { .. } => add_tracks.push(action),
1747 Action::Connect { .. } => connections.push(action),
1748 _ => rest.push(action),
1749 }
1750 }
1751 let mut ordered = add_tracks;
1752 ordered.extend(rest);
1753 ordered.extend(connections);
1754 ordered
1755 } else {
1756 Vec::new()
1757 };
1758
1759 for n in &descendant_names {
1760 self.remove_single_track(n).await;
1761 self.notify_clients(Ok(Action::RemoveTrack(n.clone())))
1762 .await;
1763 }
1764 self.remove_single_track(&name).await;
1765
1766 if record_history && !self.history_suspended && !combined_inverse.is_empty() {
1767 self.history.record(UndoEntry {
1768 forward_actions: vec![Action::RemoveTrack(name.clone())],
1769 inverse_actions: combined_inverse,
1770 });
1771 }
1772 }
1773
1774 pub(crate) async fn handle_connect(
1775 &mut self,
1776 from_track: &str,
1777 from_port: usize,
1778 to_track: &str,
1779 to_port: usize,
1780 kind: Kind,
1781 ) {
1782 match kind {
1783 Kind::Audio => {
1784 if from_track == to_track && from_track != "hw:in" && to_track != "hw:out" {
1785 let Some(track) = self.state.lock().tracks.get(from_track).cloned() else {
1786 self.notify_clients(Err(format!("Track '{from_track}' not found")))
1787 .await;
1788 return;
1789 };
1790 let result = {
1791 let mut track = track.lock();
1792 track.connect_audio_connectable(
1793 crate::connectable::ConnectableRef::TrackInput,
1794 to_port,
1795 crate::connectable::ConnectableRef::TrackOutput,
1796 from_port,
1797 )
1798 };
1799 if let Err(e) = result {
1800 self.notify_clients(Err(e)).await;
1801 return;
1802 }
1803 self.notify_clients(Ok(Action::TrackConnectAudio {
1804 track_name: from_track.to_string(),
1805 from: crate::connectable::ConnectableRef::TrackInput,
1806 from_port: to_port,
1807 to: crate::connectable::ConnectableRef::TrackOutput,
1808 to_port: from_port,
1809 }))
1810 .await;
1811 return;
1812 }
1813 let (from_audio_io, to_audio_io) =
1814 self.resolve_audio_route_ports(from_track, from_port, to_track, to_port);
1815 match (from_audio_io, to_audio_io) {
1816 (Some(source), Some(target)) => {
1817 if from_track != "hw:in"
1818 && to_track != "hw:out"
1819 && self.check_if_leads_to_kind(Kind::Audio, to_track, from_track)
1820 {
1821 self.notify_clients(Err("Circular routing is not allowed!".into()))
1822 .await;
1823 return;
1824 }
1825 crate::audio::io::AudioIO::connect(&source, &target);
1826 }
1827 (None, _) => {
1828 self.notify_clients(Err(format!(
1829 "Source track '{}' not found",
1830 from_track
1831 )))
1832 .await;
1833 }
1834 (_, None) => {
1835 self.notify_clients(Err(format!(
1836 "Destination track '{}' not found",
1837 to_track
1838 )))
1839 .await;
1840 }
1841 }
1842 }
1843 Kind::MIDI => {
1844 let from_hw_in_device = Self::midi_hw_in_device(from_track);
1845 let to_hw_out_device = Self::midi_hw_out_device(to_track);
1846 let from_is_invalid_hw = Self::midi_hw_out_device(from_track).is_some();
1847 let to_is_invalid_hw = Self::midi_hw_in_device(to_track).is_some();
1848
1849 if from_is_invalid_hw || to_is_invalid_hw {
1850 self.notify_clients(Err(
1851 "Invalid MIDI hardware connection direction".to_string()
1852 ))
1853 .await;
1854 return;
1855 }
1856
1857 if from_hw_in_device.is_none()
1858 && to_hw_out_device.is_none()
1859 && from_track != to_track
1860 && self.check_if_leads_to_kind(Kind::MIDI, to_track, from_track)
1861 {
1862 self.notify_clients(Err("Circular routing is not allowed!".into()))
1863 .await;
1864 return;
1865 }
1866
1867 if from_track == to_track {
1868 let Some(track) = self.state.lock().tracks.get(from_track).cloned() else {
1869 self.notify_clients(Err(format!("Track '{from_track}' not found")))
1870 .await;
1871 return;
1872 };
1873 let result = {
1874 let mut track = track.lock();
1875 track.connect_midi_connectable(
1876 crate::connectable::ConnectableRef::TrackInput,
1877 to_port,
1878 crate::connectable::ConnectableRef::TrackOutput,
1879 from_port,
1880 )
1881 };
1882 if let Err(e) = result {
1883 self.notify_clients(Err(e)).await;
1884 return;
1885 }
1886 self.notify_clients(Ok(Action::TrackConnectMidi {
1887 track_name: from_track.to_string(),
1888 from: crate::connectable::ConnectableRef::TrackInput,
1889 from_port: to_port,
1890 to: crate::connectable::ConnectableRef::TrackOutput,
1891 to_port: from_port,
1892 }))
1893 .await;
1894 return;
1895 }
1896
1897 let state = self.state.lock();
1898 let from_track_handle = state.tracks.get(from_track);
1899 let to_track_handle = state.tracks.get(to_track);
1900
1901 if let (Some(from_device), Some(to_device)) = (from_hw_in_device, to_hw_out_device)
1902 {
1903 let route = MidiHwThruRoute {
1904 from_device: from_device.to_string(),
1905 to_device: to_device.to_string(),
1906 };
1907 if !self.midi_hw_thru_routes.iter().any(|r| r == &route) {
1908 self.midi_hw_thru_routes.push(route);
1909 }
1910 } else if let Some(device) = from_hw_in_device {
1911 if let Some(t_t) = to_track_handle {
1912 if t_t.lock().midi.ins.get(to_port).is_none() {
1913 self.notify_clients(Err(format!(
1914 "MIDI input port {} not found on track '{}'",
1915 to_port, to_track
1916 )))
1917 .await;
1918 return;
1919 }
1920 let route = MidiHwInRoute {
1921 device: device.to_string(),
1922 to_track: to_track.to_string(),
1923 to_port,
1924 };
1925 if !self.midi_hw_in_routes.iter().any(|r| r == &route) {
1926 self.midi_hw_in_routes.push(route);
1927 }
1928 } else {
1929 self.notify_clients(Err(format!(
1930 "MIDI destination track not found: {}",
1931 to_track
1932 )))
1933 .await;
1934 }
1935 } else if let Some(device) = to_hw_out_device {
1936 if let Some(f_t) = from_track_handle {
1937 if f_t.lock().midi.outs.get(from_port).is_none() {
1938 self.notify_clients(Err(format!(
1939 "MIDI output port {} not found on track '{}'",
1940 from_port, from_track
1941 )))
1942 .await;
1943 return;
1944 }
1945 let route = MidiHwOutRoute {
1946 from_track: from_track.to_string(),
1947 from_port,
1948 device: device.to_string(),
1949 };
1950 if !self.midi_hw_out_routes.iter().any(|r| r == &route) {
1951 self.midi_hw_out_routes.push(route);
1952 }
1953 } else {
1954 self.notify_clients(Err(format!(
1955 "MIDI source track not found: {}",
1956 from_track
1957 )))
1958 .await;
1959 }
1960 } else {
1961 match (from_track_handle, to_track_handle) {
1962 (Some(f_t), Some(t_t)) => {
1963 let to_in_res = t_t.lock().midi.ins.get(to_port).cloned();
1964 if let Some(to_in) = to_in_res {
1965 let mut from_track = f_t.lock();
1966 if let Err(e) = from_track.midi.connect_out(from_port, to_in) {
1967 self.notify_clients(Err(e)).await;
1968 return;
1969 }
1970 from_track.invalidate_midi_route_cache();
1971 } else {
1972 self.notify_clients(Err(format!(
1973 "MIDI input port {} not found on track '{}'",
1974 to_port, to_track
1975 )))
1976 .await;
1977 }
1978 }
1979 _ => {
1980 self.notify_clients(Err(format!(
1981 "MIDI tracks not found: {} or {}",
1982 from_track, to_track
1983 )))
1984 .await;
1985 }
1986 }
1987 }
1988 }
1989 };
1990 }
1991
1992 pub(crate) async fn handle_disconnect(&mut self, action: Action) {
1993 let Action::Disconnect {
1994 ref from_track,
1995 from_port,
1996 ref to_track,
1997 to_port,
1998 kind,
1999 } = action
2000 else {
2001 return;
2002 };
2003
2004 if from_track == to_track && from_track != "hw:in" && to_track != "hw:out" {
2005 let Some(track) = self.state.lock().tracks.get(from_track).cloned() else {
2006 self.notify_clients(Err(format!("Track '{from_track}' not found")))
2007 .await;
2008 return;
2009 };
2010 match kind {
2011 Kind::Audio => {
2012 let result = {
2013 let mut track = track.lock();
2014 track.disconnect_audio_connectable(
2015 crate::connectable::ConnectableRef::TrackInput,
2016 to_port,
2017 crate::connectable::ConnectableRef::TrackOutput,
2018 from_port,
2019 )
2020 };
2021 if let Err(e) = result {
2022 self.notify_clients(Err(e)).await;
2023 return;
2024 }
2025 self.notify_clients(Ok(Action::TrackDisconnectAudio {
2026 track_name: from_track.clone(),
2027 from: crate::connectable::ConnectableRef::TrackInput,
2028 from_port: to_port,
2029 to: crate::connectable::ConnectableRef::TrackOutput,
2030 to_port: from_port,
2031 }))
2032 .await;
2033 }
2034 Kind::MIDI => {
2035 let result = {
2036 let mut track = track.lock();
2037 track.disconnect_midi_connectable(
2038 crate::connectable::ConnectableRef::TrackInput,
2039 to_port,
2040 crate::connectable::ConnectableRef::TrackOutput,
2041 from_port,
2042 )
2043 };
2044 if let Err(e) = result {
2045 self.notify_clients(Err(e)).await;
2046 return;
2047 }
2048 self.notify_clients(Ok(Action::TrackDisconnectMidi {
2049 track_name: from_track.clone(),
2050 from: crate::connectable::ConnectableRef::TrackInput,
2051 from_port: to_port,
2052 to: crate::connectable::ConnectableRef::TrackOutput,
2053 to_port: from_port,
2054 }))
2055 .await;
2056 }
2057 }
2058 return;
2059 }
2060
2061 if kind == Kind::Audio {
2062 if let Err(e) = self.disconnect_audio_route_and_notify(action.clone()).await {
2063 self.notify_clients(Err(e)).await;
2064 }
2065 } else if kind == Kind::MIDI {
2066 let from_hw_in_device = Self::midi_hw_in_device(from_track);
2067 let to_hw_out_device = Self::midi_hw_out_device(to_track);
2068
2069 if let (Some(from_device), Some(to_device)) = (from_hw_in_device, to_hw_out_device) {
2070 let before = self.midi_hw_thru_routes.len();
2071 self.midi_hw_thru_routes
2072 .retain(|r| !(r.from_device == from_device && r.to_device == to_device));
2073 if self.midi_hw_thru_routes.len() < before {
2074 self.notify_clients(Ok(action.clone())).await;
2075 } else {
2076 self.notify_clients(Err(format!(
2077 "Disconnect failed: MIDI route not found ({} -> {})",
2078 from_track, to_track
2079 )))
2080 .await;
2081 }
2082 return;
2083 }
2084
2085 if let Some(device) = from_hw_in_device {
2086 let before = self.midi_hw_in_routes.len();
2087 self.midi_hw_in_routes.retain(|r| {
2088 !(r.device == device && r.to_track == *to_track && r.to_port == to_port)
2089 });
2090 if self.midi_hw_in_routes.len() < before {
2091 self.notify_clients(Ok(action.clone())).await;
2092 } else {
2093 self.notify_clients(Err(format!(
2094 "Disconnect failed: MIDI route not found ({} -> {})",
2095 from_track, to_track
2096 )))
2097 .await;
2098 }
2099 return;
2100 }
2101
2102 if let Some(device) = to_hw_out_device {
2103 let before = self.midi_hw_out_routes.len();
2104 self.midi_hw_out_routes.retain(|r| {
2105 !(r.from_track == *from_track && r.from_port == from_port && r.device == device)
2106 });
2107 if self.midi_hw_out_routes.len() < before {
2108 self.notify_clients(Ok(action.clone())).await;
2109 } else {
2110 self.notify_clients(Err(format!(
2111 "Disconnect failed: MIDI route not found ({} -> {})",
2112 from_track, to_track
2113 )))
2114 .await;
2115 }
2116 return;
2117 }
2118
2119 let state = self.state.lock();
2120 if let (Some(f_t), Some(t_t)) =
2121 (state.tracks.get(from_track), state.tracks.get(to_track))
2122 && let Some(to_in) = t_t.lock().midi.ins.get(to_port).cloned()
2123 {
2124 let mut from_track = f_t.lock();
2125 if let Err(e) = from_track.midi.disconnect_out(from_port, &to_in) {
2126 self.notify_clients(Err(e)).await;
2127 } else {
2128 from_track.invalidate_midi_route_cache();
2129 self.notify_clients(Ok(action.clone())).await;
2130 }
2131 } else {
2132 self.notify_clients(Err(format!(
2133 "Disconnect failed: MIDI ports not found ({} -> {})",
2134 from_track, to_track
2135 )))
2136 .await;
2137 }
2138 }
2139 }
2140
2141 pub(crate) async fn handle_track_set_parent(
2142 &mut self,
2143 track_name: &str,
2144 parent_name: Option<&str>,
2145 ) {
2146 let track = match self.track_handle_or_err(track_name) {
2147 Ok(track) => track,
2148 Err(e) => {
2149 self.notify_clients(Err(e)).await;
2150 return;
2151 }
2152 };
2153 if parent_name == Some(track_name) {
2154 self.notify_clients(Err("Track cannot be its own parent".to_string()))
2155 .await;
2156 return;
2157 }
2158
2159 if parent_name.is_some() && track.lock().is_master() {
2161 self.notify_clients(Err(format!(
2162 "Track '{}' is the master track and cannot be made part of a folder",
2163 track_name
2164 )))
2165 .await;
2166 return;
2167 }
2168
2169 if let Some(parent_name) = parent_name {
2171 let state = self.state.lock();
2172 let parent = state.tracks.get(parent_name);
2173 if parent.is_none() {
2174 self.notify_clients(Err(format!(
2175 "Parent track '{}' does not exist",
2176 parent_name
2177 )))
2178 .await;
2179 return;
2180 }
2181 if !parent.unwrap().lock().is_folder {
2182 self.notify_clients(Err(format!("Track '{}' is not a folder", parent_name)))
2183 .await;
2184 return;
2185 }
2186 }
2187
2188 {
2190 let old_parent_name = track.lock().parent_track.clone();
2191 if let Some(old_parent_name) = old_parent_name {
2192 let state = self.state.lock();
2193 if let (Some(parent_arc), Some(child_arc)) = (
2194 state.tracks.get(&old_parent_name).cloned(),
2195 state.tracks.get(track_name).cloned(),
2196 ) {
2197 {
2198 let mut parent = parent_arc.lock();
2199 parent.child_tracks.retain(|c| c.lock().name != *track_name);
2200 }
2201 {
2202 let mut child = child_arc.lock();
2203 let parent = parent_arc.lock();
2204 child.disconnect_from_parent(&parent);
2205 }
2206 }
2207 }
2208 }
2209
2210 let mut disconnect_actions = Vec::new();
2211
2212 {
2214 let state = self.state.lock();
2215 let hw_inputs = self.all_hw_input_audio_ports();
2216 let hw_outputs = self.all_hw_output_audio_ports();
2217 if let Some(child_arc) = state.tracks.get(track_name).cloned() {
2218 let child = child_arc.lock();
2219 for (port_idx, inp) in child.audio.ins.iter().enumerate() {
2220 let sources = inp.connections();
2221 for src in sources.iter() {
2222 let _ = AudioIO::disconnect(src, inp);
2223 if let Some((src_name, src_port)) =
2224 self.find_audio_io_owner(self.state.as_ref(), src)
2225 {
2226 disconnect_actions.push(Action::Disconnect {
2227 from_track: src_name,
2228 from_port: src_port,
2229 to_track: track_name.to_string(),
2230 to_port: port_idx,
2231 kind: Kind::Audio,
2232 });
2233 } else if let Some(src_port) = hw_inputs
2234 .iter()
2235 .position(|hw_in| std::sync::Arc::ptr_eq(hw_in, src))
2236 {
2237 disconnect_actions.push(Action::Disconnect {
2238 from_track: "hw:in".to_string(),
2239 from_port: src_port,
2240 to_track: track_name.to_string(),
2241 to_port: port_idx,
2242 kind: Kind::Audio,
2243 });
2244 }
2245 }
2246 }
2247 for (port_idx, out) in child.audio.outs.iter().enumerate() {
2248 let targets = out.connections();
2249 for tgt in targets.iter() {
2250 let _ = AudioIO::disconnect(out, tgt);
2251 if let Some((tgt_name, tgt_port)) =
2252 self.find_audio_io_owner(self.state.as_ref(), tgt)
2253 {
2254 disconnect_actions.push(Action::Disconnect {
2255 from_track: track_name.to_string(),
2256 from_port: port_idx,
2257 to_track: tgt_name,
2258 to_port: tgt_port,
2259 kind: Kind::Audio,
2260 });
2261 } else if let Some(tgt_port) = hw_outputs
2262 .iter()
2263 .position(|hw_out| std::sync::Arc::ptr_eq(hw_out, tgt))
2264 {
2265 disconnect_actions.push(Action::Disconnect {
2266 from_track: track_name.to_string(),
2267 from_port: port_idx,
2268 to_track: "hw:out".to_string(),
2269 to_port: tgt_port,
2270 kind: Kind::Audio,
2271 });
2272 }
2273 }
2274 }
2275
2276 for route in self
2278 .midi_hw_in_routes
2279 .iter()
2280 .filter(|r| r.to_track == track_name)
2281 {
2282 disconnect_actions.push(Action::Disconnect {
2283 from_track: format!("midi:hw:in:{}", route.device),
2284 from_port: 0,
2285 to_track: track_name.to_string(),
2286 to_port: route.to_port,
2287 kind: Kind::MIDI,
2288 });
2289 }
2290 self.midi_hw_in_routes.retain(|r| r.to_track != track_name);
2291
2292 for route in self
2293 .midi_hw_out_routes
2294 .iter()
2295 .filter(|r| r.from_track == track_name)
2296 {
2297 disconnect_actions.push(Action::Disconnect {
2298 from_track: track_name.to_string(),
2299 from_port: route.from_port,
2300 to_track: format!("midi:hw:out:{}", route.device),
2301 to_port: 0,
2302 kind: Kind::MIDI,
2303 });
2304 }
2305 self.midi_hw_out_routes
2306 .retain(|r| r.from_track != track_name);
2307
2308 for (port_idx, out) in child.midi.outs.iter().enumerate() {
2310 let targets = out.connections();
2311 for tgt in targets {
2312 if let Some((tgt_name, tgt_port, _)) =
2313 self.find_midi_io_owner(self.state.as_ref(), &tgt)
2314 {
2315 let _ = MIDIIO::disconnect(out, &tgt);
2316 disconnect_actions.push(Action::Disconnect {
2317 from_track: track_name.to_string(),
2318 from_port: port_idx,
2319 to_track: tgt_name,
2320 to_port: tgt_port,
2321 kind: Kind::MIDI,
2322 });
2323 }
2324 }
2325 }
2326 }
2327
2328 let child_input_arcs: Vec<_> =
2330 if let Some(child_arc) = state.tracks.get(track_name).cloned() {
2331 let child = child_arc.lock();
2332 child.midi.ins.clone()
2333 } else {
2334 Vec::new()
2335 };
2336 for (other_name, other_track) in &state.tracks {
2337 if other_name.as_str() == track_name {
2338 continue;
2339 }
2340 let other = other_track.lock();
2341 for (out_port, out) in other.midi.outs.iter().enumerate() {
2342 let targets = out.connections();
2343 for tgt in targets {
2344 if let Some(to_port) = child_input_arcs
2345 .iter()
2346 .position(|inp| std::sync::Arc::ptr_eq(inp, &tgt))
2347 {
2348 let _ = MIDIIO::disconnect(out, &tgt);
2349 disconnect_actions.push(Action::Disconnect {
2350 from_track: other_name.clone(),
2351 from_port: out_port,
2352 to_track: track_name.to_string(),
2353 to_port,
2354 kind: Kind::MIDI,
2355 });
2356 }
2357 }
2358 }
2359 }
2360 }
2361
2362 {
2364 track.lock().parent_track = parent_name.map(str::to_string);
2365 }
2366
2367 if let Some(parent_name) = parent_name {
2369 let state = self.state.lock();
2370 if let (Some(parent_arc), Some(child_arc)) = (
2371 state.tracks.get(parent_name).cloned(),
2372 state.tracks.get(track_name).cloned(),
2373 ) {
2374 {
2375 let mut parent = parent_arc.lock();
2376 parent.child_tracks.push(child_arc.clone());
2377 }
2378 {
2379 let mut child = child_arc.lock();
2380 let mut parent = parent_arc.lock();
2381 if parent.audio.ins.len() == child.audio.ins.len() {
2383 for (parent_in, child_in) in
2384 parent.audio.ins.iter().zip(child.audio.ins.iter())
2385 {
2386 Track::connect_directed_audio(parent_in, child_in);
2387 }
2388 }
2389 if parent.audio.outs.len() == child.audio.outs.len() {
2391 for (child_out, parent_out) in
2392 child.audio.outs.iter().zip(parent.audio.outs.iter())
2393 {
2394 AudioIO::connect(child_out, parent_out);
2395 }
2396 }
2397 if parent.midi.ins.len() == child.midi.ins.len() {
2399 for (parent_in, child_in) in
2400 parent.midi.ins.iter().zip(child.midi.ins.iter())
2401 {
2402 child_in.add_connection(parent_in);
2403 }
2404 }
2405 if parent.midi.outs.len() == child.midi.outs.len() {
2407 for (child_out, parent_out) in
2408 child.midi.outs.iter().zip(parent.midi.outs.iter())
2409 {
2410 child_out.add_connection(parent_out);
2411 }
2412 }
2413 child.invalidate_audio_route_cache();
2414 parent.invalidate_audio_route_cache();
2415 child.invalidate_midi_route_cache();
2416 parent.invalidate_midi_route_cache();
2417 }
2418 }
2419 }
2420
2421 {
2424 let state = self.state.lock();
2425 if let Some(child_arc) = state.tracks.get(track_name).cloned() {
2426 let mut child = child_arc.lock();
2427 child.ensure_default_audio_passthrough();
2428 child.ensure_default_midi_passthrough();
2429 }
2430 }
2431
2432 for action in disconnect_actions {
2433 self.notify_clients(Ok(action)).await;
2434 }
2435
2436 self.notify_clients(Ok(Action::TrackSetParent {
2437 track_name: track_name.to_string(),
2438 parent_name: parent_name.map(str::to_string),
2439 }))
2440 .await;
2441 }
2442
2443 pub(crate) async fn handle_clip_move(&mut self, action: Action) {
2444 let Action::ClipMove {
2445 ref kind,
2446 ref from,
2447 ref to,
2448 copy,
2449 } = action
2450 else {
2451 return;
2452 };
2453 if let Some(from_track_handle) = self.state.lock().tracks.get(&from.track_name)
2454 && let Some(to_track_handle) = self.state.lock().tracks.get(&to.track_name)
2455 {
2456 let from_track = from_track_handle.lock();
2457 let mut to_track = to_track_handle.lock();
2458 match kind {
2459 Kind::Audio => {
2460 if from.clip_index >= from_track.audio.clips().len() {
2461 self.notify_clients(Err(format!(
2462 "Clip index {} is too high, as track {} has only {} clips!",
2463 from.clip_index,
2464 from_track.name.clone(),
2465 from_track.audio.clips().len(),
2466 )))
2467 .await;
2468 return;
2469 }
2470 if from_track.audio.ins.len() != to_track.audio.ins.len() {
2471 self.notify_clients(Err(format!(
2472 "Cannot move/copy audio clip from '{}' ({} inputs) to '{}' ({} inputs)",
2473 from_track.name,
2474 from_track.audio.ins.len(),
2475 to_track.name,
2476 to_track.audio.ins.len()
2477 )))
2478 .await;
2479 return;
2480 }
2481 let Some(clip_copy) = from_track.audio.clips().get(from.clip_index).cloned()
2482 else {
2483 return;
2484 };
2485 if !copy {
2486 from_track.audio.remove_clip(from.clip_index);
2487 }
2488 let mut clip_copy = (*clip_copy).clone();
2489 let clip_len = clip_copy.end.saturating_sub(clip_copy.start).max(1);
2490 clip_copy.start = to.sample_offset;
2491 clip_copy.end = clip_copy.start.saturating_add(clip_len);
2492 let max_lane = to_track.audio.ins.len().saturating_sub(1);
2493 clip_copy.input_channel = to.input_channel.min(max_lane);
2494 Self::push_audio_clip_with_cross_section_fades(&mut to_track, clip_copy);
2495 }
2496 Kind::MIDI => {
2497 if from.clip_index >= from_track.midi.clips().len() {
2498 self.notify_clients(Err(format!(
2499 "Clip index {} is too high, as track {} has only {} clips!",
2500 from.clip_index,
2501 from_track.name.clone(),
2502 from_track.midi.clips().len(),
2503 )))
2504 .await;
2505 return;
2506 }
2507 let Some(clip_copy) = from_track.midi.clips().get(from.clip_index).cloned()
2508 else {
2509 return;
2510 };
2511 if !copy {
2512 from_track.midi.remove_clip(from.clip_index);
2513 }
2514 let mut clip_copy = (*clip_copy).clone();
2515 let clip_len = clip_copy.end.saturating_sub(clip_copy.start).max(1);
2516 clip_copy.start = to.sample_offset;
2517 clip_copy.end = clip_copy.start.saturating_add(clip_len);
2518 let max_lane = to_track.midi.ins.len().saturating_sub(1);
2519 clip_copy.input_channel = to.input_channel.min(max_lane);
2520 to_track.midi.push_clip(clip_copy);
2521 }
2522 }
2523 }
2524 }
2525
2526 pub(crate) async fn handle_rename_track(&mut self, action: Action) -> bool {
2527 let Action::RenameTrack {
2528 ref old_name,
2529 ref new_name,
2530 } = action
2531 else {
2532 return false;
2533 };
2534
2535 if self.state.lock().tracks.contains_key(new_name) {
2536 self.notify_clients(Err(format!("Track '{}' already exists", new_name)))
2537 .await;
2538 return true;
2539 }
2540
2541 let Some(track) = self.state.lock().tracks.remove(old_name) else {
2542 self.notify_clients(Err(format!("Track '{}' not found", old_name)))
2543 .await;
2544 return true;
2545 };
2546
2547 track.lock().name = new_name.clone();
2548 self.state.lock().tracks.insert(new_name.clone(), track);
2549 for other in self.state.lock().tracks.values() {
2550 let mut other = other.lock();
2551 if other.parent_track.as_deref() == Some(old_name.as_str()) {
2552 other.parent_track = Some(new_name.clone());
2553 }
2554 }
2555
2556 if let Some(recording) = self.audio_recordings.remove(old_name) {
2557 self.audio_recordings.insert(new_name.clone(), recording);
2558 }
2559 if let Some(recording) = self.midi_recordings.remove(old_name) {
2560 self.midi_recordings.insert(new_name.clone(), recording);
2561 }
2562
2563 for route in &mut self.midi_hw_in_routes {
2564 if route.to_track == *old_name {
2565 route.to_track = new_name.clone();
2566 }
2567 }
2568 for route in &mut self.midi_hw_out_routes {
2569 if route.from_track == *old_name {
2570 route.from_track = new_name.clone();
2571 }
2572 }
2573 if let Some((armed_track, target, device)) = self.pending_midi_learn.clone()
2574 && armed_track == *old_name
2575 {
2576 self.pending_midi_learn = Some((new_name.clone(), target, device));
2577 }
2578
2579 self.notify_clients(Ok(Action::RenameTrack {
2580 old_name: old_name.clone(),
2581 new_name: new_name.clone(),
2582 }))
2583 .await;
2584
2585 false
2586 }
2587
2588 pub(crate) async fn handle_add_clip(&mut self, action: Action) -> bool {
2589 let Action::AddClip {
2590 ref clip_id,
2591 ref name,
2592 ref track_name,
2593 start,
2594 length,
2595 offset,
2596 input_channel,
2597 muted,
2598 ref peaks_file,
2599 kind,
2600 fade_enabled,
2601 fade_in_samples,
2602 fade_out_samples,
2603 ref source_name,
2604 source_offset,
2605 source_length,
2606 ref preview_name,
2607 ref pitch_correction_points,
2608 pitch_correction_frame_likeness,
2609 pitch_correction_inertia_ms,
2610 pitch_correction_formant_compensation,
2611 ref plugin_graph_json,
2612 } = action
2613 else {
2614 return false;
2615 };
2616
2617 self.add_clip_to_track(ClipAddRequest {
2618 clip_id,
2619 name,
2620 track_name,
2621 start,
2622 length,
2623 offset,
2624 input_channel,
2625 muted,
2626 peaks_file: peaks_file.clone(),
2627 kind,
2628 fade_enabled,
2629 fade_in_samples,
2630 fade_out_samples,
2631 source_name: source_name.clone(),
2632 source_offset,
2633 source_length,
2634 preview_name: preview_name.clone(),
2635 pitch_correction_points: pitch_correction_points.clone(),
2636 pitch_correction_frame_likeness,
2637 pitch_correction_inertia_ms,
2638 pitch_correction_formant_compensation,
2639 plugin_graph_json: plugin_graph_json.clone(),
2640 });
2641 if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
2642 let track_name = track_name.clone();
2643 tokio::task::spawn_blocking(move || {
2644 track.lock().preload_clips();
2645 tracing::debug!("Preloaded clips for track '{}' after AddClip", track_name);
2646 });
2647 }
2648
2649 false
2650 }
2651
2652 pub(crate) async fn handle_track_set_folder(&mut self, a: Action) -> bool {
2653 let Action::TrackSetFolder {
2654 ref track_name,
2655 is_folder,
2656 } = a
2657 else {
2658 return false;
2659 };
2660
2661 let track = match self.track_handle_or_err(track_name) {
2662 Ok(track) => track,
2663 Err(e) => {
2664 self.notify_clients(Err(e)).await;
2665 return true;
2666 }
2667 };
2668 if is_folder {
2669 let is_master = track.lock().is_master();
2670 if is_master {
2671 self.notify_clients(Err(format!(
2672 "Track '{}' is the master track and cannot be made a folder",
2673 track_name
2674 )))
2675 .await;
2676 return true;
2677 }
2678 }
2679 {
2680 let mut track = track.lock();
2681 track.is_folder = is_folder;
2682 track.ensure_default_audio_passthrough();
2683 track.ensure_default_midi_passthrough();
2684 }
2685 self.notify_clients(Ok(Action::TrackSetFolder {
2686 track_name: track_name.clone(),
2687 is_folder,
2688 }))
2689 .await;
2690
2691 false
2692 }
2693
2694 pub(crate) async fn handle_track_connect_audio(&mut self, a: Action) -> bool {
2695 let Action::TrackConnectAudio {
2696 ref track_name,
2697 ref from,
2698 from_port,
2699 ref to,
2700 to_port,
2701 } = a
2702 else {
2703 return false;
2704 };
2705
2706 if self
2707 .reject_if_track_frozen(track_name, "routing changes")
2708 .await
2709 {
2710 return true;
2711 }
2712 let track = match self.track_handle_or_err(track_name) {
2713 Ok(track) => track,
2714 Err(e) => {
2715 self.notify_clients(Err(e)).await;
2716 return true;
2717 }
2718 };
2719 if let Err(e) =
2720 track
2721 .lock()
2722 .connect_audio_connectable(from.clone(), from_port, to.clone(), to_port)
2723 {
2724 self.notify_clients(Err(e)).await;
2725 return true;
2726 }
2727
2728 false
2729 }
2730
2731 pub(crate) async fn handle_track_disconnect_audio(&mut self, a: Action) -> bool {
2732 let Action::TrackDisconnectAudio {
2733 ref track_name,
2734 ref from,
2735 from_port,
2736 ref to,
2737 to_port,
2738 } = a
2739 else {
2740 return false;
2741 };
2742
2743 if self
2744 .reject_if_track_frozen(track_name, "routing changes")
2745 .await
2746 {
2747 return true;
2748 }
2749 let track = match self.track_handle_or_err(track_name) {
2750 Ok(track) => track,
2751 Err(e) => {
2752 self.notify_clients(Err(e)).await;
2753 return true;
2754 }
2755 };
2756 if let Err(e) =
2757 track
2758 .lock()
2759 .disconnect_audio_connectable(from.clone(), from_port, to.clone(), to_port)
2760 {
2761 self.notify_clients(Err(e)).await;
2762 return true;
2763 }
2764
2765 false
2766 }
2767
2768 pub(crate) async fn handle_track_connect_midi(&mut self, a: Action) -> bool {
2769 let Action::TrackConnectMidi {
2770 ref track_name,
2771 ref from,
2772 from_port,
2773 ref to,
2774 to_port,
2775 } = a
2776 else {
2777 return false;
2778 };
2779
2780 if self
2781 .reject_if_track_frozen(track_name, "routing changes")
2782 .await
2783 {
2784 return true;
2785 }
2786 let track = match self.track_handle_or_err(track_name) {
2787 Ok(track) => track,
2788 Err(e) => {
2789 self.notify_clients(Err(e)).await;
2790 return true;
2791 }
2792 };
2793 if let Err(e) =
2794 track
2795 .lock()
2796 .connect_midi_connectable(from.clone(), from_port, to.clone(), to_port)
2797 {
2798 self.notify_clients(Err(e)).await;
2799 return true;
2800 }
2801
2802 false
2803 }
2804
2805 pub(crate) async fn handle_track_disconnect_midi(&mut self, a: Action) -> bool {
2806 let Action::TrackDisconnectMidi {
2807 ref track_name,
2808 ref from,
2809 from_port,
2810 ref to,
2811 to_port,
2812 } = a
2813 else {
2814 return false;
2815 };
2816
2817 if self
2818 .reject_if_track_frozen(track_name, "routing changes")
2819 .await
2820 {
2821 return true;
2822 }
2823 let track = match self.track_handle_or_err(track_name) {
2824 Ok(track) => track,
2825 Err(e) => {
2826 self.notify_clients(Err(e)).await;
2827 return true;
2828 }
2829 };
2830 if let Err(e) =
2831 track
2832 .lock()
2833 .disconnect_midi_connectable(from.clone(), from_port, to.clone(), to_port)
2834 {
2835 self.notify_clients(Err(e)).await;
2836 return true;
2837 }
2838
2839 false
2840 }
2841
2842 pub(crate) async fn handle_set_clip_pitch_correction(&mut self, a: Action) -> bool {
2843 let Action::SetClipPitchCorrection {
2844 ref track_name,
2845 clip_index,
2846 ref preview_name,
2847 ref source_name,
2848 source_offset,
2849 source_length,
2850 ref pitch_correction_points,
2851 pitch_correction_frame_likeness,
2852 pitch_correction_inertia_ms,
2853 pitch_correction_formant_compensation,
2854 } = a
2855 else {
2856 return false;
2857 };
2858
2859 self.set_clip_pitch_correction(
2860 track_name,
2861 clip_index,
2862 preview_name.clone(),
2863 source_name.clone(),
2864 source_offset,
2865 source_length,
2866 pitch_correction_points.clone(),
2867 pitch_correction_frame_likeness,
2868 pitch_correction_inertia_ms,
2869 pitch_correction_formant_compensation,
2870 );
2871
2872 false
2873 }
2874
2875 pub(crate) async fn handle_track_remove_audio_output(&mut self, a: Action) -> bool {
2876 let Action::TrackRemoveAudioOutput(ref name) = a else {
2877 return false;
2878 };
2879
2880 let track = match self.track_handle_or_err(name) {
2881 Ok(track) => track,
2882 Err(e) => {
2883 self.notify_clients(Err(e)).await;
2884 return true;
2885 }
2886 };
2887 let (hw_outputs, track_inputs) = {
2888 let state = self.state.lock();
2889 let hw_outputs = self.all_hw_output_audio_ports();
2890 let track_inputs = state
2891 .tracks
2892 .iter()
2893 .filter(|(track_name, _)| *track_name != name)
2894 .flat_map(|(_, handle)| handle.lock().audio.ins.clone())
2895 .collect::<Vec<_>>();
2896 (hw_outputs, track_inputs)
2897 };
2898 if let Err(e) = track.lock().remove_audio_output(&hw_outputs, &track_inputs) {
2899 self.notify_clients(Err(e)).await;
2900 return true;
2901 }
2902
2903 false
2904 }
2905
2906 pub(crate) async fn handle_track_toggle_folder(&mut self, a: Action) -> bool {
2907 let Action::TrackToggleFolder { ref track_name } = a else {
2908 return false;
2909 };
2910
2911 let track = match self.track_handle_or_err(track_name) {
2912 Ok(track) => track,
2913 Err(e) => {
2914 self.notify_clients(Err(e)).await;
2915 return true;
2916 }
2917 };
2918 {
2919 let t = track.lock();
2920 t.folder_open.fetch_not(Ordering::Relaxed);
2921 }
2922 self.notify_clients(Ok(Action::TrackToggleFolder {
2923 track_name: track_name.clone(),
2924 }))
2925 .await;
2926
2927 self.notify_clients(Ok(Action::TrackSetFolder {
2928 track_name: track_name.clone(),
2929 is_folder: track.lock().is_folder,
2930 }))
2931 .await;
2932
2933 false
2934 }
2935
2936 pub(crate) async fn handle_add_grouped_clip(&mut self, a: Action) -> bool {
2937 let Action::AddGroupedClip {
2938 ref track_name,
2939 kind,
2940 ref audio_clip,
2941 ref midi_clip,
2942 } = a
2943 else {
2944 return false;
2945 };
2946
2947 self.add_grouped_clip_to_track(track_name, kind, audio_clip.clone(), midi_clip.clone());
2948 if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
2949 let track_name = track_name.clone();
2950 tokio::task::spawn_blocking(move || {
2951 track.lock().preload_clips();
2952 tracing::debug!(
2953 "Preloaded clips for track '{}' after AddGroupedClip",
2954 track_name
2955 );
2956 });
2957 }
2958
2959 false
2960 }
2961
2962 pub(crate) async fn handle_track_add_audio_input(&mut self, a: Action) -> bool {
2963 let Action::TrackAddAudioInput(ref name) = a else {
2964 return false;
2965 };
2966
2967 let track = match self.track_handle_or_err(name) {
2968 Ok(track) => track,
2969 Err(e) => {
2970 self.notify_clients(Err(e)).await;
2971 return true;
2972 }
2973 };
2974 if let Err(e) = track.lock().add_audio_input() {
2975 self.notify_clients(Err(e)).await;
2976 return true;
2977 }
2978
2979 false
2980 }
2981
2982 pub(crate) async fn handle_track_add_audio_output(&mut self, a: Action) -> bool {
2983 let Action::TrackAddAudioOutput(ref name) = a else {
2984 return false;
2985 };
2986
2987 let track = match self.track_handle_or_err(name) {
2988 Ok(track) => track,
2989 Err(e) => {
2990 self.notify_clients(Err(e)).await;
2991 return true;
2992 }
2993 };
2994 if let Err(e) = track.lock().add_audio_output() {
2995 self.notify_clients(Err(e)).await;
2996 return true;
2997 }
2998
2999 false
3000 }
3001
3002 pub(crate) async fn handle_track_remove_audio_input(&mut self, a: Action) -> bool {
3003 let Action::TrackRemoveAudioInput(ref name) = a else {
3004 return false;
3005 };
3006
3007 let track = match self.track_handle_or_err(name) {
3008 Ok(track) => track,
3009 Err(e) => {
3010 self.notify_clients(Err(e)).await;
3011 return true;
3012 }
3013 };
3014 if let Err(e) = track.lock().remove_audio_input() {
3015 self.notify_clients(Err(e)).await;
3016 return true;
3017 }
3018
3019 false
3020 }
3021
3022 pub(crate) async fn handle_track_midi_cc(&mut self, a: Action) -> bool {
3023 let Action::TrackMidiCc {
3024 ref track_name,
3025 channel,
3026 cc,
3027 value,
3028 } = a
3029 else {
3030 return false;
3031 };
3032
3033 if let Some(track) = self.state.lock().tracks.get(track_name) {
3034 track
3035 .lock()
3036 .rt
3037 .pending_automation_midi_events
3038 .push(MidiEvent::new(
3039 0,
3040 vec![0xB0 | channel.min(15), cc.min(127), value.min(127)],
3041 ));
3042 }
3043
3044 false
3045 }
3046
3047 pub(crate) async fn handle_track_toggle_arm(&mut self, a: Action) -> bool {
3048 let Action::TrackToggleArm(ref name) = a else {
3049 return false;
3050 };
3051
3052 if self.reject_if_track_frozen(name, "arming/disarming").await {
3053 return true;
3054 }
3055 if let Some(track) = self.state.lock().tracks.get(name).cloned() {
3056 track.lock().arm();
3057 let armed = track.lock().armed();
3058 if !armed && self.audio_recordings.contains_key(name) {
3059 self.flush_track_recording(name).await;
3060 }
3061 } else {
3062 tracing::warn!(
3063 "TrackToggleArm for '{}' but track not found in engine",
3064 name
3065 );
3066 }
3067
3068 false
3069 }
3070
3071 pub(crate) async fn handle_track_set_midi_lane_channel(&mut self, a: Action) -> bool {
3072 let Action::TrackSetMidiLaneChannel {
3073 ref track_name,
3074 lane,
3075 channel,
3076 } = a
3077 else {
3078 return false;
3079 };
3080
3081 let track = match self.track_handle_or_err(track_name) {
3082 Ok(track) => track,
3083 Err(e) => {
3084 self.notify_clients(Err(e)).await;
3085 return true;
3086 }
3087 };
3088 track.lock().set_midi_lane_channel(lane, channel);
3089
3090 false
3091 }
3092
3093 pub(crate) async fn handle_track_set_mpe_zone(&mut self, a: Action) -> bool {
3094 let Action::TrackSetMpeZone {
3095 ref track_name,
3096 manager_channel,
3097 member_count,
3098 } = a
3099 else {
3100 return false;
3101 };
3102
3103 let track = match self.track_handle_or_err(track_name) {
3104 Ok(track) => track,
3105 Err(e) => {
3106 self.notify_clients(Err(e)).await;
3107 return true;
3108 }
3109 };
3110 track.lock().set_mpe_zone(manager_channel, member_count);
3111
3112 false
3113 }
3114
3115 pub(crate) async fn handle_track_set_mpe_pitch_bend_sensitivity(&mut self, a: Action) -> bool {
3116 let Action::TrackSetMpePitchBendSensitivity {
3117 ref track_name,
3118 channel,
3119 semitones,
3120 } = a
3121 else {
3122 return false;
3123 };
3124
3125 let track = match self.track_handle_or_err(track_name) {
3126 Ok(track) => track,
3127 Err(e) => {
3128 self.notify_clients(Err(e)).await;
3129 return true;
3130 }
3131 };
3132 track
3133 .lock()
3134 .set_mpe_pitch_bend_sensitivity(channel, semitones);
3135
3136 false
3137 }
3138
3139 pub(crate) async fn handle_track_set_frozen(&mut self, a: Action) -> bool {
3140 let Action::TrackSetFrozen {
3141 ref track_name,
3142 frozen,
3143 } = a
3144 else {
3145 return false;
3146 };
3147
3148 let track = match self.track_handle_or_err(track_name) {
3149 Ok(track) => track,
3150 Err(e) => {
3151 self.notify_clients(Err(e)).await;
3152 return true;
3153 }
3154 };
3155 track.lock().set_frozen(frozen);
3156
3157 false
3158 }
3159
3160 pub(crate) async fn handle_track_clear_default_passthrough(&mut self, a: Action) -> bool {
3161 let Action::TrackClearDefaultPassthrough { ref track_name } = a else {
3162 return false;
3163 };
3164
3165 if self
3166 .reject_if_track_frozen(track_name, "plugin graph editing")
3167 .await
3168 {
3169 return true;
3170 }
3171 let track = match self.track_handle_or_err(track_name) {
3172 Ok(track) => track,
3173 Err(e) => {
3174 self.notify_clients(Err(e)).await;
3175 return true;
3176 }
3177 };
3178 track.lock().clear_default_passthrough();
3179
3180 false
3181 }
3182
3183 pub(crate) async fn handle_set_clip_fade(&mut self, a: Action) -> bool {
3184 let Action::SetClipFade {
3185 ref track_name,
3186 clip_index,
3187 kind,
3188 fade_enabled,
3189 fade_in_samples,
3190 fade_out_samples,
3191 } = a
3192 else {
3193 return false;
3194 };
3195
3196 self.set_clip_fade(
3197 track_name,
3198 clip_index,
3199 kind,
3200 fade_enabled,
3201 fade_in_samples,
3202 fade_out_samples,
3203 );
3204
3205 false
3206 }
3207}
3208
3209#[cfg(test)]
3210mod tests {
3211 use super::*;
3212
3213 fn audio_clip(start: usize, len: usize) -> AudioClip {
3214 AudioClip::new("clip.wav".to_string(), start, start.saturating_add(len))
3215 }
3216
3217 #[test]
3218 fn cross_section_fades_match_non_aligned_overlap_length() {
3219 let mut existing = vec![audio_clip(0, 100)];
3220 let mut inserted = audio_clip(80, 100);
3221
3222 Engine::apply_audio_cross_section_fades(&mut existing, &mut inserted);
3223
3224 assert!(existing[0].fade_enabled);
3225 assert_eq!(existing[0].fade_out_samples, 20);
3226 assert!(inserted.fade_enabled);
3227 assert_eq!(inserted.fade_in_samples, 20);
3228 }
3229
3230 #[test]
3231 fn cross_section_fades_ignore_aligned_starts_and_ends() {
3232 let mut same_start = vec![audio_clip(0, 100)];
3233 let mut inserted_same_start = audio_clip(0, 40);
3234 Engine::apply_audio_cross_section_fades(&mut same_start, &mut inserted_same_start);
3235 assert_eq!(same_start[0].fade_in_samples, 240);
3236 assert_eq!(inserted_same_start.fade_in_samples, 240);
3237
3238 let mut same_end = vec![audio_clip(0, 100)];
3239 let mut inserted_same_end = audio_clip(60, 40);
3240 Engine::apply_audio_cross_section_fades(&mut same_end, &mut inserted_same_end);
3241 assert_eq!(same_end[0].fade_out_samples, 240);
3242 assert_eq!(inserted_same_end.fade_out_samples, 240);
3243 }
3244}