Skip to main content

maolan_engine/engine/
topology.rs

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