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