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.peaks_file = request.peaks_file;
682                    clip.fade_enabled = request.fade_enabled;
683                    clip.fade_in_samples = request.fade_in_samples;
684                    clip.fade_out_samples = request.fade_out_samples;
685                    clip.pitch_correction_preview_name = request.preview_name;
686                    clip.pitch_correction_source_name = request.source_name;
687                    clip.pitch_correction_source_offset = request.source_offset;
688                    clip.pitch_correction_source_length = request.source_length;
689                    clip.pitch_correction_points = request.pitch_correction_points;
690                    clip.pitch_correction_frame_likeness = request.pitch_correction_frame_likeness;
691                    clip.pitch_correction_inertia_ms = request.pitch_correction_inertia_ms;
692                    clip.pitch_correction_formant_compensation =
693                        request.pitch_correction_formant_compensation;
694                    clip.plugin_graph_json = request.plugin_graph_json;
695                    Self::push_audio_clip_with_cross_section_fades(&mut track, clip);
696                    #[cfg(unix)]
697                    track.rt.clip_pitch_shifters.clear();
698                }
699                Kind::MIDI => {
700                    let mut clip = MIDIClip::new(
701                        request.name.to_string(),
702                        request.start,
703                        request.start.saturating_add(request.length.max(1)),
704                    );
705                    clip.id = request.clip_id.to_string();
706                    clip.offset = request.offset;
707                    let max_lane = track.midi.ins.len().saturating_sub(1);
708                    clip.input_channel = request.input_channel.min(max_lane);
709                    clip.muted = request.muted;
710                    track.midi.push_clip(clip);
711                }
712            }
713            track
714                .rt
715                .session_clip_pool_audio
716                .retain(|clip| clip.id != request.clip_id);
717            track
718                .rt
719                .session_clip_pool_midi
720                .retain(|clip| clip.id != request.clip_id);
721            self.take_clip_from_unused(request.clip_id);
722        }
723    }
724
725    pub(crate) fn audio_clip_from_data(data: &crate::message::AudioClipData) -> AudioClip {
726        let mut clip = AudioClip::new(
727            data.name.clone(),
728            data.start,
729            data.start.saturating_add(data.length.max(1)),
730        );
731        clip.id = data.id.clone();
732        clip.offset = data.offset;
733        clip.input_channel = data.input_channel;
734        clip.muted = data.muted;
735        clip.peaks_file = data.peaks_file.clone();
736        clip.fade_enabled = data.fade_enabled;
737        clip.fade_in_samples = data.fade_in_samples;
738        clip.fade_out_samples = data.fade_out_samples;
739        clip.pitch_correction_preview_name = data.preview_name.clone();
740        clip.pitch_correction_source_name = data.source_name.clone();
741        clip.pitch_correction_source_offset = data.source_offset;
742        clip.pitch_correction_source_length = data.source_length;
743        clip.pitch_correction_points = data.pitch_correction_points.clone();
744        clip.pitch_correction_frame_likeness = data.pitch_correction_frame_likeness;
745        clip.pitch_correction_inertia_ms = data.pitch_correction_inertia_ms;
746        clip.pitch_correction_formant_compensation = data.pitch_correction_formant_compensation;
747        clip.plugin_graph_json = data.plugin_graph_json.clone();
748        clip.grouped_clips = data
749            .grouped_clips
750            .iter()
751            .map(Self::audio_clip_from_data)
752            .collect();
753        for child in &mut clip.grouped_clips {
754            child.fade_enabled = false;
755            child.fade_in_samples = 0;
756            child.fade_out_samples = 0;
757        }
758        clip
759    }
760
761    pub(crate) fn midi_clip_from_data(data: &crate::message::MidiClipData) -> MIDIClip {
762        let mut clip = MIDIClip::new(
763            data.name.clone(),
764            data.start,
765            data.start.saturating_add(data.length.max(1)),
766        );
767        clip.id = data.id.clone();
768        clip.offset = data.offset;
769        clip.input_channel = data.input_channel;
770        clip.muted = data.muted;
771        clip.grouped_clips = data
772            .grouped_clips
773            .iter()
774            .map(Self::midi_clip_from_data)
775            .collect();
776        clip
777    }
778
779    pub(crate) fn add_grouped_clip_to_track(
780        &self,
781        track_name: &str,
782        kind: Kind,
783        audio_clip: Option<crate::message::AudioClipData>,
784        midi_clip: Option<crate::message::MidiClipData>,
785    ) {
786        if let Some(track) = self.state.lock().tracks.get(track_name) {
787            let mut track = track.lock();
788            if track.is_master() {
789                return;
790            }
791            match kind {
792                Kind::Audio => {
793                    if let Some(mut clip) = audio_clip.map(|clip| Self::audio_clip_from_data(&clip))
794                    {
795                        let max_lane = track.audio.ins.len().saturating_sub(1);
796                        clip.input_channel = clip.input_channel.min(max_lane);
797                        let clip_id = clip.id.clone();
798                        Self::push_audio_clip_with_cross_section_fades(&mut track, clip);
799                        #[cfg(unix)]
800                        track.rt.clip_pitch_shifters.clear();
801                        track
802                            .rt
803                            .session_clip_pool_audio
804                            .retain(|clip| clip.id != clip_id);
805                        self.take_clip_from_unused(&clip_id);
806                    }
807                }
808                Kind::MIDI => {
809                    if let Some(mut clip) = midi_clip.map(|clip| Self::midi_clip_from_data(&clip)) {
810                        let max_lane = track.midi.ins.len().saturating_sub(1);
811                        clip.input_channel = clip.input_channel.min(max_lane);
812                        let clip_id = clip.id.clone();
813                        track.midi.push_clip(clip);
814                        track
815                            .rt
816                            .session_clip_pool_midi
817                            .retain(|clip| clip.id != clip_id);
818                        self.take_clip_from_unused(&clip_id);
819                    }
820                }
821            }
822        }
823    }
824
825    pub(crate) fn remove_clips_from_track(
826        &self,
827        track_name: &str,
828        kind: Kind,
829        clip_indices: &[usize],
830    ) {
831        if let Some(track) = self.state.lock().tracks.get(track_name) {
832            let track = track.lock();
833            let mut indices = clip_indices.to_vec();
834            indices.sort_unstable();
835            indices.dedup();
836            match kind {
837                Kind::Audio => {
838                    for idx in indices.into_iter().rev() {
839                        if idx < track.audio.clips().len() {
840                            track.audio.remove_clip(idx);
841                        }
842                    }
843                    #[cfg(unix)]
844                    {
845                        let mut track = track;
846                        track.rt.clip_pitch_shifters.clear();
847                    }
848                }
849                Kind::MIDI => {
850                    for idx in indices.into_iter().rev() {
851                        if idx < track.midi.clips().len() {
852                            track.midi.remove_clip(idx);
853                        }
854                    }
855                }
856            }
857        }
858    }
859
860    pub(crate) fn move_clips_to_unused(
861        &self,
862        track_name: &str,
863        kind: Kind,
864        clip_indices: &[usize],
865    ) {
866        if let Some(track) = self.state.lock().tracks.get(track_name) {
867            let mut track = track.lock();
868            let mut indices = clip_indices.to_vec();
869            indices.sort_unstable();
870            indices.dedup();
871            match kind {
872                Kind::Audio => {
873                    let mut moved = Vec::new();
874                    for idx in indices.into_iter().rev() {
875                        if idx < track.audio.clips().len()
876                            && let Some(clip) = track.audio.remove_clip(idx)
877                        {
878                            if track
879                                .rt
880                                .session_slots
881                                .values()
882                                .any(|slot| slot.clip_id == clip.id)
883                            {
884                                track.rt.session_clip_pool_audio.push(clip.clone());
885                            }
886                            moved.push(audio_clip_to_data(&clip));
887                        }
888                    }
889                    moved.reverse();
890                    self.state.lock().unused_audio_clips.extend(moved);
891                    #[cfg(unix)]
892                    track.rt.clip_pitch_shifters.clear();
893                }
894                Kind::MIDI => {
895                    let mut moved = Vec::new();
896                    for idx in indices.into_iter().rev() {
897                        if idx < track.midi.clips().len()
898                            && let Some(clip) = track.midi.remove_clip(idx)
899                        {
900                            if track
901                                .rt
902                                .session_slots
903                                .values()
904                                .any(|slot| slot.clip_id == clip.id)
905                            {
906                                track.rt.session_clip_pool_midi.push(clip.clone());
907                            }
908                            moved.push(midi_clip_to_data(&clip));
909                        }
910                    }
911                    moved.reverse();
912                    self.state.lock().unused_midi_clips.extend(moved);
913                }
914            }
915        }
916    }
917
918    pub(crate) fn delete_unused_clips(&self, clip_ids: &[String]) {
919        let mut state = self.state.lock();
920        state
921            .unused_audio_clips
922            .retain(|clip| !clip_ids.contains(&clip.id));
923        state
924            .unused_midi_clips
925            .retain(|clip| !clip_ids.contains(&clip.id));
926        for track in state.tracks.values() {
927            let mut track = track.lock();
928            track
929                .rt
930                .session_clip_pool_audio
931                .retain(|clip| !clip_ids.contains(&clip.id));
932            track
933                .rt
934                .session_clip_pool_midi
935                .retain(|clip| !clip_ids.contains(&clip.id));
936        }
937    }
938
939    pub(crate) fn set_unused_clips(
940        &self,
941        audio: Vec<crate::message::AudioClipData>,
942        midi: Vec<crate::message::MidiClipData>,
943    ) {
944        let mut state = self.state.lock();
945        state.unused_audio_clips = audio;
946        state.unused_midi_clips = midi;
947        // Session restore assigns slots before the unused pool arrives; give
948        // each track a playback fallback for slot-referenced clips that are
949        // not on its timeline.
950        for track in state.tracks.values() {
951            let mut track = track.lock();
952            let slot_clip_ids: Vec<String> = track
953                .rt
954                .session_slots
955                .values()
956                .map(|slot| slot.clip_id.clone())
957                .collect();
958            for clip_id in slot_clip_ids {
959                let on_track = track.audio.clips().iter().any(|clip| clip.id == clip_id)
960                    || track.midi.clips().iter().any(|clip| clip.id == clip_id);
961                let in_pool = track
962                    .rt
963                    .session_clip_pool_audio
964                    .iter()
965                    .any(|clip| clip.id == clip_id)
966                    || track
967                        .rt
968                        .session_clip_pool_midi
969                        .iter()
970                        .any(|clip| clip.id == clip_id);
971                if on_track || in_pool {
972                    continue;
973                }
974                if let Some(data) = state
975                    .unused_audio_clips
976                    .iter()
977                    .find(|clip| clip.id == clip_id)
978                {
979                    track
980                        .rt
981                        .session_clip_pool_audio
982                        .push(Arc::new(Self::audio_clip_from_data(data)));
983                } else if let Some(data) = state
984                    .unused_midi_clips
985                    .iter()
986                    .find(|clip| clip.id == clip_id)
987                {
988                    track
989                        .rt
990                        .session_clip_pool_midi
991                        .push(Arc::new(Self::midi_clip_from_data(data)));
992                }
993            }
994            track.rt.prune_session_clip_pool();
995        }
996    }
997
998    pub(crate) fn take_clip_from_unused(&self, clip_id: &str) {
999        let mut state = self.state.lock();
1000        state.unused_audio_clips.retain(|clip| clip.id != clip_id);
1001        state.unused_midi_clips.retain(|clip| clip.id != clip_id);
1002    }
1003
1004    pub(crate) fn rename_clip_references(
1005        &self,
1006        track_name: &str,
1007        kind: Kind,
1008        clip_index: usize,
1009        new_name: &str,
1010    ) {
1011        let Some(track) = self.state.lock().tracks.get(track_name).cloned() else {
1012            return;
1013        };
1014        let track = track.lock();
1015        let old_name = match kind {
1016            Kind::Audio => {
1017                if clip_index >= track.audio.clips().len() {
1018                    return;
1019                }
1020                track.audio.clips().get(clip_index).unwrap().name.clone()
1021            }
1022            Kind::MIDI => {
1023                if clip_index >= track.midi.clips().len() {
1024                    return;
1025                }
1026                track.midi.clips().get(clip_index).unwrap().name.clone()
1027            }
1028        };
1029
1030        let new_file_name = match kind {
1031            Kind::Audio => format!("audio/{}.wav", new_name),
1032            Kind::MIDI => {
1033                let ext = std::path::Path::new(&old_name)
1034                    .extension()
1035                    .and_then(|e| e.to_str())
1036                    .map(|s| s.to_ascii_lowercase())
1037                    .filter(|e| e == "mid" || e == "midi")
1038                    .unwrap_or_else(|| "mid".to_string());
1039                format!("midi/{}.{}", new_name, ext)
1040            }
1041        };
1042        let _ = track;
1043
1044        for other_track in self.state.lock().tracks.values() {
1045            let other_track = other_track.lock();
1046            match kind {
1047                Kind::Audio => {
1048                    let clips = other_track.audio.clips();
1049                    for idx in 0..clips.len() {
1050                        other_track.audio.update_clip(idx, |clip| {
1051                            if clip.name == old_name {
1052                                clip.name = new_file_name.clone();
1053                            }
1054                            if clip.pitch_correction_source_name.as_deref()
1055                                == Some(old_name.as_str())
1056                            {
1057                                clip.pitch_correction_source_name = Some(new_file_name.clone());
1058                            }
1059                        });
1060                    }
1061                }
1062                Kind::MIDI => {
1063                    let clips = other_track.midi.clips();
1064                    for idx in 0..clips.len() {
1065                        other_track.midi.update_clip(idx, |clip| {
1066                            if clip.name == old_name {
1067                                clip.name = new_file_name.clone();
1068                            }
1069                        });
1070                    }
1071                }
1072            }
1073        }
1074    }
1075
1076    pub(crate) fn set_clip_identity(
1077        &self,
1078        track_name: &str,
1079        kind: Kind,
1080        clip_index: usize,
1081        new_id: &str,
1082        new_name: &str,
1083    ) {
1084        let Some(track) = self.state.lock().tracks.get(track_name).cloned() else {
1085            return;
1086        };
1087        let track = track.lock();
1088        match kind {
1089            Kind::Audio => {
1090                track.audio.update_clip(clip_index, |clip| {
1091                    clip.id = new_id.to_string();
1092                    clip.name = new_name.to_string();
1093                });
1094                #[cfg(unix)]
1095                {
1096                    let mut track = track;
1097                    track.rt.clip_pitch_shifters.clear();
1098                }
1099            }
1100            Kind::MIDI => {
1101                track.midi.update_clip(clip_index, |clip| {
1102                    clip.id = new_id.to_string();
1103                    clip.name = new_name.to_string();
1104                });
1105            }
1106        }
1107    }
1108
1109    pub(crate) fn set_clip_fade(
1110        &self,
1111        track_name: &str,
1112        clip_index: usize,
1113        kind: Kind,
1114        fade_enabled: bool,
1115        fade_in_samples: usize,
1116        fade_out_samples: usize,
1117    ) {
1118        let Some(track) = self.state.lock().tracks.get(track_name).cloned() else {
1119            return;
1120        };
1121        let track = track.lock();
1122        match kind {
1123            Kind::Audio => {
1124                track.audio.update_clip(clip_index, |clip| {
1125                    clip.fade_enabled = fade_enabled;
1126                    clip.fade_in_samples = fade_in_samples;
1127                    clip.fade_out_samples = fade_out_samples;
1128                });
1129            }
1130            Kind::MIDI => {}
1131        }
1132    }
1133
1134    pub(crate) fn set_clip_bounds(
1135        &self,
1136        track_name: &str,
1137        clip_index: usize,
1138        kind: Kind,
1139        start: usize,
1140        length: usize,
1141        offset: usize,
1142    ) {
1143        let Some(track) = self.state.lock().tracks.get(track_name).cloned() else {
1144            return;
1145        };
1146        let track = track.lock();
1147        match kind {
1148            Kind::Audio => {
1149                track.audio.update_clip(clip_index, |clip| {
1150                    clip.start = start;
1151                    clip.end = start.saturating_add(length.max(1));
1152                    clip.offset = offset;
1153                    clip.pitch_correction_preview_name = None;
1154                    clip.pitch_correction_source_name = None;
1155                    clip.pitch_correction_source_offset = None;
1156                    clip.pitch_correction_source_length = None;
1157                    clip.pitch_correction_points.clear();
1158                    clip.pitch_correction_frame_likeness = None;
1159                    clip.pitch_correction_inertia_ms = None;
1160                    clip.pitch_correction_formant_compensation = None;
1161                });
1162                #[cfg(unix)]
1163                {
1164                    let mut track = track;
1165                    track.rt.clip_pitch_shifters.clear();
1166                }
1167            }
1168            Kind::MIDI => {
1169                track.midi.update_clip(clip_index, |clip| {
1170                    clip.start = start;
1171                    clip.end = start.saturating_add(length.max(1));
1172                    clip.offset = offset;
1173                });
1174            }
1175        }
1176    }
1177
1178    pub(crate) fn set_clip_source_name(
1179        &self,
1180        track_name: &str,
1181        clip_index: usize,
1182        kind: Kind,
1183        name: String,
1184    ) {
1185        let Some(track) = self.state.lock().tracks.get(track_name).cloned() else {
1186            return;
1187        };
1188        let track = track.lock();
1189        match kind {
1190            Kind::Audio => {
1191                track.audio.update_clip(clip_index, |clip| {
1192                    clip.name = name;
1193                });
1194                #[cfg(unix)]
1195                {
1196                    let mut track = track;
1197                    track.rt.clip_pitch_shifters.clear();
1198                }
1199            }
1200            Kind::MIDI => {
1201                track.midi.update_clip(clip_index, |clip| {
1202                    clip.name = name;
1203                });
1204            }
1205        }
1206    }
1207
1208    pub(crate) fn set_clip_muted(
1209        &self,
1210        track_name: &str,
1211        clip_index: usize,
1212        kind: Kind,
1213        muted: bool,
1214    ) {
1215        let Some(track) = self.state.lock().tracks.get(track_name).cloned() else {
1216            return;
1217        };
1218        let track = track.lock();
1219        match kind {
1220            Kind::Audio => {
1221                track.audio.update_clip(clip_index, |clip| {
1222                    clip.muted = muted;
1223                });
1224            }
1225            Kind::MIDI => {
1226                track.midi.update_clip(clip_index, |clip| {
1227                    clip.muted = muted;
1228                });
1229            }
1230        }
1231    }
1232
1233    #[allow(clippy::too_many_arguments)]
1234    pub(crate) fn set_clip_pitch_correction(
1235        &self,
1236        track_name: &str,
1237        clip_index: usize,
1238        preview_name: Option<String>,
1239        source_name: Option<String>,
1240        source_offset: Option<usize>,
1241        source_length: Option<usize>,
1242        pitch_correction_points: Vec<crate::message::PitchCorrectionPointData>,
1243        pitch_correction_frame_likeness: Option<f32>,
1244        pitch_correction_inertia_ms: Option<u16>,
1245        pitch_correction_formant_compensation: Option<bool>,
1246    ) {
1247        if let Some(track) = self.state.lock().tracks.get(track_name) {
1248            let track = track.lock();
1249            track.audio.update_clip(clip_index, |clip| {
1250                clip.pitch_correction_preview_name = preview_name;
1251                clip.pitch_correction_source_name = source_name;
1252                clip.pitch_correction_source_offset = source_offset;
1253                clip.pitch_correction_source_length = source_length;
1254                clip.pitch_correction_points = pitch_correction_points;
1255                clip.pitch_correction_frame_likeness = pitch_correction_frame_likeness;
1256                clip.pitch_correction_inertia_ms = pitch_correction_inertia_ms;
1257                clip.pitch_correction_formant_compensation = pitch_correction_formant_compensation;
1258            });
1259            #[cfg(unix)]
1260            {
1261                let mut track = track;
1262                track.rt.clip_pitch_shifters.clear();
1263            }
1264        }
1265    }
1266
1267    pub fn check_if_leads_to_kind(
1268        &self,
1269        kind: Kind,
1270        current_track_name: &str,
1271        target_track_name: &str,
1272    ) -> bool {
1273        routing::would_create_cycle(
1274            &target_track_name.to_string(),
1275            &current_track_name.to_string(),
1276            |track_name| self.connected_neighbors(kind, track_name),
1277        )
1278    }
1279
1280    pub(crate) fn connected_neighbors(&self, kind: Kind, current_track_name: &str) -> Vec<String> {
1281        let state = self.state.lock();
1282        let mut found_neighbors = Vec::new();
1283
1284        if let Some(current_track_handle) = state.tracks.get(current_track_name) {
1285            let current_track = current_track_handle.lock();
1286
1287            match kind {
1288                Kind::Audio => {
1289                    for out_port in &current_track.audio.outs {
1290                        let conns = out_port.connections();
1291                        for conn in conns.iter() {
1292                            for (name, next_track_handle) in &state.tracks {
1293                                let next_track = next_track_handle.lock();
1294                                let is_connected = next_track
1295                                    .audio
1296                                    .ins
1297                                    .iter()
1298                                    .any(|ins_port| Arc::ptr_eq(ins_port, conn));
1299
1300                                if is_connected {
1301                                    found_neighbors.push(name.clone());
1302                                }
1303                            }
1304                        }
1305                    }
1306                }
1307                Kind::MIDI => {
1308                    for out_port in &current_track.midi.outs {
1309                        let conns = out_port.connections();
1310                        for conn in conns.iter() {
1311                            for (name, next_track_handle) in &state.tracks {
1312                                let next_track = next_track_handle.lock();
1313                                let is_connected = next_track
1314                                    .midi
1315                                    .ins
1316                                    .iter()
1317                                    .any(|ins_port| Arc::ptr_eq(ins_port, conn));
1318
1319                                if is_connected {
1320                                    found_neighbors.push(name.clone());
1321                                }
1322                            }
1323                        }
1324                    }
1325                }
1326            }
1327        }
1328        found_neighbors
1329    }
1330
1331    pub(crate) fn find_audio_io_owner(
1332        &self,
1333        state: &crate::state::State,
1334        io: &std::sync::Arc<crate::audio::io::AudioIO>,
1335    ) -> Option<(String, usize)> {
1336        for (name, track) in &state.tracks {
1337            let t = track.lock();
1338            for (i, out) in t.audio.outs.iter().enumerate() {
1339                if std::sync::Arc::ptr_eq(out, io) {
1340                    return Some((name.clone(), i));
1341                }
1342            }
1343            for (i, inp) in t.audio.ins.iter().enumerate() {
1344                if std::sync::Arc::ptr_eq(inp, io) {
1345                    return Some((name.clone(), i));
1346                }
1347            }
1348        }
1349        None
1350    }
1351
1352    pub(crate) fn find_midi_io_owner(
1353        &self,
1354        state: &crate::state::State,
1355        io: &std::sync::Arc<crate::midi::io::MIDIIO>,
1356    ) -> Option<(String, usize, bool)> {
1357        for (name, track) in &state.tracks {
1358            let t = track.lock();
1359            for (i, out) in t.midi.outs.iter().enumerate() {
1360                if std::sync::Arc::ptr_eq(out, io) {
1361                    return Some((name.clone(), i, false));
1362                }
1363            }
1364            for (i, inp) in t.midi.ins.iter().enumerate() {
1365                if std::sync::Arc::ptr_eq(inp, io) {
1366                    return Some((name.clone(), i, true));
1367                }
1368            }
1369        }
1370        None
1371    }
1372
1373    pub(crate) fn collect_descendant_track_names(&self, name: &str, out: &mut Vec<String>) {
1374        // Clone the child arcs while briefly holding the parent lock, then release it before
1375        // recursing so we never nest locks on the same thread.
1376        let child_arcs: Vec<crate::state::TrackHandle> = {
1377            let state = self.state.lock();
1378            if let Some(track) = state.tracks.get(name) {
1379                track.lock().child_tracks.clone()
1380            } else {
1381                Vec::new()
1382            }
1383        };
1384        for child in child_arcs {
1385            let child_name = { child.lock().name.clone() };
1386            self.collect_descendant_track_names(&child_name, out);
1387            out.push(child_name);
1388        }
1389    }
1390
1391    pub(crate) async fn remove_single_track(&mut self, name: &str) {
1392        let children: Vec<crate::state::TrackHandle> = {
1393            let state = self.state.lock();
1394            if let Some(removed) = state.tracks.get(name).cloned() {
1395                removed.lock().child_tracks.clone()
1396            } else {
1397                Vec::new()
1398            }
1399        };
1400        let parent_name: Option<String> = {
1401            let state = self.state.lock();
1402            state
1403                .tracks
1404                .get(name)
1405                .map(|t| t.lock().parent_track.clone())
1406                .unwrap_or(None)
1407        };
1408        if let Some(parent_name) = parent_name {
1409            let state = self.state.lock();
1410            if let Some(parent) = state.tracks.get(&parent_name).cloned() {
1411                let mut parent = parent.lock();
1412                parent.child_tracks.retain(|c| c.lock().name != *name);
1413            }
1414        }
1415        if let Some(removed_track) = self.state.lock().tracks.get(name).cloned() {
1416            for child in children {
1417                let removed = removed_track.lock();
1418                child.lock().disconnect_from_parent(&removed);
1419                child.lock().parent_track = None;
1420            }
1421        }
1422        self.state.lock().tracks.remove(name);
1423        self.audio_recordings.remove(name);
1424        self.midi_recordings.remove(name);
1425        self.midi_hw_in_routes.retain(|r| r.to_track != *name);
1426        self.midi_hw_out_routes.retain(|r| r.from_track != *name);
1427        if self
1428            .pending_midi_learn
1429            .as_ref()
1430            .is_some_and(|(track_name, _, _)| track_name == name)
1431        {
1432            self.pending_midi_learn = None;
1433        }
1434    }
1435
1436    pub(crate) fn prepare_inverse_actions(
1437        &self,
1438        action_to_process: &Action,
1439        record_history: bool,
1440        suppress_timing_history: bool,
1441    ) -> Option<Vec<Action>> {
1442        let mut extra_inverse_actions: Vec<Action> = Vec::new();
1443        if record_history
1444            && !self.history_suspended
1445            && let Action::RemoveTrack(track_name) = action_to_process
1446        {
1447            for route in self
1448                .midi_hw_in_routes
1449                .iter()
1450                .filter(|route| &route.to_track == track_name)
1451            {
1452                extra_inverse_actions.push(Action::Connect {
1453                    from_track: format!("midi:hw:in:{}", route.device),
1454                    from_port: 0,
1455                    to_track: route.to_track.clone(),
1456                    to_port: route.to_port,
1457                    kind: Kind::MIDI,
1458                });
1459            }
1460            for route in self
1461                .midi_hw_out_routes
1462                .iter()
1463                .filter(|route| &route.from_track == track_name)
1464            {
1465                extra_inverse_actions.push(Action::Connect {
1466                    from_track: route.from_track.clone(),
1467                    from_port: route.from_port,
1468                    to_track: format!("midi:hw:out:{}", route.device),
1469                    to_port: 0,
1470                    kind: Kind::MIDI,
1471                });
1472            }
1473        }
1474        if record_history
1475            && !self.history_suspended
1476            && matches!(action_to_process, Action::ClearAllMidiLearnBindings)
1477        {
1478            if let Some(binding) = self.global_midi_learn_play_pause.clone() {
1479                extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
1480                    target: crate::message::GlobalMidiLearnTarget::PlayPause,
1481                    binding: Some(binding),
1482                });
1483            }
1484            if let Some(binding) = self.global_midi_learn_stop.clone() {
1485                extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
1486                    target: crate::message::GlobalMidiLearnTarget::Stop,
1487                    binding: Some(binding),
1488                });
1489            }
1490            if let Some(binding) = self.global_midi_learn_record_toggle.clone() {
1491                extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
1492                    target: crate::message::GlobalMidiLearnTarget::RecordToggle,
1493                    binding: Some(binding),
1494                });
1495            }
1496            for (key, binding) in self.session_midi_learn_slots.clone() {
1497                extra_inverse_actions.push(Action::SetSessionMidiLearnBinding {
1498                    target: crate::message::SessionMidiLearnTarget::Slot {
1499                        track_name: key.0,
1500                        scene_index: key.1,
1501                    },
1502                    binding: Some(binding),
1503                });
1504            }
1505            for (scene_index, binding) in self.session_midi_learn_scenes.clone() {
1506                extra_inverse_actions.push(Action::SetSessionMidiLearnBinding {
1507                    target: crate::message::SessionMidiLearnTarget::Scene(scene_index),
1508                    binding: Some(binding),
1509                });
1510            }
1511            for (track_name, binding) in self.session_midi_learn_stop_track.clone() {
1512                extra_inverse_actions.push(Action::SetSessionMidiLearnBinding {
1513                    target: crate::message::SessionMidiLearnTarget::StopTrack(track_name),
1514                    binding: Some(binding),
1515                });
1516            }
1517            if let Some(binding) = self.session_midi_learn_stop_all.clone() {
1518                extra_inverse_actions.push(Action::SetSessionMidiLearnBinding {
1519                    target: crate::message::SessionMidiLearnTarget::StopAll,
1520                    binding: Some(binding),
1521                });
1522            }
1523        }
1524        let mut inverse_actions = if record_history
1525            && !suppress_timing_history
1526            && should_record(action_to_process)
1527            && !self.history_suspended
1528        {
1529            create_inverse_actions(action_to_process, self.state.as_ref()).map(|mut actions| {
1530                actions.extend(extra_inverse_actions);
1531                actions
1532            })
1533        } else {
1534            None
1535        };
1536        if record_history && !suppress_timing_history && !self.history_suspended {
1537            match action_to_process {
1538                Action::SetTempo(_) => {
1539                    inverse_actions = Some(vec![Action::SetTempo(self.tempo_bpm)]);
1540                }
1541                Action::SetLoopEnabled(_) => {
1542                    inverse_actions = Some(vec![Action::SetLoopEnabled(self.loop_enabled)]);
1543                }
1544                Action::SetLoopRange(_) => {
1545                    inverse_actions = Some(vec![
1546                        Action::SetLoopRange(self.loop_range_samples),
1547                        Action::SetLoopEnabled(self.loop_enabled),
1548                    ]);
1549                }
1550                Action::SetPunchEnabled(_) => {
1551                    inverse_actions = Some(vec![Action::SetPunchEnabled(self.punch_enabled)]);
1552                }
1553                Action::SetPunchRange(_) => {
1554                    inverse_actions = Some(vec![
1555                        Action::SetPunchRange(self.punch_range_samples),
1556                        Action::SetPunchEnabled(self.punch_enabled),
1557                    ]);
1558                }
1559                Action::SetMetronomeEnabled(_) => {
1560                    inverse_actions =
1561                        Some(vec![Action::SetMetronomeEnabled(self.metronome_enabled)]);
1562                }
1563                Action::SetTimeSignature { .. } => {
1564                    inverse_actions = Some(vec![Action::SetTimeSignature {
1565                        numerator: self.tsig_num,
1566                        denominator: self.tsig_denom,
1567                    }]);
1568                }
1569                Action::SetTempoMap { .. } => {
1570                    inverse_actions = Some(vec![Action::SetTempoMap {
1571                        tempo_points: self.tempo_points.clone(),
1572                        time_signature_points: self.time_signature_points.clone(),
1573                    }]);
1574                }
1575                Action::SetClipPlaybackEnabled(_) => {
1576                    inverse_actions = Some(vec![Action::SetClipPlaybackEnabled(
1577                        self.clip_playback_enabled,
1578                    )]);
1579                }
1580                Action::SetRecordEnabled(_) => {
1581                    inverse_actions = Some(vec![Action::SetRecordEnabled(self.record_enabled)]);
1582                }
1583                Action::SetGlobalMidiLearnBinding { target, .. } => {
1584                    let binding = match target {
1585                        crate::message::GlobalMidiLearnTarget::PlayPause => {
1586                            self.global_midi_learn_play_pause.clone()
1587                        }
1588                        crate::message::GlobalMidiLearnTarget::Stop => {
1589                            self.global_midi_learn_stop.clone()
1590                        }
1591                        crate::message::GlobalMidiLearnTarget::RecordToggle => {
1592                            self.global_midi_learn_record_toggle.clone()
1593                        }
1594                    };
1595                    inverse_actions = Some(vec![Action::SetGlobalMidiLearnBinding {
1596                        target: *target,
1597                        binding,
1598                    }]);
1599                }
1600                Action::SetModulators(_) => {
1601                    inverse_actions = Some(vec![Action::SetModulators(self.modulators.clone())]);
1602                }
1603                Action::SetSessionMidiLearnBinding { target, .. } => {
1604                    let binding = match target {
1605                        crate::message::SessionMidiLearnTarget::Slot {
1606                            track_name,
1607                            scene_index,
1608                        } => self
1609                            .session_midi_learn_slots
1610                            .get(&(track_name.clone(), *scene_index))
1611                            .cloned(),
1612                        crate::message::SessionMidiLearnTarget::Scene(scene_index) => {
1613                            self.session_midi_learn_scenes.get(scene_index).cloned()
1614                        }
1615                        crate::message::SessionMidiLearnTarget::StopTrack(track_name) => {
1616                            self.session_midi_learn_stop_track.get(track_name).cloned()
1617                        }
1618                        crate::message::SessionMidiLearnTarget::StopAll => {
1619                            self.session_midi_learn_stop_all.clone()
1620                        }
1621                    };
1622                    inverse_actions = Some(vec![Action::SetSessionMidiLearnBinding {
1623                        target: target.clone(),
1624                        binding,
1625                    }]);
1626                }
1627                _ => {}
1628            }
1629        }
1630        inverse_actions
1631    }
1632
1633    pub(crate) async fn handle_add_track(&mut self, action: Action) {
1634        let Action::AddTrack {
1635            name,
1636            audio_ins,
1637            midi_ins,
1638            audio_outs,
1639            midi_outs,
1640            folder,
1641            mixosc_addr,
1642        } = action
1643        else {
1644            self.notify_clients(Err("Expected AddTrack action".to_string()))
1645                .await;
1646            return;
1647        };
1648        let tracks = &mut self.state.lock().tracks;
1649        if tracks.contains_key(&name) {
1650            self.notify_clients(Err(format!("Track {} already exists", name)))
1651                .await;
1652            return;
1653        }
1654        if !folder
1655            && mixosc_addr.is_none()
1656            && !self.history_suspended
1657            && audio_ins + midi_ins + audio_outs + midi_outs == 0
1658        {
1659            self.notify_clients(Err(
1660                "Track must have at one audio/MIDI I/O or a MixOSC mixer".to_string(),
1661            ))
1662            .await;
1663            return;
1664        }
1665        let maybe_hw = self
1666            .hw_driver_info
1667            .map(|info| (info.cycle_samples, info.sample_rate as f64));
1668
1669        if let Some((chsamples, sample_rate)) = maybe_hw {
1670            let track = if folder {
1671                Track::new_folder(
1672                    name.clone(),
1673                    audio_ins,
1674                    audio_outs,
1675                    midi_ins,
1676                    midi_outs,
1677                    chsamples,
1678                    sample_rate,
1679                )
1680            } else {
1681                Track::new(
1682                    name.clone(),
1683                    audio_ins,
1684                    audio_outs,
1685                    midi_ins,
1686                    midi_outs,
1687                    chsamples,
1688                    sample_rate,
1689                )
1690            };
1691            tracks.insert(name.clone(), Arc::new(track));
1692            if let Some(track) = tracks.get(&name) {
1693                let mut t = track.lock();
1694                t.set_clip_playback_enabled(self.clip_playback_enabled);
1695                t.set_transport_timing(self.tempo_bpm, self.tsig_num, self.tsig_denom);
1696                t.set_session_base_dir(self.session_dir.clone());
1697                t.mixosc_addr = mixosc_addr;
1698            }
1699        } else {
1700            self.notify_clients(Err(
1701                "Engine needs to open audio device before adding audio track".to_string(),
1702            ))
1703            .await;
1704        }
1705    }
1706
1707    pub(crate) async fn handle_remove_track(&mut self, name: String, record_history: bool) {
1708        let mut descendant_names = Vec::new();
1709        self.collect_descendant_track_names(&name, &mut descendant_names);
1710        let names_to_remove: Vec<String> = descendant_names
1711            .iter()
1712            .cloned()
1713            .chain(std::iter::once(name.clone()))
1714            .collect();
1715
1716        let combined_inverse = if record_history && !self.history_suspended {
1717            let mut inv = Vec::new();
1718            for n in &names_to_remove {
1719                if let Some(mut actions) =
1720                    create_inverse_actions(&Action::RemoveTrack(n.clone()), self.state.as_ref())
1721                {
1722                    inv.append(&mut actions);
1723                }
1724                for route in self.midi_hw_in_routes.iter().filter(|r| &r.to_track == n) {
1725                    inv.push(Action::Connect {
1726                        from_track: format!("midi:hw:in:{}", route.device),
1727                        from_port: 0,
1728                        to_track: route.to_track.clone(),
1729                        to_port: route.to_port,
1730                        kind: Kind::MIDI,
1731                    });
1732                }
1733                for route in self
1734                    .midi_hw_out_routes
1735                    .iter()
1736                    .filter(|r| &r.from_track == n)
1737                {
1738                    inv.push(Action::Connect {
1739                        from_track: route.from_track.clone(),
1740                        from_port: route.from_port,
1741                        to_track: format!("midi:hw:out:{}", route.device),
1742                        to_port: 0,
1743                        kind: Kind::MIDI,
1744                    });
1745                }
1746            }
1747
1748            // Reorder so all AddTrack actions come first, then everything else, then
1749            // explicit Connect actions. This mirrors EndHistoryGroup and guarantees that
1750            // tracks are recreated before they are re-parented or reconnected.
1751            let mut add_tracks = Vec::new();
1752            let mut connections = Vec::new();
1753            let mut rest = Vec::new();
1754            for action in inv {
1755                match action {
1756                    Action::AddTrack { .. } => add_tracks.push(action),
1757                    Action::Connect { .. } => connections.push(action),
1758                    _ => rest.push(action),
1759                }
1760            }
1761            let mut ordered = add_tracks;
1762            ordered.extend(rest);
1763            ordered.extend(connections);
1764            ordered
1765        } else {
1766            Vec::new()
1767        };
1768
1769        for n in &descendant_names {
1770            self.remove_single_track(n).await;
1771            self.notify_clients(Ok(Action::RemoveTrack(n.clone())))
1772                .await;
1773        }
1774        self.remove_single_track(&name).await;
1775
1776        if record_history && !self.history_suspended && !combined_inverse.is_empty() {
1777            self.history.record(UndoEntry {
1778                forward_actions: vec![Action::RemoveTrack(name.clone())],
1779                inverse_actions: combined_inverse,
1780            });
1781        }
1782    }
1783
1784    pub(crate) async fn handle_connect(
1785        &mut self,
1786        from_track: &str,
1787        from_port: usize,
1788        to_track: &str,
1789        to_port: usize,
1790        kind: Kind,
1791    ) {
1792        match kind {
1793            Kind::Audio => {
1794                if from_track == to_track && from_track != "hw:in" && to_track != "hw:out" {
1795                    let Some(track) = self.state.lock().tracks.get(from_track).cloned() else {
1796                        self.notify_clients(Err(format!("Track '{from_track}' not found")))
1797                            .await;
1798                        return;
1799                    };
1800                    let result = {
1801                        let mut track = track.lock();
1802                        track.connect_audio_connectable(
1803                            crate::connectable::ConnectableRef::TrackInput,
1804                            to_port,
1805                            crate::connectable::ConnectableRef::TrackOutput,
1806                            from_port,
1807                        )
1808                    };
1809                    if let Err(e) = result {
1810                        self.notify_clients(Err(e)).await;
1811                        return;
1812                    }
1813                    self.notify_clients(Ok(Action::TrackConnectAudio {
1814                        track_name: from_track.to_string(),
1815                        from: crate::connectable::ConnectableRef::TrackInput,
1816                        from_port: to_port,
1817                        to: crate::connectable::ConnectableRef::TrackOutput,
1818                        to_port: from_port,
1819                    }))
1820                    .await;
1821                    return;
1822                }
1823                let (from_audio_io, to_audio_io) =
1824                    self.resolve_audio_route_ports(from_track, from_port, to_track, to_port);
1825                match (from_audio_io, to_audio_io) {
1826                    (Some(source), Some(target)) => {
1827                        if from_track != "hw:in"
1828                            && to_track != "hw:out"
1829                            && self.check_if_leads_to_kind(Kind::Audio, to_track, from_track)
1830                        {
1831                            self.notify_clients(Err("Circular routing is not allowed!".into()))
1832                                .await;
1833                            return;
1834                        }
1835                        crate::audio::io::AudioIO::connect(&source, &target);
1836                    }
1837                    (None, _) => {
1838                        self.notify_clients(Err(format!(
1839                            "Source track '{}' not found",
1840                            from_track
1841                        )))
1842                        .await;
1843                    }
1844                    (_, None) => {
1845                        self.notify_clients(Err(format!(
1846                            "Destination track '{}' not found",
1847                            to_track
1848                        )))
1849                        .await;
1850                    }
1851                }
1852            }
1853            Kind::MIDI => {
1854                let from_hw_in_device = Self::midi_hw_in_device(from_track);
1855                let to_hw_out_device = Self::midi_hw_out_device(to_track);
1856                let from_is_invalid_hw = Self::midi_hw_out_device(from_track).is_some();
1857                let to_is_invalid_hw = Self::midi_hw_in_device(to_track).is_some();
1858
1859                if from_is_invalid_hw || to_is_invalid_hw {
1860                    self.notify_clients(Err(
1861                        "Invalid MIDI hardware connection direction".to_string()
1862                    ))
1863                    .await;
1864                    return;
1865                }
1866
1867                if from_hw_in_device.is_none()
1868                    && to_hw_out_device.is_none()
1869                    && from_track != to_track
1870                    && self.check_if_leads_to_kind(Kind::MIDI, to_track, from_track)
1871                {
1872                    self.notify_clients(Err("Circular routing is not allowed!".into()))
1873                        .await;
1874                    return;
1875                }
1876
1877                if from_track == to_track {
1878                    let Some(track) = self.state.lock().tracks.get(from_track).cloned() else {
1879                        self.notify_clients(Err(format!("Track '{from_track}' not found")))
1880                            .await;
1881                        return;
1882                    };
1883                    let result = {
1884                        let mut track = track.lock();
1885                        track.connect_midi_connectable(
1886                            crate::connectable::ConnectableRef::TrackInput,
1887                            to_port,
1888                            crate::connectable::ConnectableRef::TrackOutput,
1889                            from_port,
1890                        )
1891                    };
1892                    if let Err(e) = result {
1893                        self.notify_clients(Err(e)).await;
1894                        return;
1895                    }
1896                    self.notify_clients(Ok(Action::TrackConnectMidi {
1897                        track_name: from_track.to_string(),
1898                        from: crate::connectable::ConnectableRef::TrackInput,
1899                        from_port: to_port,
1900                        to: crate::connectable::ConnectableRef::TrackOutput,
1901                        to_port: from_port,
1902                    }))
1903                    .await;
1904                    return;
1905                }
1906
1907                let state = self.state.lock();
1908                let from_track_handle = state.tracks.get(from_track);
1909                let to_track_handle = state.tracks.get(to_track);
1910
1911                if let (Some(from_device), Some(to_device)) = (from_hw_in_device, to_hw_out_device)
1912                {
1913                    let route = MidiHwThruRoute {
1914                        from_device: from_device.to_string(),
1915                        to_device: to_device.to_string(),
1916                    };
1917                    if !self.midi_hw_thru_routes.iter().any(|r| r == &route) {
1918                        self.midi_hw_thru_routes.push(route);
1919                    }
1920                } else if let Some(device) = from_hw_in_device {
1921                    if let Some(t_t) = to_track_handle {
1922                        if t_t.lock().midi.ins.get(to_port).is_none() {
1923                            self.notify_clients(Err(format!(
1924                                "MIDI input port {} not found on track '{}'",
1925                                to_port, to_track
1926                            )))
1927                            .await;
1928                            return;
1929                        }
1930                        let route = MidiHwInRoute {
1931                            device: device.to_string(),
1932                            to_track: to_track.to_string(),
1933                            to_port,
1934                        };
1935                        if !self.midi_hw_in_routes.iter().any(|r| r == &route) {
1936                            self.midi_hw_in_routes.push(route);
1937                        }
1938                    } else {
1939                        self.notify_clients(Err(format!(
1940                            "MIDI destination track not found: {}",
1941                            to_track
1942                        )))
1943                        .await;
1944                    }
1945                } else if let Some(device) = to_hw_out_device {
1946                    if let Some(f_t) = from_track_handle {
1947                        if f_t.lock().midi.outs.get(from_port).is_none() {
1948                            self.notify_clients(Err(format!(
1949                                "MIDI output port {} not found on track '{}'",
1950                                from_port, from_track
1951                            )))
1952                            .await;
1953                            return;
1954                        }
1955                        let route = MidiHwOutRoute {
1956                            from_track: from_track.to_string(),
1957                            from_port,
1958                            device: device.to_string(),
1959                        };
1960                        if !self.midi_hw_out_routes.iter().any(|r| r == &route) {
1961                            self.midi_hw_out_routes.push(route);
1962                        }
1963                    } else {
1964                        self.notify_clients(Err(format!(
1965                            "MIDI source track not found: {}",
1966                            from_track
1967                        )))
1968                        .await;
1969                    }
1970                } else {
1971                    match (from_track_handle, to_track_handle) {
1972                        (Some(f_t), Some(t_t)) => {
1973                            let to_in_res = t_t.lock().midi.ins.get(to_port).cloned();
1974                            if let Some(to_in) = to_in_res {
1975                                let mut from_track = f_t.lock();
1976                                if let Err(e) = from_track.midi.connect_out(from_port, to_in) {
1977                                    self.notify_clients(Err(e)).await;
1978                                    return;
1979                                }
1980                                from_track.invalidate_midi_route_cache();
1981                            } else {
1982                                self.notify_clients(Err(format!(
1983                                    "MIDI input port {} not found on track '{}'",
1984                                    to_port, to_track
1985                                )))
1986                                .await;
1987                            }
1988                        }
1989                        _ => {
1990                            self.notify_clients(Err(format!(
1991                                "MIDI tracks not found: {} or {}",
1992                                from_track, to_track
1993                            )))
1994                            .await;
1995                        }
1996                    }
1997                }
1998            }
1999        };
2000    }
2001
2002    pub(crate) async fn handle_disconnect(&mut self, action: Action) {
2003        let Action::Disconnect {
2004            ref from_track,
2005            from_port,
2006            ref to_track,
2007            to_port,
2008            kind,
2009        } = action
2010        else {
2011            return;
2012        };
2013
2014        if from_track == to_track && from_track != "hw:in" && to_track != "hw:out" {
2015            let Some(track) = self.state.lock().tracks.get(from_track).cloned() else {
2016                self.notify_clients(Err(format!("Track '{from_track}' not found")))
2017                    .await;
2018                return;
2019            };
2020            match kind {
2021                Kind::Audio => {
2022                    let result = {
2023                        let mut track = track.lock();
2024                        track.disconnect_audio_connectable(
2025                            crate::connectable::ConnectableRef::TrackInput,
2026                            to_port,
2027                            crate::connectable::ConnectableRef::TrackOutput,
2028                            from_port,
2029                        )
2030                    };
2031                    if let Err(e) = result {
2032                        self.notify_clients(Err(e)).await;
2033                        return;
2034                    }
2035                    self.notify_clients(Ok(Action::TrackDisconnectAudio {
2036                        track_name: from_track.clone(),
2037                        from: crate::connectable::ConnectableRef::TrackInput,
2038                        from_port: to_port,
2039                        to: crate::connectable::ConnectableRef::TrackOutput,
2040                        to_port: from_port,
2041                    }))
2042                    .await;
2043                }
2044                Kind::MIDI => {
2045                    let result = {
2046                        let mut track = track.lock();
2047                        track.disconnect_midi_connectable(
2048                            crate::connectable::ConnectableRef::TrackInput,
2049                            to_port,
2050                            crate::connectable::ConnectableRef::TrackOutput,
2051                            from_port,
2052                        )
2053                    };
2054                    if let Err(e) = result {
2055                        self.notify_clients(Err(e)).await;
2056                        return;
2057                    }
2058                    self.notify_clients(Ok(Action::TrackDisconnectMidi {
2059                        track_name: from_track.clone(),
2060                        from: crate::connectable::ConnectableRef::TrackInput,
2061                        from_port: to_port,
2062                        to: crate::connectable::ConnectableRef::TrackOutput,
2063                        to_port: from_port,
2064                    }))
2065                    .await;
2066                }
2067            }
2068            return;
2069        }
2070
2071        if kind == Kind::Audio {
2072            if let Err(e) = self.disconnect_audio_route_and_notify(action.clone()).await {
2073                self.notify_clients(Err(e)).await;
2074            }
2075        } else if kind == Kind::MIDI {
2076            let from_hw_in_device = Self::midi_hw_in_device(from_track);
2077            let to_hw_out_device = Self::midi_hw_out_device(to_track);
2078
2079            if let (Some(from_device), Some(to_device)) = (from_hw_in_device, to_hw_out_device) {
2080                let before = self.midi_hw_thru_routes.len();
2081                self.midi_hw_thru_routes
2082                    .retain(|r| !(r.from_device == from_device && r.to_device == to_device));
2083                if self.midi_hw_thru_routes.len() < before {
2084                    self.notify_clients(Ok(action.clone())).await;
2085                } else {
2086                    self.notify_clients(Err(format!(
2087                        "Disconnect failed: MIDI route not found ({} -> {})",
2088                        from_track, to_track
2089                    )))
2090                    .await;
2091                }
2092                return;
2093            }
2094
2095            if let Some(device) = from_hw_in_device {
2096                let before = self.midi_hw_in_routes.len();
2097                self.midi_hw_in_routes.retain(|r| {
2098                    !(r.device == device && r.to_track == *to_track && r.to_port == to_port)
2099                });
2100                if self.midi_hw_in_routes.len() < before {
2101                    self.notify_clients(Ok(action.clone())).await;
2102                } else {
2103                    self.notify_clients(Err(format!(
2104                        "Disconnect failed: MIDI route not found ({} -> {})",
2105                        from_track, to_track
2106                    )))
2107                    .await;
2108                }
2109                return;
2110            }
2111
2112            if let Some(device) = to_hw_out_device {
2113                let before = self.midi_hw_out_routes.len();
2114                self.midi_hw_out_routes.retain(|r| {
2115                    !(r.from_track == *from_track && r.from_port == from_port && r.device == device)
2116                });
2117                if self.midi_hw_out_routes.len() < before {
2118                    self.notify_clients(Ok(action.clone())).await;
2119                } else {
2120                    self.notify_clients(Err(format!(
2121                        "Disconnect failed: MIDI route not found ({} -> {})",
2122                        from_track, to_track
2123                    )))
2124                    .await;
2125                }
2126                return;
2127            }
2128
2129            let state = self.state.lock();
2130            if let (Some(f_t), Some(t_t)) =
2131                (state.tracks.get(from_track), state.tracks.get(to_track))
2132                && let Some(to_in) = t_t.lock().midi.ins.get(to_port).cloned()
2133            {
2134                let mut from_track = f_t.lock();
2135                if let Err(e) = from_track.midi.disconnect_out(from_port, &to_in) {
2136                    self.notify_clients(Err(e)).await;
2137                } else {
2138                    from_track.invalidate_midi_route_cache();
2139                    self.notify_clients(Ok(action.clone())).await;
2140                }
2141            } else {
2142                self.notify_clients(Err(format!(
2143                    "Disconnect failed: MIDI ports not found ({} -> {})",
2144                    from_track, to_track
2145                )))
2146                .await;
2147            }
2148        }
2149    }
2150
2151    pub(crate) async fn handle_track_set_parent(
2152        &mut self,
2153        track_name: &str,
2154        parent_name: Option<&str>,
2155    ) {
2156        let track = match self.track_handle_or_err(track_name) {
2157            Ok(track) => track,
2158            Err(e) => {
2159                self.notify_clients(Err(e)).await;
2160                return;
2161            }
2162        };
2163        if parent_name == Some(track_name) {
2164            self.notify_clients(Err("Track cannot be its own parent".to_string()))
2165                .await;
2166            return;
2167        }
2168
2169        // The master track cannot be made part of a folder.
2170        if parent_name.is_some() && track.lock().is_master() {
2171            self.notify_clients(Err(format!(
2172                "Track '{}' is the master track and cannot be made part of a folder",
2173                track_name
2174            )))
2175            .await;
2176            return;
2177        }
2178
2179        // Validate the new parent is a folder (if any).
2180        if let Some(parent_name) = parent_name {
2181            let state = self.state.lock();
2182            let parent = state.tracks.get(parent_name);
2183            if parent.is_none() {
2184                self.notify_clients(Err(format!(
2185                    "Parent track '{}' does not exist",
2186                    parent_name
2187                )))
2188                .await;
2189                return;
2190            }
2191            if !parent.unwrap().lock().is_folder {
2192                self.notify_clients(Err(format!("Track '{}' is not a folder", parent_name)))
2193                    .await;
2194                return;
2195            }
2196        }
2197
2198        // Disconnect from the old parent and update its child list.
2199        {
2200            let old_parent_name = track.lock().parent_track.clone();
2201            if let Some(old_parent_name) = old_parent_name {
2202                let state = self.state.lock();
2203                if let (Some(parent_arc), Some(child_arc)) = (
2204                    state.tracks.get(&old_parent_name).cloned(),
2205                    state.tracks.get(track_name).cloned(),
2206                ) {
2207                    {
2208                        let mut parent = parent_arc.lock();
2209                        parent.child_tracks.retain(|c| c.lock().name != *track_name);
2210                    }
2211                    {
2212                        let mut child = child_arc.lock();
2213                        let parent = parent_arc.lock();
2214                        child.disconnect_from_parent(&parent);
2215                    }
2216                }
2217            }
2218        }
2219
2220        let mut disconnect_actions = Vec::new();
2221
2222        // Remove all existing audio and MIDI connections involving this track.
2223        {
2224            let state = self.state.lock();
2225            let hw_inputs = self.all_hw_input_audio_ports();
2226            let hw_outputs = self.all_hw_output_audio_ports();
2227            if let Some(child_arc) = state.tracks.get(track_name).cloned() {
2228                let child = child_arc.lock();
2229                for (port_idx, inp) in child.audio.ins.iter().enumerate() {
2230                    let sources = inp.connections();
2231                    for src in sources.iter() {
2232                        let _ = AudioIO::disconnect(src, inp);
2233                        if let Some((src_name, src_port)) =
2234                            self.find_audio_io_owner(self.state.as_ref(), src)
2235                        {
2236                            disconnect_actions.push(Action::Disconnect {
2237                                from_track: src_name,
2238                                from_port: src_port,
2239                                to_track: track_name.to_string(),
2240                                to_port: port_idx,
2241                                kind: Kind::Audio,
2242                            });
2243                        } else if let Some(src_port) = hw_inputs
2244                            .iter()
2245                            .position(|hw_in| std::sync::Arc::ptr_eq(hw_in, src))
2246                        {
2247                            disconnect_actions.push(Action::Disconnect {
2248                                from_track: "hw:in".to_string(),
2249                                from_port: src_port,
2250                                to_track: track_name.to_string(),
2251                                to_port: port_idx,
2252                                kind: Kind::Audio,
2253                            });
2254                        }
2255                    }
2256                }
2257                for (port_idx, out) in child.audio.outs.iter().enumerate() {
2258                    let targets = out.connections();
2259                    for tgt in targets.iter() {
2260                        let _ = AudioIO::disconnect(out, tgt);
2261                        if let Some((tgt_name, tgt_port)) =
2262                            self.find_audio_io_owner(self.state.as_ref(), tgt)
2263                        {
2264                            disconnect_actions.push(Action::Disconnect {
2265                                from_track: track_name.to_string(),
2266                                from_port: port_idx,
2267                                to_track: tgt_name,
2268                                to_port: tgt_port,
2269                                kind: Kind::Audio,
2270                            });
2271                        } else if let Some(tgt_port) = hw_outputs
2272                            .iter()
2273                            .position(|hw_out| std::sync::Arc::ptr_eq(hw_out, tgt))
2274                        {
2275                            disconnect_actions.push(Action::Disconnect {
2276                                from_track: track_name.to_string(),
2277                                from_port: port_idx,
2278                                to_track: "hw:out".to_string(),
2279                                to_port: tgt_port,
2280                                kind: Kind::Audio,
2281                            });
2282                        }
2283                    }
2284                }
2285
2286                // Remove MIDI hardware routes.
2287                for route in self
2288                    .midi_hw_in_routes
2289                    .iter()
2290                    .filter(|r| r.to_track == track_name)
2291                {
2292                    disconnect_actions.push(Action::Disconnect {
2293                        from_track: format!("midi:hw:in:{}", route.device),
2294                        from_port: 0,
2295                        to_track: track_name.to_string(),
2296                        to_port: route.to_port,
2297                        kind: Kind::MIDI,
2298                    });
2299                }
2300                self.midi_hw_in_routes.retain(|r| r.to_track != track_name);
2301
2302                for route in self
2303                    .midi_hw_out_routes
2304                    .iter()
2305                    .filter(|r| r.from_track == track_name)
2306                {
2307                    disconnect_actions.push(Action::Disconnect {
2308                        from_track: track_name.to_string(),
2309                        from_port: route.from_port,
2310                        to_track: format!("midi:hw:out:{}", route.device),
2311                        to_port: 0,
2312                        kind: Kind::MIDI,
2313                    });
2314                }
2315                self.midi_hw_out_routes
2316                    .retain(|r| r.from_track != track_name);
2317
2318                // Remove track-to-track MIDI connections where this track is the source.
2319                for (port_idx, out) in child.midi.outs.iter().enumerate() {
2320                    let targets = out.connections();
2321                    for tgt in targets {
2322                        if let Some((tgt_name, tgt_port, _)) =
2323                            self.find_midi_io_owner(self.state.as_ref(), &tgt)
2324                        {
2325                            let _ = MIDIIO::disconnect(out, &tgt);
2326                            disconnect_actions.push(Action::Disconnect {
2327                                from_track: track_name.to_string(),
2328                                from_port: port_idx,
2329                                to_track: tgt_name,
2330                                to_port: tgt_port,
2331                                kind: Kind::MIDI,
2332                            });
2333                        }
2334                    }
2335                }
2336            }
2337
2338            // Remove track-to-track MIDI connections where this track is the target.
2339            let child_input_arcs: Vec<_> =
2340                if let Some(child_arc) = state.tracks.get(track_name).cloned() {
2341                    let child = child_arc.lock();
2342                    child.midi.ins.clone()
2343                } else {
2344                    Vec::new()
2345                };
2346            for (other_name, other_track) in &state.tracks {
2347                if other_name.as_str() == track_name {
2348                    continue;
2349                }
2350                let other = other_track.lock();
2351                for (out_port, out) in other.midi.outs.iter().enumerate() {
2352                    let targets = out.connections();
2353                    for tgt in targets {
2354                        if let Some(to_port) = child_input_arcs
2355                            .iter()
2356                            .position(|inp| std::sync::Arc::ptr_eq(inp, &tgt))
2357                        {
2358                            let _ = MIDIIO::disconnect(out, &tgt);
2359                            disconnect_actions.push(Action::Disconnect {
2360                                from_track: other_name.clone(),
2361                                from_port: out_port,
2362                                to_track: track_name.to_string(),
2363                                to_port,
2364                                kind: Kind::MIDI,
2365                            });
2366                        }
2367                    }
2368                }
2369            }
2370        }
2371
2372        // Apply the parent change.
2373        {
2374            track.lock().parent_track = parent_name.map(str::to_string);
2375        }
2376
2377        // Connect to the new parent and add to its child list.
2378        if let Some(parent_name) = parent_name {
2379            let state = self.state.lock();
2380            if let (Some(parent_arc), Some(child_arc)) = (
2381                state.tracks.get(parent_name).cloned(),
2382                state.tracks.get(track_name).cloned(),
2383            ) {
2384                {
2385                    let mut parent = parent_arc.lock();
2386                    parent.child_tracks.push(child_arc.clone());
2387                }
2388                {
2389                    let mut child = child_arc.lock();
2390                    let mut parent = parent_arc.lock();
2391                    // Folder input -> child input (one-to-one when counts match).
2392                    if parent.audio.ins.len() == child.audio.ins.len() {
2393                        for (parent_in, child_in) in
2394                            parent.audio.ins.iter().zip(child.audio.ins.iter())
2395                        {
2396                            Track::connect_directed_audio(parent_in, child_in);
2397                        }
2398                    }
2399                    // Child output -> folder output (one-to-one when counts match).
2400                    if parent.audio.outs.len() == child.audio.outs.len() {
2401                        for (child_out, parent_out) in
2402                            child.audio.outs.iter().zip(parent.audio.outs.iter())
2403                        {
2404                            AudioIO::connect(child_out, parent_out);
2405                        }
2406                    }
2407                    // Folder MIDI input -> child MIDI input (one-to-one when counts match).
2408                    if parent.midi.ins.len() == child.midi.ins.len() {
2409                        for (parent_in, child_in) in
2410                            parent.midi.ins.iter().zip(child.midi.ins.iter())
2411                        {
2412                            child_in.add_connection(parent_in);
2413                        }
2414                    }
2415                    // Child MIDI output -> folder MIDI output (one-to-one when counts match).
2416                    if parent.midi.outs.len() == child.midi.outs.len() {
2417                        for (child_out, parent_out) in
2418                            child.midi.outs.iter().zip(parent.midi.outs.iter())
2419                        {
2420                            child_out.add_connection(parent_out);
2421                        }
2422                    }
2423                    child.invalidate_audio_route_cache();
2424                    parent.invalidate_audio_route_cache();
2425                    child.invalidate_midi_route_cache();
2426                    parent.invalidate_midi_route_cache();
2427                }
2428            }
2429        }
2430
2431        // Restore default input->output passthrough so audio/MIDI can flow
2432        // through the track whether it is a root track or a folder child.
2433        {
2434            let state = self.state.lock();
2435            if let Some(child_arc) = state.tracks.get(track_name).cloned() {
2436                let mut child = child_arc.lock();
2437                child.ensure_default_audio_passthrough();
2438                child.ensure_default_midi_passthrough();
2439            }
2440        }
2441
2442        for action in disconnect_actions {
2443            self.notify_clients(Ok(action)).await;
2444        }
2445
2446        self.notify_clients(Ok(Action::TrackSetParent {
2447            track_name: track_name.to_string(),
2448            parent_name: parent_name.map(str::to_string),
2449        }))
2450        .await;
2451    }
2452
2453    pub(crate) async fn handle_clip_move(&mut self, action: Action) {
2454        let Action::ClipMove {
2455            ref kind,
2456            ref from,
2457            ref to,
2458            copy,
2459        } = action
2460        else {
2461            return;
2462        };
2463        if let Some(from_track_handle) = self.state.lock().tracks.get(&from.track_name)
2464            && let Some(to_track_handle) = self.state.lock().tracks.get(&to.track_name)
2465        {
2466            let from_track = from_track_handle.lock();
2467            let mut to_track = to_track_handle.lock();
2468            match kind {
2469                Kind::Audio => {
2470                    if from.clip_index >= from_track.audio.clips().len() {
2471                        self.notify_clients(Err(format!(
2472                            "Clip index {} is too high, as track {} has only {} clips!",
2473                            from.clip_index,
2474                            from_track.name.clone(),
2475                            from_track.audio.clips().len(),
2476                        )))
2477                        .await;
2478                        return;
2479                    }
2480                    if from_track.audio.ins.len() != to_track.audio.ins.len() {
2481                        self.notify_clients(Err(format!(
2482                            "Cannot move/copy audio clip from '{}' ({} inputs) to '{}' ({} inputs)",
2483                            from_track.name,
2484                            from_track.audio.ins.len(),
2485                            to_track.name,
2486                            to_track.audio.ins.len()
2487                        )))
2488                        .await;
2489                        return;
2490                    }
2491                    let Some(clip_copy) = from_track.audio.clips().get(from.clip_index).cloned()
2492                    else {
2493                        return;
2494                    };
2495                    if !copy {
2496                        from_track.audio.remove_clip(from.clip_index);
2497                    }
2498                    let mut clip_copy = (*clip_copy).clone();
2499                    let clip_len = clip_copy.end.saturating_sub(clip_copy.start).max(1);
2500                    clip_copy.start = to.sample_offset;
2501                    clip_copy.end = clip_copy.start.saturating_add(clip_len);
2502                    let max_lane = to_track.audio.ins.len().saturating_sub(1);
2503                    clip_copy.input_channel = to.input_channel.min(max_lane);
2504                    Self::push_audio_clip_with_cross_section_fades(&mut to_track, clip_copy);
2505                }
2506                Kind::MIDI => {
2507                    if from.clip_index >= from_track.midi.clips().len() {
2508                        self.notify_clients(Err(format!(
2509                            "Clip index {} is too high, as track {} has only {} clips!",
2510                            from.clip_index,
2511                            from_track.name.clone(),
2512                            from_track.midi.clips().len(),
2513                        )))
2514                        .await;
2515                        return;
2516                    }
2517                    let Some(clip_copy) = from_track.midi.clips().get(from.clip_index).cloned()
2518                    else {
2519                        return;
2520                    };
2521                    if !copy {
2522                        from_track.midi.remove_clip(from.clip_index);
2523                    }
2524                    let mut clip_copy = (*clip_copy).clone();
2525                    let clip_len = clip_copy.end.saturating_sub(clip_copy.start).max(1);
2526                    clip_copy.start = to.sample_offset;
2527                    clip_copy.end = clip_copy.start.saturating_add(clip_len);
2528                    let max_lane = to_track.midi.ins.len().saturating_sub(1);
2529                    clip_copy.input_channel = to.input_channel.min(max_lane);
2530                    to_track.midi.push_clip(clip_copy);
2531                }
2532            }
2533        }
2534    }
2535
2536    pub(crate) async fn handle_rename_track(&mut self, action: Action) -> bool {
2537        let Action::RenameTrack {
2538            ref old_name,
2539            ref new_name,
2540        } = action
2541        else {
2542            return false;
2543        };
2544
2545        if self.state.lock().tracks.contains_key(new_name) {
2546            self.notify_clients(Err(format!("Track '{}' already exists", new_name)))
2547                .await;
2548            return true;
2549        }
2550
2551        let Some(track) = self.state.lock().tracks.remove(old_name) else {
2552            self.notify_clients(Err(format!("Track '{}' not found", old_name)))
2553                .await;
2554            return true;
2555        };
2556
2557        track.lock().name = new_name.clone();
2558        self.state.lock().tracks.insert(new_name.clone(), track);
2559        for other in self.state.lock().tracks.values() {
2560            let mut other = other.lock();
2561            if other.parent_track.as_deref() == Some(old_name.as_str()) {
2562                other.parent_track = Some(new_name.clone());
2563            }
2564        }
2565
2566        if let Some(recording) = self.audio_recordings.remove(old_name) {
2567            self.audio_recordings.insert(new_name.clone(), recording);
2568        }
2569        if let Some(recording) = self.midi_recordings.remove(old_name) {
2570            self.midi_recordings.insert(new_name.clone(), recording);
2571        }
2572
2573        for route in &mut self.midi_hw_in_routes {
2574            if route.to_track == *old_name {
2575                route.to_track = new_name.clone();
2576            }
2577        }
2578        for route in &mut self.midi_hw_out_routes {
2579            if route.from_track == *old_name {
2580                route.from_track = new_name.clone();
2581            }
2582        }
2583        if let Some((armed_track, target, device)) = self.pending_midi_learn.clone()
2584            && armed_track == *old_name
2585        {
2586            self.pending_midi_learn = Some((new_name.clone(), target, device));
2587        }
2588
2589        self.notify_clients(Ok(Action::RenameTrack {
2590            old_name: old_name.clone(),
2591            new_name: new_name.clone(),
2592        }))
2593        .await;
2594
2595        false
2596    }
2597
2598    pub(crate) async fn handle_add_clip(&mut self, action: Action) -> bool {
2599        let Action::AddClip {
2600            ref clip_id,
2601            ref name,
2602            ref track_name,
2603            start,
2604            length,
2605            offset,
2606            input_channel,
2607            muted,
2608            ref peaks_file,
2609            kind,
2610            fade_enabled,
2611            fade_in_samples,
2612            fade_out_samples,
2613            ref source_name,
2614            source_offset,
2615            source_length,
2616            ref preview_name,
2617            ref pitch_correction_points,
2618            pitch_correction_frame_likeness,
2619            pitch_correction_inertia_ms,
2620            pitch_correction_formant_compensation,
2621            ref plugin_graph_json,
2622        } = action
2623        else {
2624            return false;
2625        };
2626
2627        self.add_clip_to_track(ClipAddRequest {
2628            clip_id,
2629            name,
2630            track_name,
2631            start,
2632            length,
2633            offset,
2634            input_channel,
2635            muted,
2636            peaks_file: peaks_file.clone(),
2637            kind,
2638            fade_enabled,
2639            fade_in_samples,
2640            fade_out_samples,
2641            source_name: source_name.clone(),
2642            source_offset,
2643            source_length,
2644            preview_name: preview_name.clone(),
2645            pitch_correction_points: pitch_correction_points.clone(),
2646            pitch_correction_frame_likeness,
2647            pitch_correction_inertia_ms,
2648            pitch_correction_formant_compensation,
2649            plugin_graph_json: plugin_graph_json.clone(),
2650        });
2651        if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
2652            let track_name = track_name.clone();
2653            tokio::task::spawn_blocking(move || {
2654                track.lock().preload_clips();
2655                tracing::debug!("Preloaded clips for track '{}' after AddClip", track_name);
2656            });
2657        }
2658
2659        false
2660    }
2661
2662    pub(crate) async fn handle_track_set_folder(&mut self, a: Action) -> bool {
2663        let Action::TrackSetFolder {
2664            ref track_name,
2665            is_folder,
2666        } = a
2667        else {
2668            return false;
2669        };
2670
2671        let track = match self.track_handle_or_err(track_name) {
2672            Ok(track) => track,
2673            Err(e) => {
2674                self.notify_clients(Err(e)).await;
2675                return true;
2676            }
2677        };
2678        if is_folder {
2679            let is_master = track.lock().is_master();
2680            if is_master {
2681                self.notify_clients(Err(format!(
2682                    "Track '{}' is the master track and cannot be made a folder",
2683                    track_name
2684                )))
2685                .await;
2686                return true;
2687            }
2688        }
2689        {
2690            let mut track = track.lock();
2691            track.is_folder = is_folder;
2692            track.ensure_default_audio_passthrough();
2693            track.ensure_default_midi_passthrough();
2694        }
2695        self.notify_clients(Ok(Action::TrackSetFolder {
2696            track_name: track_name.clone(),
2697            is_folder,
2698        }))
2699        .await;
2700
2701        false
2702    }
2703
2704    pub(crate) async fn handle_track_connect_audio(&mut self, a: Action) -> bool {
2705        let Action::TrackConnectAudio {
2706            ref track_name,
2707            ref from,
2708            from_port,
2709            ref to,
2710            to_port,
2711        } = a
2712        else {
2713            return false;
2714        };
2715
2716        if self
2717            .reject_if_track_frozen(track_name, "routing changes")
2718            .await
2719        {
2720            return true;
2721        }
2722        let track = match self.track_handle_or_err(track_name) {
2723            Ok(track) => track,
2724            Err(e) => {
2725                self.notify_clients(Err(e)).await;
2726                return true;
2727            }
2728        };
2729        if let Err(e) =
2730            track
2731                .lock()
2732                .connect_audio_connectable(from.clone(), from_port, to.clone(), to_port)
2733        {
2734            self.notify_clients(Err(e)).await;
2735            return true;
2736        }
2737
2738        false
2739    }
2740
2741    pub(crate) async fn handle_track_disconnect_audio(&mut self, a: Action) -> bool {
2742        let Action::TrackDisconnectAudio {
2743            ref track_name,
2744            ref from,
2745            from_port,
2746            ref to,
2747            to_port,
2748        } = a
2749        else {
2750            return false;
2751        };
2752
2753        if self
2754            .reject_if_track_frozen(track_name, "routing changes")
2755            .await
2756        {
2757            return true;
2758        }
2759        let track = match self.track_handle_or_err(track_name) {
2760            Ok(track) => track,
2761            Err(e) => {
2762                self.notify_clients(Err(e)).await;
2763                return true;
2764            }
2765        };
2766        if let Err(e) =
2767            track
2768                .lock()
2769                .disconnect_audio_connectable(from.clone(), from_port, to.clone(), to_port)
2770        {
2771            self.notify_clients(Err(e)).await;
2772            return true;
2773        }
2774
2775        false
2776    }
2777
2778    pub(crate) async fn handle_track_connect_midi(&mut self, a: Action) -> bool {
2779        let Action::TrackConnectMidi {
2780            ref track_name,
2781            ref from,
2782            from_port,
2783            ref to,
2784            to_port,
2785        } = a
2786        else {
2787            return false;
2788        };
2789
2790        if self
2791            .reject_if_track_frozen(track_name, "routing changes")
2792            .await
2793        {
2794            return true;
2795        }
2796        let track = match self.track_handle_or_err(track_name) {
2797            Ok(track) => track,
2798            Err(e) => {
2799                self.notify_clients(Err(e)).await;
2800                return true;
2801            }
2802        };
2803        if let Err(e) =
2804            track
2805                .lock()
2806                .connect_midi_connectable(from.clone(), from_port, to.clone(), to_port)
2807        {
2808            self.notify_clients(Err(e)).await;
2809            return true;
2810        }
2811
2812        false
2813    }
2814
2815    pub(crate) async fn handle_track_disconnect_midi(&mut self, a: Action) -> bool {
2816        let Action::TrackDisconnectMidi {
2817            ref track_name,
2818            ref from,
2819            from_port,
2820            ref to,
2821            to_port,
2822        } = a
2823        else {
2824            return false;
2825        };
2826
2827        if self
2828            .reject_if_track_frozen(track_name, "routing changes")
2829            .await
2830        {
2831            return true;
2832        }
2833        let track = match self.track_handle_or_err(track_name) {
2834            Ok(track) => track,
2835            Err(e) => {
2836                self.notify_clients(Err(e)).await;
2837                return true;
2838            }
2839        };
2840        if let Err(e) =
2841            track
2842                .lock()
2843                .disconnect_midi_connectable(from.clone(), from_port, to.clone(), to_port)
2844        {
2845            self.notify_clients(Err(e)).await;
2846            return true;
2847        }
2848
2849        false
2850    }
2851
2852    pub(crate) async fn handle_set_clip_pitch_correction(&mut self, a: Action) -> bool {
2853        let Action::SetClipPitchCorrection {
2854            ref track_name,
2855            clip_index,
2856            ref preview_name,
2857            ref source_name,
2858            source_offset,
2859            source_length,
2860            ref pitch_correction_points,
2861            pitch_correction_frame_likeness,
2862            pitch_correction_inertia_ms,
2863            pitch_correction_formant_compensation,
2864        } = a
2865        else {
2866            return false;
2867        };
2868
2869        self.set_clip_pitch_correction(
2870            track_name,
2871            clip_index,
2872            preview_name.clone(),
2873            source_name.clone(),
2874            source_offset,
2875            source_length,
2876            pitch_correction_points.clone(),
2877            pitch_correction_frame_likeness,
2878            pitch_correction_inertia_ms,
2879            pitch_correction_formant_compensation,
2880        );
2881
2882        false
2883    }
2884
2885    pub(crate) async fn handle_track_remove_audio_output(&mut self, a: Action) -> bool {
2886        let Action::TrackRemoveAudioOutput(ref name) = a else {
2887            return false;
2888        };
2889
2890        let track = match self.track_handle_or_err(name) {
2891            Ok(track) => track,
2892            Err(e) => {
2893                self.notify_clients(Err(e)).await;
2894                return true;
2895            }
2896        };
2897        let (hw_outputs, track_inputs) = {
2898            let state = self.state.lock();
2899            let hw_outputs = self.all_hw_output_audio_ports();
2900            let track_inputs = state
2901                .tracks
2902                .iter()
2903                .filter(|(track_name, _)| *track_name != name)
2904                .flat_map(|(_, handle)| handle.lock().audio.ins.clone())
2905                .collect::<Vec<_>>();
2906            (hw_outputs, track_inputs)
2907        };
2908        if let Err(e) = track.lock().remove_audio_output(&hw_outputs, &track_inputs) {
2909            self.notify_clients(Err(e)).await;
2910            return true;
2911        }
2912
2913        false
2914    }
2915
2916    pub(crate) async fn handle_track_toggle_folder(&mut self, a: Action) -> bool {
2917        let Action::TrackToggleFolder { ref track_name } = a else {
2918            return false;
2919        };
2920
2921        let track = match self.track_handle_or_err(track_name) {
2922            Ok(track) => track,
2923            Err(e) => {
2924                self.notify_clients(Err(e)).await;
2925                return true;
2926            }
2927        };
2928        {
2929            let t = track.lock();
2930            t.folder_open.fetch_not(Ordering::Relaxed);
2931        }
2932        self.notify_clients(Ok(Action::TrackToggleFolder {
2933            track_name: track_name.clone(),
2934        }))
2935        .await;
2936
2937        self.notify_clients(Ok(Action::TrackSetFolder {
2938            track_name: track_name.clone(),
2939            is_folder: track.lock().is_folder,
2940        }))
2941        .await;
2942
2943        false
2944    }
2945
2946    pub(crate) async fn handle_add_grouped_clip(&mut self, a: Action) -> bool {
2947        let Action::AddGroupedClip {
2948            ref track_name,
2949            kind,
2950            ref audio_clip,
2951            ref midi_clip,
2952        } = a
2953        else {
2954            return false;
2955        };
2956
2957        self.add_grouped_clip_to_track(track_name, kind, audio_clip.clone(), midi_clip.clone());
2958        if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
2959            let track_name = track_name.clone();
2960            tokio::task::spawn_blocking(move || {
2961                track.lock().preload_clips();
2962                tracing::debug!(
2963                    "Preloaded clips for track '{}' after AddGroupedClip",
2964                    track_name
2965                );
2966            });
2967        }
2968
2969        false
2970    }
2971
2972    pub(crate) async fn handle_track_add_audio_input(&mut self, a: Action) -> bool {
2973        let Action::TrackAddAudioInput(ref name) = a else {
2974            return false;
2975        };
2976
2977        let track = match self.track_handle_or_err(name) {
2978            Ok(track) => track,
2979            Err(e) => {
2980                self.notify_clients(Err(e)).await;
2981                return true;
2982            }
2983        };
2984        if let Err(e) = track.lock().add_audio_input() {
2985            self.notify_clients(Err(e)).await;
2986            return true;
2987        }
2988
2989        false
2990    }
2991
2992    pub(crate) async fn handle_track_add_audio_output(&mut self, a: Action) -> bool {
2993        let Action::TrackAddAudioOutput(ref name) = a else {
2994            return false;
2995        };
2996
2997        let track = match self.track_handle_or_err(name) {
2998            Ok(track) => track,
2999            Err(e) => {
3000                self.notify_clients(Err(e)).await;
3001                return true;
3002            }
3003        };
3004        if let Err(e) = track.lock().add_audio_output() {
3005            self.notify_clients(Err(e)).await;
3006            return true;
3007        }
3008
3009        false
3010    }
3011
3012    pub(crate) async fn handle_track_remove_audio_input(&mut self, a: Action) -> bool {
3013        let Action::TrackRemoveAudioInput(ref name) = a else {
3014            return false;
3015        };
3016
3017        let track = match self.track_handle_or_err(name) {
3018            Ok(track) => track,
3019            Err(e) => {
3020                self.notify_clients(Err(e)).await;
3021                return true;
3022            }
3023        };
3024        if let Err(e) = track.lock().remove_audio_input() {
3025            self.notify_clients(Err(e)).await;
3026            return true;
3027        }
3028
3029        false
3030    }
3031
3032    pub(crate) async fn handle_track_midi_cc(&mut self, a: Action) -> bool {
3033        let Action::TrackMidiCc {
3034            ref track_name,
3035            channel,
3036            cc,
3037            value,
3038        } = a
3039        else {
3040            return false;
3041        };
3042
3043        if let Some(track) = self.state.lock().tracks.get(track_name) {
3044            track
3045                .lock()
3046                .rt
3047                .pending_automation_midi_events
3048                .push(MidiEvent::new(
3049                    0,
3050                    vec![0xB0 | channel.min(15), cc.min(127), value.min(127)],
3051                ));
3052        }
3053
3054        false
3055    }
3056
3057    pub(crate) async fn handle_track_toggle_arm(&mut self, a: Action) -> bool {
3058        let Action::TrackToggleArm(ref name) = a else {
3059            return false;
3060        };
3061
3062        if self.reject_if_track_frozen(name, "arming/disarming").await {
3063            return true;
3064        }
3065        if let Some(track) = self.state.lock().tracks.get(name).cloned() {
3066            track.lock().arm();
3067            let armed = track.lock().armed();
3068            if !armed && self.audio_recordings.contains_key(name) {
3069                self.flush_track_recording(name).await;
3070            }
3071        } else {
3072            tracing::warn!(
3073                "TrackToggleArm for '{}' but track not found in engine",
3074                name
3075            );
3076        }
3077
3078        false
3079    }
3080
3081    pub(crate) async fn handle_track_set_midi_lane_channel(&mut self, a: Action) -> bool {
3082        let Action::TrackSetMidiLaneChannel {
3083            ref track_name,
3084            lane,
3085            channel,
3086        } = a
3087        else {
3088            return false;
3089        };
3090
3091        let track = match self.track_handle_or_err(track_name) {
3092            Ok(track) => track,
3093            Err(e) => {
3094                self.notify_clients(Err(e)).await;
3095                return true;
3096            }
3097        };
3098        track.lock().set_midi_lane_channel(lane, channel);
3099
3100        false
3101    }
3102
3103    pub(crate) async fn handle_track_set_mpe_zone(&mut self, a: Action) -> bool {
3104        let Action::TrackSetMpeZone {
3105            ref track_name,
3106            manager_channel,
3107            member_count,
3108        } = a
3109        else {
3110            return false;
3111        };
3112
3113        let track = match self.track_handle_or_err(track_name) {
3114            Ok(track) => track,
3115            Err(e) => {
3116                self.notify_clients(Err(e)).await;
3117                return true;
3118            }
3119        };
3120        track.lock().set_mpe_zone(manager_channel, member_count);
3121
3122        false
3123    }
3124
3125    pub(crate) async fn handle_track_set_mpe_pitch_bend_sensitivity(&mut self, a: Action) -> bool {
3126        let Action::TrackSetMpePitchBendSensitivity {
3127            ref track_name,
3128            channel,
3129            semitones,
3130        } = a
3131        else {
3132            return false;
3133        };
3134
3135        let track = match self.track_handle_or_err(track_name) {
3136            Ok(track) => track,
3137            Err(e) => {
3138                self.notify_clients(Err(e)).await;
3139                return true;
3140            }
3141        };
3142        track
3143            .lock()
3144            .set_mpe_pitch_bend_sensitivity(channel, semitones);
3145
3146        false
3147    }
3148
3149    pub(crate) async fn handle_track_set_frozen(&mut self, a: Action) -> bool {
3150        let Action::TrackSetFrozen {
3151            ref track_name,
3152            frozen,
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_frozen(frozen);
3166
3167        false
3168    }
3169
3170    pub(crate) async fn handle_track_clear_default_passthrough(&mut self, a: Action) -> bool {
3171        let Action::TrackClearDefaultPassthrough { ref track_name } = a else {
3172            return false;
3173        };
3174
3175        if self
3176            .reject_if_track_frozen(track_name, "plugin graph editing")
3177            .await
3178        {
3179            return true;
3180        }
3181        let track = match self.track_handle_or_err(track_name) {
3182            Ok(track) => track,
3183            Err(e) => {
3184                self.notify_clients(Err(e)).await;
3185                return true;
3186            }
3187        };
3188        track.lock().clear_default_passthrough();
3189
3190        false
3191    }
3192
3193    pub(crate) async fn handle_set_clip_fade(&mut self, a: Action) -> bool {
3194        let Action::SetClipFade {
3195            ref track_name,
3196            clip_index,
3197            kind,
3198            fade_enabled,
3199            fade_in_samples,
3200            fade_out_samples,
3201        } = a
3202        else {
3203            return false;
3204        };
3205
3206        self.set_clip_fade(
3207            track_name,
3208            clip_index,
3209            kind,
3210            fade_enabled,
3211            fade_in_samples,
3212            fade_out_samples,
3213        );
3214
3215        false
3216    }
3217}
3218
3219#[cfg(test)]
3220mod tests {
3221    use super::*;
3222
3223    fn audio_clip(start: usize, len: usize) -> AudioClip {
3224        AudioClip::new("clip.wav".to_string(), start, start.saturating_add(len))
3225    }
3226
3227    #[test]
3228    fn cross_section_fades_match_non_aligned_overlap_length() {
3229        let mut existing = vec![audio_clip(0, 100)];
3230        let mut inserted = audio_clip(80, 100);
3231
3232        Engine::apply_audio_cross_section_fades(&mut existing, &mut inserted);
3233
3234        assert!(existing[0].fade_enabled);
3235        assert_eq!(existing[0].fade_out_samples, 20);
3236        assert!(inserted.fade_enabled);
3237        assert_eq!(inserted.fade_in_samples, 20);
3238    }
3239
3240    #[test]
3241    fn cross_section_fades_ignore_aligned_starts_and_ends() {
3242        let mut same_start = vec![audio_clip(0, 100)];
3243        let mut inserted_same_start = audio_clip(0, 40);
3244        Engine::apply_audio_cross_section_fades(&mut same_start, &mut inserted_same_start);
3245        assert_eq!(same_start[0].fade_in_samples, 240);
3246        assert_eq!(inserted_same_start.fade_in_samples, 240);
3247
3248        let mut same_end = vec![audio_clip(0, 100)];
3249        let mut inserted_same_end = audio_clip(60, 40);
3250        Engine::apply_audio_cross_section_fades(&mut same_end, &mut inserted_same_end);
3251        assert_eq!(same_end[0].fade_out_samples, 240);
3252        assert_eq!(inserted_same_end.fade_out_samples, 240);
3253    }
3254}