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