Skip to main content

maolan_engine/engine/
topology.rs

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