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(&mut self, action: Action) {
1634 let Action::AddTrack {
1635 name,
1636 audio_ins,
1637 midi_ins,
1638 audio_outs,
1639 midi_outs,
1640 folder,
1641 mixosc_addr,
1642 } = action
1643 else {
1644 self.notify_clients(Err("Expected AddTrack action".to_string()))
1645 .await;
1646 return;
1647 };
1648 let tracks = &mut self.state.lock().tracks;
1649 if tracks.contains_key(&name) {
1650 self.notify_clients(Err(format!("Track {} already exists", name)))
1651 .await;
1652 return;
1653 }
1654 if !folder
1655 && mixosc_addr.is_none()
1656 && !self.history_suspended
1657 && audio_ins + midi_ins + audio_outs + midi_outs == 0
1658 {
1659 self.notify_clients(Err(
1660 "Track must have at one audio/MIDI I/O or a MixOSC mixer".to_string(),
1661 ))
1662 .await;
1663 return;
1664 }
1665 let maybe_hw = self
1666 .hw_driver_info
1667 .map(|info| (info.cycle_samples, info.sample_rate as f64));
1668
1669 if let Some((chsamples, sample_rate)) = maybe_hw {
1670 let track = if folder {
1671 Track::new_folder(
1672 name.clone(),
1673 audio_ins,
1674 audio_outs,
1675 midi_ins,
1676 midi_outs,
1677 chsamples,
1678 sample_rate,
1679 )
1680 } else {
1681 Track::new(
1682 name.clone(),
1683 audio_ins,
1684 audio_outs,
1685 midi_ins,
1686 midi_outs,
1687 chsamples,
1688 sample_rate,
1689 )
1690 };
1691 tracks.insert(name.clone(), Arc::new(track));
1692 if let Some(track) = tracks.get(&name) {
1693 let mut t = track.lock();
1694 t.set_clip_playback_enabled(self.clip_playback_enabled);
1695 t.set_transport_timing(self.tempo_bpm, self.tsig_num, self.tsig_denom);
1696 t.set_session_base_dir(self.session_dir.clone());
1697 t.mixosc_addr = mixosc_addr;
1698 }
1699 } else {
1700 self.notify_clients(Err(
1701 "Engine needs to open audio device before adding audio track".to_string(),
1702 ))
1703 .await;
1704 }
1705 }
1706
1707 pub(crate) async fn handle_remove_track(&mut self, name: String, record_history: bool) {
1708 let mut descendant_names = Vec::new();
1709 self.collect_descendant_track_names(&name, &mut descendant_names);
1710 let names_to_remove: Vec<String> = descendant_names
1711 .iter()
1712 .cloned()
1713 .chain(std::iter::once(name.clone()))
1714 .collect();
1715
1716 let combined_inverse = if record_history && !self.history_suspended {
1717 let mut inv = Vec::new();
1718 for n in &names_to_remove {
1719 if let Some(mut actions) =
1720 create_inverse_actions(&Action::RemoveTrack(n.clone()), self.state.as_ref())
1721 {
1722 inv.append(&mut actions);
1723 }
1724 for route in self.midi_hw_in_routes.iter().filter(|r| &r.to_track == n) {
1725 inv.push(Action::Connect {
1726 from_track: format!("midi:hw:in:{}", route.device),
1727 from_port: 0,
1728 to_track: route.to_track.clone(),
1729 to_port: route.to_port,
1730 kind: Kind::MIDI,
1731 });
1732 }
1733 for route in self
1734 .midi_hw_out_routes
1735 .iter()
1736 .filter(|r| &r.from_track == n)
1737 {
1738 inv.push(Action::Connect {
1739 from_track: route.from_track.clone(),
1740 from_port: route.from_port,
1741 to_track: format!("midi:hw:out:{}", route.device),
1742 to_port: 0,
1743 kind: Kind::MIDI,
1744 });
1745 }
1746 }
1747
1748 let mut add_tracks = Vec::new();
1752 let mut connections = Vec::new();
1753 let mut rest = Vec::new();
1754 for action in inv {
1755 match action {
1756 Action::AddTrack { .. } => add_tracks.push(action),
1757 Action::Connect { .. } => connections.push(action),
1758 _ => rest.push(action),
1759 }
1760 }
1761 let mut ordered = add_tracks;
1762 ordered.extend(rest);
1763 ordered.extend(connections);
1764 ordered
1765 } else {
1766 Vec::new()
1767 };
1768
1769 for n in &descendant_names {
1770 self.remove_single_track(n).await;
1771 self.notify_clients(Ok(Action::RemoveTrack(n.clone())))
1772 .await;
1773 }
1774 self.remove_single_track(&name).await;
1775
1776 if record_history && !self.history_suspended && !combined_inverse.is_empty() {
1777 self.history.record(UndoEntry {
1778 forward_actions: vec![Action::RemoveTrack(name.clone())],
1779 inverse_actions: combined_inverse,
1780 });
1781 }
1782 }
1783
1784 pub(crate) async fn handle_connect(
1785 &mut self,
1786 from_track: &str,
1787 from_port: usize,
1788 to_track: &str,
1789 to_port: usize,
1790 kind: Kind,
1791 ) {
1792 match kind {
1793 Kind::Audio => {
1794 if from_track == to_track && from_track != "hw:in" && to_track != "hw:out" {
1795 let Some(track) = self.state.lock().tracks.get(from_track).cloned() else {
1796 self.notify_clients(Err(format!("Track '{from_track}' not found")))
1797 .await;
1798 return;
1799 };
1800 let result = {
1801 let mut track = track.lock();
1802 track.connect_audio_connectable(
1803 crate::connectable::ConnectableRef::TrackInput,
1804 to_port,
1805 crate::connectable::ConnectableRef::TrackOutput,
1806 from_port,
1807 )
1808 };
1809 if let Err(e) = result {
1810 self.notify_clients(Err(e)).await;
1811 return;
1812 }
1813 self.notify_clients(Ok(Action::TrackConnectAudio {
1814 track_name: from_track.to_string(),
1815 from: crate::connectable::ConnectableRef::TrackInput,
1816 from_port: to_port,
1817 to: crate::connectable::ConnectableRef::TrackOutput,
1818 to_port: from_port,
1819 }))
1820 .await;
1821 return;
1822 }
1823 let (from_audio_io, to_audio_io) =
1824 self.resolve_audio_route_ports(from_track, from_port, to_track, to_port);
1825 match (from_audio_io, to_audio_io) {
1826 (Some(source), Some(target)) => {
1827 if from_track != "hw:in"
1828 && to_track != "hw:out"
1829 && self.check_if_leads_to_kind(Kind::Audio, to_track, from_track)
1830 {
1831 self.notify_clients(Err("Circular routing is not allowed!".into()))
1832 .await;
1833 return;
1834 }
1835 crate::audio::io::AudioIO::connect(&source, &target);
1836 }
1837 (None, _) => {
1838 self.notify_clients(Err(format!(
1839 "Source track '{}' not found",
1840 from_track
1841 )))
1842 .await;
1843 }
1844 (_, None) => {
1845 self.notify_clients(Err(format!(
1846 "Destination track '{}' not found",
1847 to_track
1848 )))
1849 .await;
1850 }
1851 }
1852 }
1853 Kind::MIDI => {
1854 let from_hw_in_device = Self::midi_hw_in_device(from_track);
1855 let to_hw_out_device = Self::midi_hw_out_device(to_track);
1856 let from_is_invalid_hw = Self::midi_hw_out_device(from_track).is_some();
1857 let to_is_invalid_hw = Self::midi_hw_in_device(to_track).is_some();
1858
1859 if from_is_invalid_hw || to_is_invalid_hw {
1860 self.notify_clients(Err(
1861 "Invalid MIDI hardware connection direction".to_string()
1862 ))
1863 .await;
1864 return;
1865 }
1866
1867 if from_hw_in_device.is_none()
1868 && to_hw_out_device.is_none()
1869 && from_track != to_track
1870 && self.check_if_leads_to_kind(Kind::MIDI, to_track, from_track)
1871 {
1872 self.notify_clients(Err("Circular routing is not allowed!".into()))
1873 .await;
1874 return;
1875 }
1876
1877 if from_track == to_track {
1878 let Some(track) = self.state.lock().tracks.get(from_track).cloned() else {
1879 self.notify_clients(Err(format!("Track '{from_track}' not found")))
1880 .await;
1881 return;
1882 };
1883 let result = {
1884 let mut track = track.lock();
1885 track.connect_midi_connectable(
1886 crate::connectable::ConnectableRef::TrackInput,
1887 to_port,
1888 crate::connectable::ConnectableRef::TrackOutput,
1889 from_port,
1890 )
1891 };
1892 if let Err(e) = result {
1893 self.notify_clients(Err(e)).await;
1894 return;
1895 }
1896 self.notify_clients(Ok(Action::TrackConnectMidi {
1897 track_name: from_track.to_string(),
1898 from: crate::connectable::ConnectableRef::TrackInput,
1899 from_port: to_port,
1900 to: crate::connectable::ConnectableRef::TrackOutput,
1901 to_port: from_port,
1902 }))
1903 .await;
1904 return;
1905 }
1906
1907 let state = self.state.lock();
1908 let from_track_handle = state.tracks.get(from_track);
1909 let to_track_handle = state.tracks.get(to_track);
1910
1911 if let (Some(from_device), Some(to_device)) = (from_hw_in_device, to_hw_out_device)
1912 {
1913 let route = MidiHwThruRoute {
1914 from_device: from_device.to_string(),
1915 to_device: to_device.to_string(),
1916 };
1917 if !self.midi_hw_thru_routes.iter().any(|r| r == &route) {
1918 self.midi_hw_thru_routes.push(route);
1919 }
1920 } else if let Some(device) = from_hw_in_device {
1921 if let Some(t_t) = to_track_handle {
1922 if t_t.lock().midi.ins.get(to_port).is_none() {
1923 self.notify_clients(Err(format!(
1924 "MIDI input port {} not found on track '{}'",
1925 to_port, to_track
1926 )))
1927 .await;
1928 return;
1929 }
1930 let route = MidiHwInRoute {
1931 device: device.to_string(),
1932 to_track: to_track.to_string(),
1933 to_port,
1934 };
1935 if !self.midi_hw_in_routes.iter().any(|r| r == &route) {
1936 self.midi_hw_in_routes.push(route);
1937 }
1938 } else {
1939 self.notify_clients(Err(format!(
1940 "MIDI destination track not found: {}",
1941 to_track
1942 )))
1943 .await;
1944 }
1945 } else if let Some(device) = to_hw_out_device {
1946 if let Some(f_t) = from_track_handle {
1947 if f_t.lock().midi.outs.get(from_port).is_none() {
1948 self.notify_clients(Err(format!(
1949 "MIDI output port {} not found on track '{}'",
1950 from_port, from_track
1951 )))
1952 .await;
1953 return;
1954 }
1955 let route = MidiHwOutRoute {
1956 from_track: from_track.to_string(),
1957 from_port,
1958 device: device.to_string(),
1959 };
1960 if !self.midi_hw_out_routes.iter().any(|r| r == &route) {
1961 self.midi_hw_out_routes.push(route);
1962 }
1963 } else {
1964 self.notify_clients(Err(format!(
1965 "MIDI source track not found: {}",
1966 from_track
1967 )))
1968 .await;
1969 }
1970 } else {
1971 match (from_track_handle, to_track_handle) {
1972 (Some(f_t), Some(t_t)) => {
1973 let to_in_res = t_t.lock().midi.ins.get(to_port).cloned();
1974 if let Some(to_in) = to_in_res {
1975 let mut from_track = f_t.lock();
1976 if let Err(e) = from_track.midi.connect_out(from_port, to_in) {
1977 self.notify_clients(Err(e)).await;
1978 return;
1979 }
1980 from_track.invalidate_midi_route_cache();
1981 } else {
1982 self.notify_clients(Err(format!(
1983 "MIDI input port {} not found on track '{}'",
1984 to_port, to_track
1985 )))
1986 .await;
1987 }
1988 }
1989 _ => {
1990 self.notify_clients(Err(format!(
1991 "MIDI tracks not found: {} or {}",
1992 from_track, to_track
1993 )))
1994 .await;
1995 }
1996 }
1997 }
1998 }
1999 };
2000 }
2001
2002 pub(crate) async fn handle_disconnect(&mut self, action: Action) {
2003 let Action::Disconnect {
2004 ref from_track,
2005 from_port,
2006 ref to_track,
2007 to_port,
2008 kind,
2009 } = action
2010 else {
2011 return;
2012 };
2013
2014 if from_track == to_track && from_track != "hw:in" && to_track != "hw:out" {
2015 let Some(track) = self.state.lock().tracks.get(from_track).cloned() else {
2016 self.notify_clients(Err(format!("Track '{from_track}' not found")))
2017 .await;
2018 return;
2019 };
2020 match kind {
2021 Kind::Audio => {
2022 let result = {
2023 let mut track = track.lock();
2024 track.disconnect_audio_connectable(
2025 crate::connectable::ConnectableRef::TrackInput,
2026 to_port,
2027 crate::connectable::ConnectableRef::TrackOutput,
2028 from_port,
2029 )
2030 };
2031 if let Err(e) = result {
2032 self.notify_clients(Err(e)).await;
2033 return;
2034 }
2035 self.notify_clients(Ok(Action::TrackDisconnectAudio {
2036 track_name: from_track.clone(),
2037 from: crate::connectable::ConnectableRef::TrackInput,
2038 from_port: to_port,
2039 to: crate::connectable::ConnectableRef::TrackOutput,
2040 to_port: from_port,
2041 }))
2042 .await;
2043 }
2044 Kind::MIDI => {
2045 let result = {
2046 let mut track = track.lock();
2047 track.disconnect_midi_connectable(
2048 crate::connectable::ConnectableRef::TrackInput,
2049 to_port,
2050 crate::connectable::ConnectableRef::TrackOutput,
2051 from_port,
2052 )
2053 };
2054 if let Err(e) = result {
2055 self.notify_clients(Err(e)).await;
2056 return;
2057 }
2058 self.notify_clients(Ok(Action::TrackDisconnectMidi {
2059 track_name: from_track.clone(),
2060 from: crate::connectable::ConnectableRef::TrackInput,
2061 from_port: to_port,
2062 to: crate::connectable::ConnectableRef::TrackOutput,
2063 to_port: from_port,
2064 }))
2065 .await;
2066 }
2067 }
2068 return;
2069 }
2070
2071 if kind == Kind::Audio {
2072 if let Err(e) = self.disconnect_audio_route_and_notify(action.clone()).await {
2073 self.notify_clients(Err(e)).await;
2074 }
2075 } else if kind == Kind::MIDI {
2076 let from_hw_in_device = Self::midi_hw_in_device(from_track);
2077 let to_hw_out_device = Self::midi_hw_out_device(to_track);
2078
2079 if let (Some(from_device), Some(to_device)) = (from_hw_in_device, to_hw_out_device) {
2080 let before = self.midi_hw_thru_routes.len();
2081 self.midi_hw_thru_routes
2082 .retain(|r| !(r.from_device == from_device && r.to_device == to_device));
2083 if self.midi_hw_thru_routes.len() < before {
2084 self.notify_clients(Ok(action.clone())).await;
2085 } else {
2086 self.notify_clients(Err(format!(
2087 "Disconnect failed: MIDI route not found ({} -> {})",
2088 from_track, to_track
2089 )))
2090 .await;
2091 }
2092 return;
2093 }
2094
2095 if let Some(device) = from_hw_in_device {
2096 let before = self.midi_hw_in_routes.len();
2097 self.midi_hw_in_routes.retain(|r| {
2098 !(r.device == device && r.to_track == *to_track && r.to_port == to_port)
2099 });
2100 if self.midi_hw_in_routes.len() < before {
2101 self.notify_clients(Ok(action.clone())).await;
2102 } else {
2103 self.notify_clients(Err(format!(
2104 "Disconnect failed: MIDI route not found ({} -> {})",
2105 from_track, to_track
2106 )))
2107 .await;
2108 }
2109 return;
2110 }
2111
2112 if let Some(device) = to_hw_out_device {
2113 let before = self.midi_hw_out_routes.len();
2114 self.midi_hw_out_routes.retain(|r| {
2115 !(r.from_track == *from_track && r.from_port == from_port && r.device == device)
2116 });
2117 if self.midi_hw_out_routes.len() < before {
2118 self.notify_clients(Ok(action.clone())).await;
2119 } else {
2120 self.notify_clients(Err(format!(
2121 "Disconnect failed: MIDI route not found ({} -> {})",
2122 from_track, to_track
2123 )))
2124 .await;
2125 }
2126 return;
2127 }
2128
2129 let state = self.state.lock();
2130 if let (Some(f_t), Some(t_t)) =
2131 (state.tracks.get(from_track), state.tracks.get(to_track))
2132 && let Some(to_in) = t_t.lock().midi.ins.get(to_port).cloned()
2133 {
2134 let mut from_track = f_t.lock();
2135 if let Err(e) = from_track.midi.disconnect_out(from_port, &to_in) {
2136 self.notify_clients(Err(e)).await;
2137 } else {
2138 from_track.invalidate_midi_route_cache();
2139 self.notify_clients(Ok(action.clone())).await;
2140 }
2141 } else {
2142 self.notify_clients(Err(format!(
2143 "Disconnect failed: MIDI ports not found ({} -> {})",
2144 from_track, to_track
2145 )))
2146 .await;
2147 }
2148 }
2149 }
2150
2151 pub(crate) async fn handle_track_set_parent(
2152 &mut self,
2153 track_name: &str,
2154 parent_name: Option<&str>,
2155 ) {
2156 let track = match self.track_handle_or_err(track_name) {
2157 Ok(track) => track,
2158 Err(e) => {
2159 self.notify_clients(Err(e)).await;
2160 return;
2161 }
2162 };
2163 if parent_name == Some(track_name) {
2164 self.notify_clients(Err("Track cannot be its own parent".to_string()))
2165 .await;
2166 return;
2167 }
2168
2169 if parent_name.is_some() && track.lock().is_master() {
2171 self.notify_clients(Err(format!(
2172 "Track '{}' is the master track and cannot be made part of a folder",
2173 track_name
2174 )))
2175 .await;
2176 return;
2177 }
2178
2179 if let Some(parent_name) = parent_name {
2181 let state = self.state.lock();
2182 let parent = state.tracks.get(parent_name);
2183 if parent.is_none() {
2184 self.notify_clients(Err(format!(
2185 "Parent track '{}' does not exist",
2186 parent_name
2187 )))
2188 .await;
2189 return;
2190 }
2191 if !parent.unwrap().lock().is_folder {
2192 self.notify_clients(Err(format!("Track '{}' is not a folder", parent_name)))
2193 .await;
2194 return;
2195 }
2196 }
2197
2198 {
2200 let old_parent_name = track.lock().parent_track.clone();
2201 if let Some(old_parent_name) = old_parent_name {
2202 let state = self.state.lock();
2203 if let (Some(parent_arc), Some(child_arc)) = (
2204 state.tracks.get(&old_parent_name).cloned(),
2205 state.tracks.get(track_name).cloned(),
2206 ) {
2207 {
2208 let mut parent = parent_arc.lock();
2209 parent.child_tracks.retain(|c| c.lock().name != *track_name);
2210 }
2211 {
2212 let mut child = child_arc.lock();
2213 let parent = parent_arc.lock();
2214 child.disconnect_from_parent(&parent);
2215 }
2216 }
2217 }
2218 }
2219
2220 let mut disconnect_actions = Vec::new();
2221
2222 {
2224 let state = self.state.lock();
2225 let hw_inputs = self.all_hw_input_audio_ports();
2226 let hw_outputs = self.all_hw_output_audio_ports();
2227 if let Some(child_arc) = state.tracks.get(track_name).cloned() {
2228 let child = child_arc.lock();
2229 for (port_idx, inp) in child.audio.ins.iter().enumerate() {
2230 let sources = inp.connections();
2231 for src in sources.iter() {
2232 let _ = AudioIO::disconnect(src, inp);
2233 if let Some((src_name, src_port)) =
2234 self.find_audio_io_owner(self.state.as_ref(), src)
2235 {
2236 disconnect_actions.push(Action::Disconnect {
2237 from_track: src_name,
2238 from_port: src_port,
2239 to_track: track_name.to_string(),
2240 to_port: port_idx,
2241 kind: Kind::Audio,
2242 });
2243 } else if let Some(src_port) = hw_inputs
2244 .iter()
2245 .position(|hw_in| std::sync::Arc::ptr_eq(hw_in, src))
2246 {
2247 disconnect_actions.push(Action::Disconnect {
2248 from_track: "hw:in".to_string(),
2249 from_port: src_port,
2250 to_track: track_name.to_string(),
2251 to_port: port_idx,
2252 kind: Kind::Audio,
2253 });
2254 }
2255 }
2256 }
2257 for (port_idx, out) in child.audio.outs.iter().enumerate() {
2258 let targets = out.connections();
2259 for tgt in targets.iter() {
2260 let _ = AudioIO::disconnect(out, tgt);
2261 if let Some((tgt_name, tgt_port)) =
2262 self.find_audio_io_owner(self.state.as_ref(), tgt)
2263 {
2264 disconnect_actions.push(Action::Disconnect {
2265 from_track: track_name.to_string(),
2266 from_port: port_idx,
2267 to_track: tgt_name,
2268 to_port: tgt_port,
2269 kind: Kind::Audio,
2270 });
2271 } else if let Some(tgt_port) = hw_outputs
2272 .iter()
2273 .position(|hw_out| std::sync::Arc::ptr_eq(hw_out, tgt))
2274 {
2275 disconnect_actions.push(Action::Disconnect {
2276 from_track: track_name.to_string(),
2277 from_port: port_idx,
2278 to_track: "hw:out".to_string(),
2279 to_port: tgt_port,
2280 kind: Kind::Audio,
2281 });
2282 }
2283 }
2284 }
2285
2286 for route in self
2288 .midi_hw_in_routes
2289 .iter()
2290 .filter(|r| r.to_track == track_name)
2291 {
2292 disconnect_actions.push(Action::Disconnect {
2293 from_track: format!("midi:hw:in:{}", route.device),
2294 from_port: 0,
2295 to_track: track_name.to_string(),
2296 to_port: route.to_port,
2297 kind: Kind::MIDI,
2298 });
2299 }
2300 self.midi_hw_in_routes.retain(|r| r.to_track != track_name);
2301
2302 for route in self
2303 .midi_hw_out_routes
2304 .iter()
2305 .filter(|r| r.from_track == track_name)
2306 {
2307 disconnect_actions.push(Action::Disconnect {
2308 from_track: track_name.to_string(),
2309 from_port: route.from_port,
2310 to_track: format!("midi:hw:out:{}", route.device),
2311 to_port: 0,
2312 kind: Kind::MIDI,
2313 });
2314 }
2315 self.midi_hw_out_routes
2316 .retain(|r| r.from_track != track_name);
2317
2318 for (port_idx, out) in child.midi.outs.iter().enumerate() {
2320 let targets = out.connections();
2321 for tgt in targets {
2322 if let Some((tgt_name, tgt_port, _)) =
2323 self.find_midi_io_owner(self.state.as_ref(), &tgt)
2324 {
2325 let _ = MIDIIO::disconnect(out, &tgt);
2326 disconnect_actions.push(Action::Disconnect {
2327 from_track: track_name.to_string(),
2328 from_port: port_idx,
2329 to_track: tgt_name,
2330 to_port: tgt_port,
2331 kind: Kind::MIDI,
2332 });
2333 }
2334 }
2335 }
2336 }
2337
2338 let child_input_arcs: Vec<_> =
2340 if let Some(child_arc) = state.tracks.get(track_name).cloned() {
2341 let child = child_arc.lock();
2342 child.midi.ins.clone()
2343 } else {
2344 Vec::new()
2345 };
2346 for (other_name, other_track) in &state.tracks {
2347 if other_name.as_str() == track_name {
2348 continue;
2349 }
2350 let other = other_track.lock();
2351 for (out_port, out) in other.midi.outs.iter().enumerate() {
2352 let targets = out.connections();
2353 for tgt in targets {
2354 if let Some(to_port) = child_input_arcs
2355 .iter()
2356 .position(|inp| std::sync::Arc::ptr_eq(inp, &tgt))
2357 {
2358 let _ = MIDIIO::disconnect(out, &tgt);
2359 disconnect_actions.push(Action::Disconnect {
2360 from_track: other_name.clone(),
2361 from_port: out_port,
2362 to_track: track_name.to_string(),
2363 to_port,
2364 kind: Kind::MIDI,
2365 });
2366 }
2367 }
2368 }
2369 }
2370 }
2371
2372 {
2374 track.lock().parent_track = parent_name.map(str::to_string);
2375 }
2376
2377 if let Some(parent_name) = parent_name {
2379 let state = self.state.lock();
2380 if let (Some(parent_arc), Some(child_arc)) = (
2381 state.tracks.get(parent_name).cloned(),
2382 state.tracks.get(track_name).cloned(),
2383 ) {
2384 {
2385 let mut parent = parent_arc.lock();
2386 parent.child_tracks.push(child_arc.clone());
2387 }
2388 {
2389 let mut child = child_arc.lock();
2390 let mut parent = parent_arc.lock();
2391 if parent.audio.ins.len() == child.audio.ins.len() {
2393 for (parent_in, child_in) in
2394 parent.audio.ins.iter().zip(child.audio.ins.iter())
2395 {
2396 Track::connect_directed_audio(parent_in, child_in);
2397 }
2398 }
2399 if parent.audio.outs.len() == child.audio.outs.len() {
2401 for (child_out, parent_out) in
2402 child.audio.outs.iter().zip(parent.audio.outs.iter())
2403 {
2404 AudioIO::connect(child_out, parent_out);
2405 }
2406 }
2407 if parent.midi.ins.len() == child.midi.ins.len() {
2409 for (parent_in, child_in) in
2410 parent.midi.ins.iter().zip(child.midi.ins.iter())
2411 {
2412 child_in.add_connection(parent_in);
2413 }
2414 }
2415 if parent.midi.outs.len() == child.midi.outs.len() {
2417 for (child_out, parent_out) in
2418 child.midi.outs.iter().zip(parent.midi.outs.iter())
2419 {
2420 child_out.add_connection(parent_out);
2421 }
2422 }
2423 child.invalidate_audio_route_cache();
2424 parent.invalidate_audio_route_cache();
2425 child.invalidate_midi_route_cache();
2426 parent.invalidate_midi_route_cache();
2427 }
2428 }
2429 }
2430
2431 {
2434 let state = self.state.lock();
2435 if let Some(child_arc) = state.tracks.get(track_name).cloned() {
2436 let mut child = child_arc.lock();
2437 child.ensure_default_audio_passthrough();
2438 child.ensure_default_midi_passthrough();
2439 }
2440 }
2441
2442 for action in disconnect_actions {
2443 self.notify_clients(Ok(action)).await;
2444 }
2445
2446 self.notify_clients(Ok(Action::TrackSetParent {
2447 track_name: track_name.to_string(),
2448 parent_name: parent_name.map(str::to_string),
2449 }))
2450 .await;
2451 }
2452
2453 pub(crate) async fn handle_clip_move(&mut self, action: Action) {
2454 let Action::ClipMove {
2455 ref kind,
2456 ref from,
2457 ref to,
2458 copy,
2459 } = action
2460 else {
2461 return;
2462 };
2463 if let Some(from_track_handle) = self.state.lock().tracks.get(&from.track_name)
2464 && let Some(to_track_handle) = self.state.lock().tracks.get(&to.track_name)
2465 {
2466 let from_track = from_track_handle.lock();
2467 let mut to_track = to_track_handle.lock();
2468 match kind {
2469 Kind::Audio => {
2470 if from.clip_index >= from_track.audio.clips().len() {
2471 self.notify_clients(Err(format!(
2472 "Clip index {} is too high, as track {} has only {} clips!",
2473 from.clip_index,
2474 from_track.name.clone(),
2475 from_track.audio.clips().len(),
2476 )))
2477 .await;
2478 return;
2479 }
2480 if from_track.audio.ins.len() != to_track.audio.ins.len() {
2481 self.notify_clients(Err(format!(
2482 "Cannot move/copy audio clip from '{}' ({} inputs) to '{}' ({} inputs)",
2483 from_track.name,
2484 from_track.audio.ins.len(),
2485 to_track.name,
2486 to_track.audio.ins.len()
2487 )))
2488 .await;
2489 return;
2490 }
2491 let Some(clip_copy) = from_track.audio.clips().get(from.clip_index).cloned()
2492 else {
2493 return;
2494 };
2495 if !copy {
2496 from_track.audio.remove_clip(from.clip_index);
2497 }
2498 let mut clip_copy = (*clip_copy).clone();
2499 let clip_len = clip_copy.end.saturating_sub(clip_copy.start).max(1);
2500 clip_copy.start = to.sample_offset;
2501 clip_copy.end = clip_copy.start.saturating_add(clip_len);
2502 let max_lane = to_track.audio.ins.len().saturating_sub(1);
2503 clip_copy.input_channel = to.input_channel.min(max_lane);
2504 Self::push_audio_clip_with_cross_section_fades(&mut to_track, clip_copy);
2505 }
2506 Kind::MIDI => {
2507 if from.clip_index >= from_track.midi.clips().len() {
2508 self.notify_clients(Err(format!(
2509 "Clip index {} is too high, as track {} has only {} clips!",
2510 from.clip_index,
2511 from_track.name.clone(),
2512 from_track.midi.clips().len(),
2513 )))
2514 .await;
2515 return;
2516 }
2517 let Some(clip_copy) = from_track.midi.clips().get(from.clip_index).cloned()
2518 else {
2519 return;
2520 };
2521 if !copy {
2522 from_track.midi.remove_clip(from.clip_index);
2523 }
2524 let mut clip_copy = (*clip_copy).clone();
2525 let clip_len = clip_copy.end.saturating_sub(clip_copy.start).max(1);
2526 clip_copy.start = to.sample_offset;
2527 clip_copy.end = clip_copy.start.saturating_add(clip_len);
2528 let max_lane = to_track.midi.ins.len().saturating_sub(1);
2529 clip_copy.input_channel = to.input_channel.min(max_lane);
2530 to_track.midi.push_clip(clip_copy);
2531 }
2532 }
2533 }
2534 }
2535
2536 pub(crate) async fn handle_rename_track(&mut self, action: Action) -> bool {
2537 let Action::RenameTrack {
2538 ref old_name,
2539 ref new_name,
2540 } = action
2541 else {
2542 return false;
2543 };
2544
2545 if self.state.lock().tracks.contains_key(new_name) {
2546 self.notify_clients(Err(format!("Track '{}' already exists", new_name)))
2547 .await;
2548 return true;
2549 }
2550
2551 let Some(track) = self.state.lock().tracks.remove(old_name) else {
2552 self.notify_clients(Err(format!("Track '{}' not found", old_name)))
2553 .await;
2554 return true;
2555 };
2556
2557 track.lock().name = new_name.clone();
2558 self.state.lock().tracks.insert(new_name.clone(), track);
2559 for other in self.state.lock().tracks.values() {
2560 let mut other = other.lock();
2561 if other.parent_track.as_deref() == Some(old_name.as_str()) {
2562 other.parent_track = Some(new_name.clone());
2563 }
2564 }
2565
2566 if let Some(recording) = self.audio_recordings.remove(old_name) {
2567 self.audio_recordings.insert(new_name.clone(), recording);
2568 }
2569 if let Some(recording) = self.midi_recordings.remove(old_name) {
2570 self.midi_recordings.insert(new_name.clone(), recording);
2571 }
2572
2573 for route in &mut self.midi_hw_in_routes {
2574 if route.to_track == *old_name {
2575 route.to_track = new_name.clone();
2576 }
2577 }
2578 for route in &mut self.midi_hw_out_routes {
2579 if route.from_track == *old_name {
2580 route.from_track = new_name.clone();
2581 }
2582 }
2583 if let Some((armed_track, target, device)) = self.pending_midi_learn.clone()
2584 && armed_track == *old_name
2585 {
2586 self.pending_midi_learn = Some((new_name.clone(), target, device));
2587 }
2588
2589 self.notify_clients(Ok(Action::RenameTrack {
2590 old_name: old_name.clone(),
2591 new_name: new_name.clone(),
2592 }))
2593 .await;
2594
2595 false
2596 }
2597
2598 pub(crate) async fn handle_add_clip(&mut self, action: Action) -> bool {
2599 let Action::AddClip {
2600 ref clip_id,
2601 ref name,
2602 ref track_name,
2603 start,
2604 length,
2605 offset,
2606 input_channel,
2607 muted,
2608 ref peaks_file,
2609 kind,
2610 fade_enabled,
2611 fade_in_samples,
2612 fade_out_samples,
2613 ref source_name,
2614 source_offset,
2615 source_length,
2616 ref preview_name,
2617 ref pitch_correction_points,
2618 pitch_correction_frame_likeness,
2619 pitch_correction_inertia_ms,
2620 pitch_correction_formant_compensation,
2621 ref plugin_graph_json,
2622 } = action
2623 else {
2624 return false;
2625 };
2626
2627 self.add_clip_to_track(ClipAddRequest {
2628 clip_id,
2629 name,
2630 track_name,
2631 start,
2632 length,
2633 offset,
2634 input_channel,
2635 muted,
2636 peaks_file: peaks_file.clone(),
2637 kind,
2638 fade_enabled,
2639 fade_in_samples,
2640 fade_out_samples,
2641 source_name: source_name.clone(),
2642 source_offset,
2643 source_length,
2644 preview_name: preview_name.clone(),
2645 pitch_correction_points: pitch_correction_points.clone(),
2646 pitch_correction_frame_likeness,
2647 pitch_correction_inertia_ms,
2648 pitch_correction_formant_compensation,
2649 plugin_graph_json: plugin_graph_json.clone(),
2650 });
2651 if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
2652 let track_name = track_name.clone();
2653 tokio::task::spawn_blocking(move || {
2654 track.lock().preload_clips();
2655 tracing::debug!("Preloaded clips for track '{}' after AddClip", track_name);
2656 });
2657 }
2658
2659 false
2660 }
2661
2662 pub(crate) async fn handle_track_set_folder(&mut self, a: Action) -> bool {
2663 let Action::TrackSetFolder {
2664 ref track_name,
2665 is_folder,
2666 } = a
2667 else {
2668 return false;
2669 };
2670
2671 let track = match self.track_handle_or_err(track_name) {
2672 Ok(track) => track,
2673 Err(e) => {
2674 self.notify_clients(Err(e)).await;
2675 return true;
2676 }
2677 };
2678 if is_folder {
2679 let is_master = track.lock().is_master();
2680 if is_master {
2681 self.notify_clients(Err(format!(
2682 "Track '{}' is the master track and cannot be made a folder",
2683 track_name
2684 )))
2685 .await;
2686 return true;
2687 }
2688 }
2689 {
2690 let mut track = track.lock();
2691 track.is_folder = is_folder;
2692 track.ensure_default_audio_passthrough();
2693 track.ensure_default_midi_passthrough();
2694 }
2695 self.notify_clients(Ok(Action::TrackSetFolder {
2696 track_name: track_name.clone(),
2697 is_folder,
2698 }))
2699 .await;
2700
2701 false
2702 }
2703
2704 pub(crate) async fn handle_track_connect_audio(&mut self, a: Action) -> bool {
2705 let Action::TrackConnectAudio {
2706 ref track_name,
2707 ref from,
2708 from_port,
2709 ref to,
2710 to_port,
2711 } = a
2712 else {
2713 return false;
2714 };
2715
2716 if self
2717 .reject_if_track_frozen(track_name, "routing changes")
2718 .await
2719 {
2720 return true;
2721 }
2722 let track = match self.track_handle_or_err(track_name) {
2723 Ok(track) => track,
2724 Err(e) => {
2725 self.notify_clients(Err(e)).await;
2726 return true;
2727 }
2728 };
2729 if let Err(e) =
2730 track
2731 .lock()
2732 .connect_audio_connectable(from.clone(), from_port, to.clone(), to_port)
2733 {
2734 self.notify_clients(Err(e)).await;
2735 return true;
2736 }
2737
2738 false
2739 }
2740
2741 pub(crate) async fn handle_track_disconnect_audio(&mut self, a: Action) -> bool {
2742 let Action::TrackDisconnectAudio {
2743 ref track_name,
2744 ref from,
2745 from_port,
2746 ref to,
2747 to_port,
2748 } = a
2749 else {
2750 return false;
2751 };
2752
2753 if self
2754 .reject_if_track_frozen(track_name, "routing changes")
2755 .await
2756 {
2757 return true;
2758 }
2759 let track = match self.track_handle_or_err(track_name) {
2760 Ok(track) => track,
2761 Err(e) => {
2762 self.notify_clients(Err(e)).await;
2763 return true;
2764 }
2765 };
2766 if let Err(e) =
2767 track
2768 .lock()
2769 .disconnect_audio_connectable(from.clone(), from_port, to.clone(), to_port)
2770 {
2771 self.notify_clients(Err(e)).await;
2772 return true;
2773 }
2774
2775 false
2776 }
2777
2778 pub(crate) async fn handle_track_connect_midi(&mut self, a: Action) -> bool {
2779 let Action::TrackConnectMidi {
2780 ref track_name,
2781 ref from,
2782 from_port,
2783 ref to,
2784 to_port,
2785 } = a
2786 else {
2787 return false;
2788 };
2789
2790 if self
2791 .reject_if_track_frozen(track_name, "routing changes")
2792 .await
2793 {
2794 return true;
2795 }
2796 let track = match self.track_handle_or_err(track_name) {
2797 Ok(track) => track,
2798 Err(e) => {
2799 self.notify_clients(Err(e)).await;
2800 return true;
2801 }
2802 };
2803 if let Err(e) =
2804 track
2805 .lock()
2806 .connect_midi_connectable(from.clone(), from_port, to.clone(), to_port)
2807 {
2808 self.notify_clients(Err(e)).await;
2809 return true;
2810 }
2811
2812 false
2813 }
2814
2815 pub(crate) async fn handle_track_disconnect_midi(&mut self, a: Action) -> bool {
2816 let Action::TrackDisconnectMidi {
2817 ref track_name,
2818 ref from,
2819 from_port,
2820 ref to,
2821 to_port,
2822 } = a
2823 else {
2824 return false;
2825 };
2826
2827 if self
2828 .reject_if_track_frozen(track_name, "routing changes")
2829 .await
2830 {
2831 return true;
2832 }
2833 let track = match self.track_handle_or_err(track_name) {
2834 Ok(track) => track,
2835 Err(e) => {
2836 self.notify_clients(Err(e)).await;
2837 return true;
2838 }
2839 };
2840 if let Err(e) =
2841 track
2842 .lock()
2843 .disconnect_midi_connectable(from.clone(), from_port, to.clone(), to_port)
2844 {
2845 self.notify_clients(Err(e)).await;
2846 return true;
2847 }
2848
2849 false
2850 }
2851
2852 pub(crate) async fn handle_set_clip_pitch_correction(&mut self, a: Action) -> bool {
2853 let Action::SetClipPitchCorrection {
2854 ref track_name,
2855 clip_index,
2856 ref preview_name,
2857 ref source_name,
2858 source_offset,
2859 source_length,
2860 ref pitch_correction_points,
2861 pitch_correction_frame_likeness,
2862 pitch_correction_inertia_ms,
2863 pitch_correction_formant_compensation,
2864 } = a
2865 else {
2866 return false;
2867 };
2868
2869 self.set_clip_pitch_correction(
2870 track_name,
2871 clip_index,
2872 preview_name.clone(),
2873 source_name.clone(),
2874 source_offset,
2875 source_length,
2876 pitch_correction_points.clone(),
2877 pitch_correction_frame_likeness,
2878 pitch_correction_inertia_ms,
2879 pitch_correction_formant_compensation,
2880 );
2881
2882 false
2883 }
2884
2885 pub(crate) async fn handle_track_remove_audio_output(&mut self, a: Action) -> bool {
2886 let Action::TrackRemoveAudioOutput(ref name) = a else {
2887 return false;
2888 };
2889
2890 let track = match self.track_handle_or_err(name) {
2891 Ok(track) => track,
2892 Err(e) => {
2893 self.notify_clients(Err(e)).await;
2894 return true;
2895 }
2896 };
2897 let (hw_outputs, track_inputs) = {
2898 let state = self.state.lock();
2899 let hw_outputs = self.all_hw_output_audio_ports();
2900 let track_inputs = state
2901 .tracks
2902 .iter()
2903 .filter(|(track_name, _)| *track_name != name)
2904 .flat_map(|(_, handle)| handle.lock().audio.ins.clone())
2905 .collect::<Vec<_>>();
2906 (hw_outputs, track_inputs)
2907 };
2908 if let Err(e) = track.lock().remove_audio_output(&hw_outputs, &track_inputs) {
2909 self.notify_clients(Err(e)).await;
2910 return true;
2911 }
2912
2913 false
2914 }
2915
2916 pub(crate) async fn handle_track_toggle_folder(&mut self, a: Action) -> bool {
2917 let Action::TrackToggleFolder { ref track_name } = a else {
2918 return false;
2919 };
2920
2921 let track = match self.track_handle_or_err(track_name) {
2922 Ok(track) => track,
2923 Err(e) => {
2924 self.notify_clients(Err(e)).await;
2925 return true;
2926 }
2927 };
2928 {
2929 let t = track.lock();
2930 t.folder_open.fetch_not(Ordering::Relaxed);
2931 }
2932 self.notify_clients(Ok(Action::TrackToggleFolder {
2933 track_name: track_name.clone(),
2934 }))
2935 .await;
2936
2937 self.notify_clients(Ok(Action::TrackSetFolder {
2938 track_name: track_name.clone(),
2939 is_folder: track.lock().is_folder,
2940 }))
2941 .await;
2942
2943 false
2944 }
2945
2946 pub(crate) async fn handle_add_grouped_clip(&mut self, a: Action) -> bool {
2947 let Action::AddGroupedClip {
2948 ref track_name,
2949 kind,
2950 ref audio_clip,
2951 ref midi_clip,
2952 } = a
2953 else {
2954 return false;
2955 };
2956
2957 self.add_grouped_clip_to_track(track_name, kind, audio_clip.clone(), midi_clip.clone());
2958 if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
2959 let track_name = track_name.clone();
2960 tokio::task::spawn_blocking(move || {
2961 track.lock().preload_clips();
2962 tracing::debug!(
2963 "Preloaded clips for track '{}' after AddGroupedClip",
2964 track_name
2965 );
2966 });
2967 }
2968
2969 false
2970 }
2971
2972 pub(crate) async fn handle_track_add_audio_input(&mut self, a: Action) -> bool {
2973 let Action::TrackAddAudioInput(ref name) = a else {
2974 return false;
2975 };
2976
2977 let track = match self.track_handle_or_err(name) {
2978 Ok(track) => track,
2979 Err(e) => {
2980 self.notify_clients(Err(e)).await;
2981 return true;
2982 }
2983 };
2984 if let Err(e) = track.lock().add_audio_input() {
2985 self.notify_clients(Err(e)).await;
2986 return true;
2987 }
2988
2989 false
2990 }
2991
2992 pub(crate) async fn handle_track_add_audio_output(&mut self, a: Action) -> bool {
2993 let Action::TrackAddAudioOutput(ref name) = a else {
2994 return false;
2995 };
2996
2997 let track = match self.track_handle_or_err(name) {
2998 Ok(track) => track,
2999 Err(e) => {
3000 self.notify_clients(Err(e)).await;
3001 return true;
3002 }
3003 };
3004 if let Err(e) = track.lock().add_audio_output() {
3005 self.notify_clients(Err(e)).await;
3006 return true;
3007 }
3008
3009 false
3010 }
3011
3012 pub(crate) async fn handle_track_remove_audio_input(&mut self, a: Action) -> bool {
3013 let Action::TrackRemoveAudioInput(ref name) = a else {
3014 return false;
3015 };
3016
3017 let track = match self.track_handle_or_err(name) {
3018 Ok(track) => track,
3019 Err(e) => {
3020 self.notify_clients(Err(e)).await;
3021 return true;
3022 }
3023 };
3024 if let Err(e) = track.lock().remove_audio_input() {
3025 self.notify_clients(Err(e)).await;
3026 return true;
3027 }
3028
3029 false
3030 }
3031
3032 pub(crate) async fn handle_track_midi_cc(&mut self, a: Action) -> bool {
3033 let Action::TrackMidiCc {
3034 ref track_name,
3035 channel,
3036 cc,
3037 value,
3038 } = a
3039 else {
3040 return false;
3041 };
3042
3043 if let Some(track) = self.state.lock().tracks.get(track_name) {
3044 track
3045 .lock()
3046 .rt
3047 .pending_automation_midi_events
3048 .push(MidiEvent::new(
3049 0,
3050 vec![0xB0 | channel.min(15), cc.min(127), value.min(127)],
3051 ));
3052 }
3053
3054 false
3055 }
3056
3057 pub(crate) async fn handle_track_toggle_arm(&mut self, a: Action) -> bool {
3058 let Action::TrackToggleArm(ref name) = a else {
3059 return false;
3060 };
3061
3062 if self.reject_if_track_frozen(name, "arming/disarming").await {
3063 return true;
3064 }
3065 if let Some(track) = self.state.lock().tracks.get(name).cloned() {
3066 track.lock().arm();
3067 let armed = track.lock().armed();
3068 if !armed && self.audio_recordings.contains_key(name) {
3069 self.flush_track_recording(name).await;
3070 }
3071 } else {
3072 tracing::warn!(
3073 "TrackToggleArm for '{}' but track not found in engine",
3074 name
3075 );
3076 }
3077
3078 false
3079 }
3080
3081 pub(crate) async fn handle_track_set_midi_lane_channel(&mut self, a: Action) -> bool {
3082 let Action::TrackSetMidiLaneChannel {
3083 ref track_name,
3084 lane,
3085 channel,
3086 } = a
3087 else {
3088 return false;
3089 };
3090
3091 let track = match self.track_handle_or_err(track_name) {
3092 Ok(track) => track,
3093 Err(e) => {
3094 self.notify_clients(Err(e)).await;
3095 return true;
3096 }
3097 };
3098 track.lock().set_midi_lane_channel(lane, channel);
3099
3100 false
3101 }
3102
3103 pub(crate) async fn handle_track_set_mpe_zone(&mut self, a: Action) -> bool {
3104 let Action::TrackSetMpeZone {
3105 ref track_name,
3106 manager_channel,
3107 member_count,
3108 } = a
3109 else {
3110 return false;
3111 };
3112
3113 let track = match self.track_handle_or_err(track_name) {
3114 Ok(track) => track,
3115 Err(e) => {
3116 self.notify_clients(Err(e)).await;
3117 return true;
3118 }
3119 };
3120 track.lock().set_mpe_zone(manager_channel, member_count);
3121
3122 false
3123 }
3124
3125 pub(crate) async fn handle_track_set_mpe_pitch_bend_sensitivity(&mut self, a: Action) -> bool {
3126 let Action::TrackSetMpePitchBendSensitivity {
3127 ref track_name,
3128 channel,
3129 semitones,
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
3143 .lock()
3144 .set_mpe_pitch_bend_sensitivity(channel, semitones);
3145
3146 false
3147 }
3148
3149 pub(crate) async fn handle_track_set_frozen(&mut self, a: Action) -> bool {
3150 let Action::TrackSetFrozen {
3151 ref track_name,
3152 frozen,
3153 } = a
3154 else {
3155 return false;
3156 };
3157
3158 let track = match self.track_handle_or_err(track_name) {
3159 Ok(track) => track,
3160 Err(e) => {
3161 self.notify_clients(Err(e)).await;
3162 return true;
3163 }
3164 };
3165 track.lock().set_frozen(frozen);
3166
3167 false
3168 }
3169
3170 pub(crate) async fn handle_track_clear_default_passthrough(&mut self, a: Action) -> bool {
3171 let Action::TrackClearDefaultPassthrough { ref track_name } = a else {
3172 return false;
3173 };
3174
3175 if self
3176 .reject_if_track_frozen(track_name, "plugin graph editing")
3177 .await
3178 {
3179 return true;
3180 }
3181 let track = match self.track_handle_or_err(track_name) {
3182 Ok(track) => track,
3183 Err(e) => {
3184 self.notify_clients(Err(e)).await;
3185 return true;
3186 }
3187 };
3188 track.lock().clear_default_passthrough();
3189
3190 false
3191 }
3192
3193 pub(crate) async fn handle_set_clip_fade(&mut self, a: Action) -> bool {
3194 let Action::SetClipFade {
3195 ref track_name,
3196 clip_index,
3197 kind,
3198 fade_enabled,
3199 fade_in_samples,
3200 fade_out_samples,
3201 } = a
3202 else {
3203 return false;
3204 };
3205
3206 self.set_clip_fade(
3207 track_name,
3208 clip_index,
3209 kind,
3210 fade_enabled,
3211 fade_in_samples,
3212 fade_out_samples,
3213 );
3214
3215 false
3216 }
3217}
3218
3219#[cfg(test)]
3220mod tests {
3221 use super::*;
3222
3223 fn audio_clip(start: usize, len: usize) -> AudioClip {
3224 AudioClip::new("clip.wav".to_string(), start, start.saturating_add(len))
3225 }
3226
3227 #[test]
3228 fn cross_section_fades_match_non_aligned_overlap_length() {
3229 let mut existing = vec![audio_clip(0, 100)];
3230 let mut inserted = audio_clip(80, 100);
3231
3232 Engine::apply_audio_cross_section_fades(&mut existing, &mut inserted);
3233
3234 assert!(existing[0].fade_enabled);
3235 assert_eq!(existing[0].fade_out_samples, 20);
3236 assert!(inserted.fade_enabled);
3237 assert_eq!(inserted.fade_in_samples, 20);
3238 }
3239
3240 #[test]
3241 fn cross_section_fades_ignore_aligned_starts_and_ends() {
3242 let mut same_start = vec![audio_clip(0, 100)];
3243 let mut inserted_same_start = audio_clip(0, 40);
3244 Engine::apply_audio_cross_section_fades(&mut same_start, &mut inserted_same_start);
3245 assert_eq!(same_start[0].fade_in_samples, 240);
3246 assert_eq!(inserted_same_start.fade_in_samples, 240);
3247
3248 let mut same_end = vec![audio_clip(0, 100)];
3249 let mut inserted_same_end = audio_clip(60, 40);
3250 Engine::apply_audio_cross_section_fades(&mut same_end, &mut inserted_same_end);
3251 assert_eq!(same_end[0].fade_out_samples, 240);
3252 assert_eq!(inserted_same_end.fade_out_samples, 240);
3253 }
3254}