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