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