Skip to main content

maolan_engine/engine/
topology.rs

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