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(
1634        &mut self,
1635        name: String,
1636        audio_ins: usize,
1637        midi_ins: usize,
1638        audio_outs: usize,
1639        midi_outs: usize,
1640        folder: bool,
1641    ) {
1642        let tracks = &mut self.state.lock().tracks;
1643        if tracks.contains_key(&name) {
1644            self.notify_clients(Err(format!("Track {} already exists", name)))
1645                .await;
1646            return;
1647        }
1648        if !folder && !self.history_suspended && audio_ins + midi_ins + audio_outs + midi_outs == 0
1649        {
1650            self.notify_clients(Err(
1651                "Track must have at least one audio or MIDI input/output".to_string(),
1652            ))
1653            .await;
1654            return;
1655        }
1656        let maybe_hw = self
1657            .hw_driver_info
1658            .map(|info| (info.cycle_samples, info.sample_rate as f64));
1659
1660        if let Some((chsamples, sample_rate)) = maybe_hw {
1661            let track = if folder {
1662                Track::new_folder(
1663                    name.clone(),
1664                    audio_ins,
1665                    audio_outs,
1666                    midi_ins,
1667                    midi_outs,
1668                    chsamples,
1669                    sample_rate,
1670                )
1671            } else {
1672                Track::new(
1673                    name.clone(),
1674                    audio_ins,
1675                    audio_outs,
1676                    midi_ins,
1677                    midi_outs,
1678                    chsamples,
1679                    sample_rate,
1680                )
1681            };
1682            tracks.insert(name.clone(), Arc::new(track));
1683            if let Some(track) = tracks.get(&name) {
1684                let mut t = track.lock();
1685                t.set_clip_playback_enabled(self.clip_playback_enabled);
1686                t.set_transport_timing(self.tempo_bpm, self.tsig_num, self.tsig_denom);
1687                t.set_session_base_dir(self.session_dir.clone());
1688            }
1689        } else {
1690            self.notify_clients(Err(
1691                "Engine needs to open audio device before adding audio track".to_string(),
1692            ))
1693            .await;
1694        }
1695    }
1696
1697    pub(crate) async fn handle_remove_track(&mut self, name: String, record_history: bool) {
1698        let mut descendant_names = Vec::new();
1699        self.collect_descendant_track_names(&name, &mut descendant_names);
1700        let names_to_remove: Vec<String> = descendant_names
1701            .iter()
1702            .cloned()
1703            .chain(std::iter::once(name.clone()))
1704            .collect();
1705
1706        let combined_inverse = if record_history && !self.history_suspended {
1707            let mut inv = Vec::new();
1708            for n in &names_to_remove {
1709                if let Some(mut actions) =
1710                    create_inverse_actions(&Action::RemoveTrack(n.clone()), self.state.as_ref())
1711                {
1712                    inv.append(&mut actions);
1713                }
1714                for route in self.midi_hw_in_routes.iter().filter(|r| &r.to_track == n) {
1715                    inv.push(Action::Connect {
1716                        from_track: format!("midi:hw:in:{}", route.device),
1717                        from_port: 0,
1718                        to_track: route.to_track.clone(),
1719                        to_port: route.to_port,
1720                        kind: Kind::MIDI,
1721                    });
1722                }
1723                for route in self
1724                    .midi_hw_out_routes
1725                    .iter()
1726                    .filter(|r| &r.from_track == n)
1727                {
1728                    inv.push(Action::Connect {
1729                        from_track: route.from_track.clone(),
1730                        from_port: route.from_port,
1731                        to_track: format!("midi:hw:out:{}", route.device),
1732                        to_port: 0,
1733                        kind: Kind::MIDI,
1734                    });
1735                }
1736            }
1737
1738            // Reorder so all AddTrack actions come first, then everything else, then
1739            // explicit Connect actions. This mirrors EndHistoryGroup and guarantees that
1740            // tracks are recreated before they are re-parented or reconnected.
1741            let mut add_tracks = Vec::new();
1742            let mut connections = Vec::new();
1743            let mut rest = Vec::new();
1744            for action in inv {
1745                match action {
1746                    Action::AddTrack { .. } => add_tracks.push(action),
1747                    Action::Connect { .. } => connections.push(action),
1748                    _ => rest.push(action),
1749                }
1750            }
1751            let mut ordered = add_tracks;
1752            ordered.extend(rest);
1753            ordered.extend(connections);
1754            ordered
1755        } else {
1756            Vec::new()
1757        };
1758
1759        for n in &descendant_names {
1760            self.remove_single_track(n).await;
1761            self.notify_clients(Ok(Action::RemoveTrack(n.clone())))
1762                .await;
1763        }
1764        self.remove_single_track(&name).await;
1765
1766        if record_history && !self.history_suspended && !combined_inverse.is_empty() {
1767            self.history.record(UndoEntry {
1768                forward_actions: vec![Action::RemoveTrack(name.clone())],
1769                inverse_actions: combined_inverse,
1770            });
1771        }
1772    }
1773
1774    pub(crate) async fn handle_connect(
1775        &mut self,
1776        from_track: &str,
1777        from_port: usize,
1778        to_track: &str,
1779        to_port: usize,
1780        kind: Kind,
1781    ) {
1782        match kind {
1783            Kind::Audio => {
1784                if from_track == to_track && from_track != "hw:in" && to_track != "hw:out" {
1785                    let Some(track) = self.state.lock().tracks.get(from_track).cloned() else {
1786                        self.notify_clients(Err(format!("Track '{from_track}' not found")))
1787                            .await;
1788                        return;
1789                    };
1790                    let result = {
1791                        let mut track = track.lock();
1792                        track.connect_audio_connectable(
1793                            crate::connectable::ConnectableRef::TrackInput,
1794                            to_port,
1795                            crate::connectable::ConnectableRef::TrackOutput,
1796                            from_port,
1797                        )
1798                    };
1799                    if let Err(e) = result {
1800                        self.notify_clients(Err(e)).await;
1801                        return;
1802                    }
1803                    self.notify_clients(Ok(Action::TrackConnectAudio {
1804                        track_name: from_track.to_string(),
1805                        from: crate::connectable::ConnectableRef::TrackInput,
1806                        from_port: to_port,
1807                        to: crate::connectable::ConnectableRef::TrackOutput,
1808                        to_port: from_port,
1809                    }))
1810                    .await;
1811                    return;
1812                }
1813                let (from_audio_io, to_audio_io) =
1814                    self.resolve_audio_route_ports(from_track, from_port, to_track, to_port);
1815                match (from_audio_io, to_audio_io) {
1816                    (Some(source), Some(target)) => {
1817                        if from_track != "hw:in"
1818                            && to_track != "hw:out"
1819                            && self.check_if_leads_to_kind(Kind::Audio, to_track, from_track)
1820                        {
1821                            self.notify_clients(Err("Circular routing is not allowed!".into()))
1822                                .await;
1823                            return;
1824                        }
1825                        crate::audio::io::AudioIO::connect(&source, &target);
1826                    }
1827                    (None, _) => {
1828                        self.notify_clients(Err(format!(
1829                            "Source track '{}' not found",
1830                            from_track
1831                        )))
1832                        .await;
1833                    }
1834                    (_, None) => {
1835                        self.notify_clients(Err(format!(
1836                            "Destination track '{}' not found",
1837                            to_track
1838                        )))
1839                        .await;
1840                    }
1841                }
1842            }
1843            Kind::MIDI => {
1844                let from_hw_in_device = Self::midi_hw_in_device(from_track);
1845                let to_hw_out_device = Self::midi_hw_out_device(to_track);
1846                let from_is_invalid_hw = Self::midi_hw_out_device(from_track).is_some();
1847                let to_is_invalid_hw = Self::midi_hw_in_device(to_track).is_some();
1848
1849                if from_is_invalid_hw || to_is_invalid_hw {
1850                    self.notify_clients(Err(
1851                        "Invalid MIDI hardware connection direction".to_string()
1852                    ))
1853                    .await;
1854                    return;
1855                }
1856
1857                if from_hw_in_device.is_none()
1858                    && to_hw_out_device.is_none()
1859                    && from_track != to_track
1860                    && self.check_if_leads_to_kind(Kind::MIDI, to_track, from_track)
1861                {
1862                    self.notify_clients(Err("Circular routing is not allowed!".into()))
1863                        .await;
1864                    return;
1865                }
1866
1867                if from_track == to_track {
1868                    let Some(track) = self.state.lock().tracks.get(from_track).cloned() else {
1869                        self.notify_clients(Err(format!("Track '{from_track}' not found")))
1870                            .await;
1871                        return;
1872                    };
1873                    let result = {
1874                        let mut track = track.lock();
1875                        track.connect_midi_connectable(
1876                            crate::connectable::ConnectableRef::TrackInput,
1877                            to_port,
1878                            crate::connectable::ConnectableRef::TrackOutput,
1879                            from_port,
1880                        )
1881                    };
1882                    if let Err(e) = result {
1883                        self.notify_clients(Err(e)).await;
1884                        return;
1885                    }
1886                    self.notify_clients(Ok(Action::TrackConnectMidi {
1887                        track_name: from_track.to_string(),
1888                        from: crate::connectable::ConnectableRef::TrackInput,
1889                        from_port: to_port,
1890                        to: crate::connectable::ConnectableRef::TrackOutput,
1891                        to_port: from_port,
1892                    }))
1893                    .await;
1894                    return;
1895                }
1896
1897                let state = self.state.lock();
1898                let from_track_handle = state.tracks.get(from_track);
1899                let to_track_handle = state.tracks.get(to_track);
1900
1901                if let (Some(from_device), Some(to_device)) = (from_hw_in_device, to_hw_out_device)
1902                {
1903                    let route = MidiHwThruRoute {
1904                        from_device: from_device.to_string(),
1905                        to_device: to_device.to_string(),
1906                    };
1907                    if !self.midi_hw_thru_routes.iter().any(|r| r == &route) {
1908                        self.midi_hw_thru_routes.push(route);
1909                    }
1910                } else if let Some(device) = from_hw_in_device {
1911                    if let Some(t_t) = to_track_handle {
1912                        if t_t.lock().midi.ins.get(to_port).is_none() {
1913                            self.notify_clients(Err(format!(
1914                                "MIDI input port {} not found on track '{}'",
1915                                to_port, to_track
1916                            )))
1917                            .await;
1918                            return;
1919                        }
1920                        let route = MidiHwInRoute {
1921                            device: device.to_string(),
1922                            to_track: to_track.to_string(),
1923                            to_port,
1924                        };
1925                        if !self.midi_hw_in_routes.iter().any(|r| r == &route) {
1926                            self.midi_hw_in_routes.push(route);
1927                        }
1928                    } else {
1929                        self.notify_clients(Err(format!(
1930                            "MIDI destination track not found: {}",
1931                            to_track
1932                        )))
1933                        .await;
1934                    }
1935                } else if let Some(device) = to_hw_out_device {
1936                    if let Some(f_t) = from_track_handle {
1937                        if f_t.lock().midi.outs.get(from_port).is_none() {
1938                            self.notify_clients(Err(format!(
1939                                "MIDI output port {} not found on track '{}'",
1940                                from_port, from_track
1941                            )))
1942                            .await;
1943                            return;
1944                        }
1945                        let route = MidiHwOutRoute {
1946                            from_track: from_track.to_string(),
1947                            from_port,
1948                            device: device.to_string(),
1949                        };
1950                        if !self.midi_hw_out_routes.iter().any(|r| r == &route) {
1951                            self.midi_hw_out_routes.push(route);
1952                        }
1953                    } else {
1954                        self.notify_clients(Err(format!(
1955                            "MIDI source track not found: {}",
1956                            from_track
1957                        )))
1958                        .await;
1959                    }
1960                } else {
1961                    match (from_track_handle, to_track_handle) {
1962                        (Some(f_t), Some(t_t)) => {
1963                            let to_in_res = t_t.lock().midi.ins.get(to_port).cloned();
1964                            if let Some(to_in) = to_in_res {
1965                                let mut from_track = f_t.lock();
1966                                if let Err(e) = from_track.midi.connect_out(from_port, to_in) {
1967                                    self.notify_clients(Err(e)).await;
1968                                    return;
1969                                }
1970                                from_track.invalidate_midi_route_cache();
1971                            } else {
1972                                self.notify_clients(Err(format!(
1973                                    "MIDI input port {} not found on track '{}'",
1974                                    to_port, to_track
1975                                )))
1976                                .await;
1977                            }
1978                        }
1979                        _ => {
1980                            self.notify_clients(Err(format!(
1981                                "MIDI tracks not found: {} or {}",
1982                                from_track, to_track
1983                            )))
1984                            .await;
1985                        }
1986                    }
1987                }
1988            }
1989        };
1990    }
1991
1992    pub(crate) async fn handle_disconnect(&mut self, action: Action) {
1993        let Action::Disconnect {
1994            ref from_track,
1995            from_port,
1996            ref to_track,
1997            to_port,
1998            kind,
1999        } = action
2000        else {
2001            return;
2002        };
2003
2004        if from_track == to_track && from_track != "hw:in" && to_track != "hw:out" {
2005            let Some(track) = self.state.lock().tracks.get(from_track).cloned() else {
2006                self.notify_clients(Err(format!("Track '{from_track}' not found")))
2007                    .await;
2008                return;
2009            };
2010            match kind {
2011                Kind::Audio => {
2012                    let result = {
2013                        let mut track = track.lock();
2014                        track.disconnect_audio_connectable(
2015                            crate::connectable::ConnectableRef::TrackInput,
2016                            to_port,
2017                            crate::connectable::ConnectableRef::TrackOutput,
2018                            from_port,
2019                        )
2020                    };
2021                    if let Err(e) = result {
2022                        self.notify_clients(Err(e)).await;
2023                        return;
2024                    }
2025                    self.notify_clients(Ok(Action::TrackDisconnectAudio {
2026                        track_name: from_track.clone(),
2027                        from: crate::connectable::ConnectableRef::TrackInput,
2028                        from_port: to_port,
2029                        to: crate::connectable::ConnectableRef::TrackOutput,
2030                        to_port: from_port,
2031                    }))
2032                    .await;
2033                }
2034                Kind::MIDI => {
2035                    let result = {
2036                        let mut track = track.lock();
2037                        track.disconnect_midi_connectable(
2038                            crate::connectable::ConnectableRef::TrackInput,
2039                            to_port,
2040                            crate::connectable::ConnectableRef::TrackOutput,
2041                            from_port,
2042                        )
2043                    };
2044                    if let Err(e) = result {
2045                        self.notify_clients(Err(e)).await;
2046                        return;
2047                    }
2048                    self.notify_clients(Ok(Action::TrackDisconnectMidi {
2049                        track_name: from_track.clone(),
2050                        from: crate::connectable::ConnectableRef::TrackInput,
2051                        from_port: to_port,
2052                        to: crate::connectable::ConnectableRef::TrackOutput,
2053                        to_port: from_port,
2054                    }))
2055                    .await;
2056                }
2057            }
2058            return;
2059        }
2060
2061        if kind == Kind::Audio {
2062            if let Err(e) = self.disconnect_audio_route_and_notify(action.clone()).await {
2063                self.notify_clients(Err(e)).await;
2064            }
2065        } else if kind == Kind::MIDI {
2066            let from_hw_in_device = Self::midi_hw_in_device(from_track);
2067            let to_hw_out_device = Self::midi_hw_out_device(to_track);
2068
2069            if let (Some(from_device), Some(to_device)) = (from_hw_in_device, to_hw_out_device) {
2070                let before = self.midi_hw_thru_routes.len();
2071                self.midi_hw_thru_routes
2072                    .retain(|r| !(r.from_device == from_device && r.to_device == to_device));
2073                if self.midi_hw_thru_routes.len() < before {
2074                    self.notify_clients(Ok(action.clone())).await;
2075                } else {
2076                    self.notify_clients(Err(format!(
2077                        "Disconnect failed: MIDI route not found ({} -> {})",
2078                        from_track, to_track
2079                    )))
2080                    .await;
2081                }
2082                return;
2083            }
2084
2085            if let Some(device) = from_hw_in_device {
2086                let before = self.midi_hw_in_routes.len();
2087                self.midi_hw_in_routes.retain(|r| {
2088                    !(r.device == device && r.to_track == *to_track && r.to_port == to_port)
2089                });
2090                if self.midi_hw_in_routes.len() < before {
2091                    self.notify_clients(Ok(action.clone())).await;
2092                } else {
2093                    self.notify_clients(Err(format!(
2094                        "Disconnect failed: MIDI route not found ({} -> {})",
2095                        from_track, to_track
2096                    )))
2097                    .await;
2098                }
2099                return;
2100            }
2101
2102            if let Some(device) = to_hw_out_device {
2103                let before = self.midi_hw_out_routes.len();
2104                self.midi_hw_out_routes.retain(|r| {
2105                    !(r.from_track == *from_track && r.from_port == from_port && r.device == device)
2106                });
2107                if self.midi_hw_out_routes.len() < before {
2108                    self.notify_clients(Ok(action.clone())).await;
2109                } else {
2110                    self.notify_clients(Err(format!(
2111                        "Disconnect failed: MIDI route not found ({} -> {})",
2112                        from_track, to_track
2113                    )))
2114                    .await;
2115                }
2116                return;
2117            }
2118
2119            let state = self.state.lock();
2120            if let (Some(f_t), Some(t_t)) =
2121                (state.tracks.get(from_track), state.tracks.get(to_track))
2122                && let Some(to_in) = t_t.lock().midi.ins.get(to_port).cloned()
2123            {
2124                let mut from_track = f_t.lock();
2125                if let Err(e) = from_track.midi.disconnect_out(from_port, &to_in) {
2126                    self.notify_clients(Err(e)).await;
2127                } else {
2128                    from_track.invalidate_midi_route_cache();
2129                    self.notify_clients(Ok(action.clone())).await;
2130                }
2131            } else {
2132                self.notify_clients(Err(format!(
2133                    "Disconnect failed: MIDI ports not found ({} -> {})",
2134                    from_track, to_track
2135                )))
2136                .await;
2137            }
2138        }
2139    }
2140
2141    pub(crate) async fn handle_track_set_parent(
2142        &mut self,
2143        track_name: &str,
2144        parent_name: Option<&str>,
2145    ) {
2146        let track = match self.track_handle_or_err(track_name) {
2147            Ok(track) => track,
2148            Err(e) => {
2149                self.notify_clients(Err(e)).await;
2150                return;
2151            }
2152        };
2153        if parent_name == Some(track_name) {
2154            self.notify_clients(Err("Track cannot be its own parent".to_string()))
2155                .await;
2156            return;
2157        }
2158
2159        // The master track cannot be made part of a folder.
2160        if parent_name.is_some() && track.lock().is_master() {
2161            self.notify_clients(Err(format!(
2162                "Track '{}' is the master track and cannot be made part of a folder",
2163                track_name
2164            )))
2165            .await;
2166            return;
2167        }
2168
2169        // Validate the new parent is a folder (if any).
2170        if let Some(parent_name) = parent_name {
2171            let state = self.state.lock();
2172            let parent = state.tracks.get(parent_name);
2173            if parent.is_none() {
2174                self.notify_clients(Err(format!(
2175                    "Parent track '{}' does not exist",
2176                    parent_name
2177                )))
2178                .await;
2179                return;
2180            }
2181            if !parent.unwrap().lock().is_folder {
2182                self.notify_clients(Err(format!("Track '{}' is not a folder", parent_name)))
2183                    .await;
2184                return;
2185            }
2186        }
2187
2188        // Disconnect from the old parent and update its child list.
2189        {
2190            let old_parent_name = track.lock().parent_track.clone();
2191            if let Some(old_parent_name) = old_parent_name {
2192                let state = self.state.lock();
2193                if let (Some(parent_arc), Some(child_arc)) = (
2194                    state.tracks.get(&old_parent_name).cloned(),
2195                    state.tracks.get(track_name).cloned(),
2196                ) {
2197                    {
2198                        let mut parent = parent_arc.lock();
2199                        parent.child_tracks.retain(|c| c.lock().name != *track_name);
2200                    }
2201                    {
2202                        let mut child = child_arc.lock();
2203                        let parent = parent_arc.lock();
2204                        child.disconnect_from_parent(&parent);
2205                    }
2206                }
2207            }
2208        }
2209
2210        let mut disconnect_actions = Vec::new();
2211
2212        // Remove all existing audio and MIDI connections involving this track.
2213        {
2214            let state = self.state.lock();
2215            let hw_inputs = self.all_hw_input_audio_ports();
2216            let hw_outputs = self.all_hw_output_audio_ports();
2217            if let Some(child_arc) = state.tracks.get(track_name).cloned() {
2218                let child = child_arc.lock();
2219                for (port_idx, inp) in child.audio.ins.iter().enumerate() {
2220                    let sources = inp.connections();
2221                    for src in sources.iter() {
2222                        let _ = AudioIO::disconnect(src, inp);
2223                        if let Some((src_name, src_port)) =
2224                            self.find_audio_io_owner(self.state.as_ref(), src)
2225                        {
2226                            disconnect_actions.push(Action::Disconnect {
2227                                from_track: src_name,
2228                                from_port: src_port,
2229                                to_track: track_name.to_string(),
2230                                to_port: port_idx,
2231                                kind: Kind::Audio,
2232                            });
2233                        } else if let Some(src_port) = hw_inputs
2234                            .iter()
2235                            .position(|hw_in| std::sync::Arc::ptr_eq(hw_in, src))
2236                        {
2237                            disconnect_actions.push(Action::Disconnect {
2238                                from_track: "hw:in".to_string(),
2239                                from_port: src_port,
2240                                to_track: track_name.to_string(),
2241                                to_port: port_idx,
2242                                kind: Kind::Audio,
2243                            });
2244                        }
2245                    }
2246                }
2247                for (port_idx, out) in child.audio.outs.iter().enumerate() {
2248                    let targets = out.connections();
2249                    for tgt in targets.iter() {
2250                        let _ = AudioIO::disconnect(out, tgt);
2251                        if let Some((tgt_name, tgt_port)) =
2252                            self.find_audio_io_owner(self.state.as_ref(), tgt)
2253                        {
2254                            disconnect_actions.push(Action::Disconnect {
2255                                from_track: track_name.to_string(),
2256                                from_port: port_idx,
2257                                to_track: tgt_name,
2258                                to_port: tgt_port,
2259                                kind: Kind::Audio,
2260                            });
2261                        } else if let Some(tgt_port) = hw_outputs
2262                            .iter()
2263                            .position(|hw_out| std::sync::Arc::ptr_eq(hw_out, tgt))
2264                        {
2265                            disconnect_actions.push(Action::Disconnect {
2266                                from_track: track_name.to_string(),
2267                                from_port: port_idx,
2268                                to_track: "hw:out".to_string(),
2269                                to_port: tgt_port,
2270                                kind: Kind::Audio,
2271                            });
2272                        }
2273                    }
2274                }
2275
2276                // Remove MIDI hardware routes.
2277                for route in self
2278                    .midi_hw_in_routes
2279                    .iter()
2280                    .filter(|r| r.to_track == track_name)
2281                {
2282                    disconnect_actions.push(Action::Disconnect {
2283                        from_track: format!("midi:hw:in:{}", route.device),
2284                        from_port: 0,
2285                        to_track: track_name.to_string(),
2286                        to_port: route.to_port,
2287                        kind: Kind::MIDI,
2288                    });
2289                }
2290                self.midi_hw_in_routes.retain(|r| r.to_track != track_name);
2291
2292                for route in self
2293                    .midi_hw_out_routes
2294                    .iter()
2295                    .filter(|r| r.from_track == track_name)
2296                {
2297                    disconnect_actions.push(Action::Disconnect {
2298                        from_track: track_name.to_string(),
2299                        from_port: route.from_port,
2300                        to_track: format!("midi:hw:out:{}", route.device),
2301                        to_port: 0,
2302                        kind: Kind::MIDI,
2303                    });
2304                }
2305                self.midi_hw_out_routes
2306                    .retain(|r| r.from_track != track_name);
2307
2308                // Remove track-to-track MIDI connections where this track is the source.
2309                for (port_idx, out) in child.midi.outs.iter().enumerate() {
2310                    let targets = out.connections();
2311                    for tgt in targets {
2312                        if let Some((tgt_name, tgt_port, _)) =
2313                            self.find_midi_io_owner(self.state.as_ref(), &tgt)
2314                        {
2315                            let _ = MIDIIO::disconnect(out, &tgt);
2316                            disconnect_actions.push(Action::Disconnect {
2317                                from_track: track_name.to_string(),
2318                                from_port: port_idx,
2319                                to_track: tgt_name,
2320                                to_port: tgt_port,
2321                                kind: Kind::MIDI,
2322                            });
2323                        }
2324                    }
2325                }
2326            }
2327
2328            // Remove track-to-track MIDI connections where this track is the target.
2329            let child_input_arcs: Vec<_> =
2330                if let Some(child_arc) = state.tracks.get(track_name).cloned() {
2331                    let child = child_arc.lock();
2332                    child.midi.ins.clone()
2333                } else {
2334                    Vec::new()
2335                };
2336            for (other_name, other_track) in &state.tracks {
2337                if other_name.as_str() == track_name {
2338                    continue;
2339                }
2340                let other = other_track.lock();
2341                for (out_port, out) in other.midi.outs.iter().enumerate() {
2342                    let targets = out.connections();
2343                    for tgt in targets {
2344                        if let Some(to_port) = child_input_arcs
2345                            .iter()
2346                            .position(|inp| std::sync::Arc::ptr_eq(inp, &tgt))
2347                        {
2348                            let _ = MIDIIO::disconnect(out, &tgt);
2349                            disconnect_actions.push(Action::Disconnect {
2350                                from_track: other_name.clone(),
2351                                from_port: out_port,
2352                                to_track: track_name.to_string(),
2353                                to_port,
2354                                kind: Kind::MIDI,
2355                            });
2356                        }
2357                    }
2358                }
2359            }
2360        }
2361
2362        // Apply the parent change.
2363        {
2364            track.lock().parent_track = parent_name.map(str::to_string);
2365        }
2366
2367        // Connect to the new parent and add to its child list.
2368        if let Some(parent_name) = parent_name {
2369            let state = self.state.lock();
2370            if let (Some(parent_arc), Some(child_arc)) = (
2371                state.tracks.get(parent_name).cloned(),
2372                state.tracks.get(track_name).cloned(),
2373            ) {
2374                {
2375                    let mut parent = parent_arc.lock();
2376                    parent.child_tracks.push(child_arc.clone());
2377                }
2378                {
2379                    let mut child = child_arc.lock();
2380                    let mut parent = parent_arc.lock();
2381                    // Folder input -> child input (one-to-one when counts match).
2382                    if parent.audio.ins.len() == child.audio.ins.len() {
2383                        for (parent_in, child_in) in
2384                            parent.audio.ins.iter().zip(child.audio.ins.iter())
2385                        {
2386                            Track::connect_directed_audio(parent_in, child_in);
2387                        }
2388                    }
2389                    // Child output -> folder output (one-to-one when counts match).
2390                    if parent.audio.outs.len() == child.audio.outs.len() {
2391                        for (child_out, parent_out) in
2392                            child.audio.outs.iter().zip(parent.audio.outs.iter())
2393                        {
2394                            AudioIO::connect(child_out, parent_out);
2395                        }
2396                    }
2397                    // Folder MIDI input -> child MIDI input (one-to-one when counts match).
2398                    if parent.midi.ins.len() == child.midi.ins.len() {
2399                        for (parent_in, child_in) in
2400                            parent.midi.ins.iter().zip(child.midi.ins.iter())
2401                        {
2402                            child_in.add_connection(parent_in);
2403                        }
2404                    }
2405                    // Child MIDI output -> folder MIDI output (one-to-one when counts match).
2406                    if parent.midi.outs.len() == child.midi.outs.len() {
2407                        for (child_out, parent_out) in
2408                            child.midi.outs.iter().zip(parent.midi.outs.iter())
2409                        {
2410                            child_out.add_connection(parent_out);
2411                        }
2412                    }
2413                    child.invalidate_audio_route_cache();
2414                    parent.invalidate_audio_route_cache();
2415                    child.invalidate_midi_route_cache();
2416                    parent.invalidate_midi_route_cache();
2417                }
2418            }
2419        }
2420
2421        // Restore default input->output passthrough so audio/MIDI can flow
2422        // through the track whether it is a root track or a folder child.
2423        {
2424            let state = self.state.lock();
2425            if let Some(child_arc) = state.tracks.get(track_name).cloned() {
2426                let mut child = child_arc.lock();
2427                child.ensure_default_audio_passthrough();
2428                child.ensure_default_midi_passthrough();
2429            }
2430        }
2431
2432        for action in disconnect_actions {
2433            self.notify_clients(Ok(action)).await;
2434        }
2435
2436        self.notify_clients(Ok(Action::TrackSetParent {
2437            track_name: track_name.to_string(),
2438            parent_name: parent_name.map(str::to_string),
2439        }))
2440        .await;
2441    }
2442
2443    pub(crate) async fn handle_clip_move(&mut self, action: Action) {
2444        let Action::ClipMove {
2445            ref kind,
2446            ref from,
2447            ref to,
2448            copy,
2449        } = action
2450        else {
2451            return;
2452        };
2453        if let Some(from_track_handle) = self.state.lock().tracks.get(&from.track_name)
2454            && let Some(to_track_handle) = self.state.lock().tracks.get(&to.track_name)
2455        {
2456            let from_track = from_track_handle.lock();
2457            let mut to_track = to_track_handle.lock();
2458            match kind {
2459                Kind::Audio => {
2460                    if from.clip_index >= from_track.audio.clips().len() {
2461                        self.notify_clients(Err(format!(
2462                            "Clip index {} is too high, as track {} has only {} clips!",
2463                            from.clip_index,
2464                            from_track.name.clone(),
2465                            from_track.audio.clips().len(),
2466                        )))
2467                        .await;
2468                        return;
2469                    }
2470                    if from_track.audio.ins.len() != to_track.audio.ins.len() {
2471                        self.notify_clients(Err(format!(
2472                            "Cannot move/copy audio clip from '{}' ({} inputs) to '{}' ({} inputs)",
2473                            from_track.name,
2474                            from_track.audio.ins.len(),
2475                            to_track.name,
2476                            to_track.audio.ins.len()
2477                        )))
2478                        .await;
2479                        return;
2480                    }
2481                    let Some(clip_copy) = from_track.audio.clips().get(from.clip_index).cloned()
2482                    else {
2483                        return;
2484                    };
2485                    if !copy {
2486                        from_track.audio.remove_clip(from.clip_index);
2487                    }
2488                    let mut clip_copy = (*clip_copy).clone();
2489                    let clip_len = clip_copy.end.saturating_sub(clip_copy.start).max(1);
2490                    clip_copy.start = to.sample_offset;
2491                    clip_copy.end = clip_copy.start.saturating_add(clip_len);
2492                    let max_lane = to_track.audio.ins.len().saturating_sub(1);
2493                    clip_copy.input_channel = to.input_channel.min(max_lane);
2494                    Self::push_audio_clip_with_cross_section_fades(&mut to_track, clip_copy);
2495                }
2496                Kind::MIDI => {
2497                    if from.clip_index >= from_track.midi.clips().len() {
2498                        self.notify_clients(Err(format!(
2499                            "Clip index {} is too high, as track {} has only {} clips!",
2500                            from.clip_index,
2501                            from_track.name.clone(),
2502                            from_track.midi.clips().len(),
2503                        )))
2504                        .await;
2505                        return;
2506                    }
2507                    let Some(clip_copy) = from_track.midi.clips().get(from.clip_index).cloned()
2508                    else {
2509                        return;
2510                    };
2511                    if !copy {
2512                        from_track.midi.remove_clip(from.clip_index);
2513                    }
2514                    let mut clip_copy = (*clip_copy).clone();
2515                    let clip_len = clip_copy.end.saturating_sub(clip_copy.start).max(1);
2516                    clip_copy.start = to.sample_offset;
2517                    clip_copy.end = clip_copy.start.saturating_add(clip_len);
2518                    let max_lane = to_track.midi.ins.len().saturating_sub(1);
2519                    clip_copy.input_channel = to.input_channel.min(max_lane);
2520                    to_track.midi.push_clip(clip_copy);
2521                }
2522            }
2523        }
2524    }
2525
2526    pub(crate) async fn handle_rename_track(&mut self, action: Action) -> bool {
2527        let Action::RenameTrack {
2528            ref old_name,
2529            ref new_name,
2530        } = action
2531        else {
2532            return false;
2533        };
2534
2535        if self.state.lock().tracks.contains_key(new_name) {
2536            self.notify_clients(Err(format!("Track '{}' already exists", new_name)))
2537                .await;
2538            return true;
2539        }
2540
2541        let Some(track) = self.state.lock().tracks.remove(old_name) else {
2542            self.notify_clients(Err(format!("Track '{}' not found", old_name)))
2543                .await;
2544            return true;
2545        };
2546
2547        track.lock().name = new_name.clone();
2548        self.state.lock().tracks.insert(new_name.clone(), track);
2549        for other in self.state.lock().tracks.values() {
2550            let mut other = other.lock();
2551            if other.parent_track.as_deref() == Some(old_name.as_str()) {
2552                other.parent_track = Some(new_name.clone());
2553            }
2554        }
2555
2556        if let Some(recording) = self.audio_recordings.remove(old_name) {
2557            self.audio_recordings.insert(new_name.clone(), recording);
2558        }
2559        if let Some(recording) = self.midi_recordings.remove(old_name) {
2560            self.midi_recordings.insert(new_name.clone(), recording);
2561        }
2562
2563        for route in &mut self.midi_hw_in_routes {
2564            if route.to_track == *old_name {
2565                route.to_track = new_name.clone();
2566            }
2567        }
2568        for route in &mut self.midi_hw_out_routes {
2569            if route.from_track == *old_name {
2570                route.from_track = new_name.clone();
2571            }
2572        }
2573        if let Some((armed_track, target, device)) = self.pending_midi_learn.clone()
2574            && armed_track == *old_name
2575        {
2576            self.pending_midi_learn = Some((new_name.clone(), target, device));
2577        }
2578
2579        self.notify_clients(Ok(Action::RenameTrack {
2580            old_name: old_name.clone(),
2581            new_name: new_name.clone(),
2582        }))
2583        .await;
2584
2585        false
2586    }
2587
2588    pub(crate) async fn handle_add_clip(&mut self, action: Action) -> bool {
2589        let Action::AddClip {
2590            ref clip_id,
2591            ref name,
2592            ref track_name,
2593            start,
2594            length,
2595            offset,
2596            input_channel,
2597            muted,
2598            ref peaks_file,
2599            kind,
2600            fade_enabled,
2601            fade_in_samples,
2602            fade_out_samples,
2603            ref source_name,
2604            source_offset,
2605            source_length,
2606            ref preview_name,
2607            ref pitch_correction_points,
2608            pitch_correction_frame_likeness,
2609            pitch_correction_inertia_ms,
2610            pitch_correction_formant_compensation,
2611            ref plugin_graph_json,
2612        } = action
2613        else {
2614            return false;
2615        };
2616
2617        self.add_clip_to_track(ClipAddRequest {
2618            clip_id,
2619            name,
2620            track_name,
2621            start,
2622            length,
2623            offset,
2624            input_channel,
2625            muted,
2626            peaks_file: peaks_file.clone(),
2627            kind,
2628            fade_enabled,
2629            fade_in_samples,
2630            fade_out_samples,
2631            source_name: source_name.clone(),
2632            source_offset,
2633            source_length,
2634            preview_name: preview_name.clone(),
2635            pitch_correction_points: pitch_correction_points.clone(),
2636            pitch_correction_frame_likeness,
2637            pitch_correction_inertia_ms,
2638            pitch_correction_formant_compensation,
2639            plugin_graph_json: plugin_graph_json.clone(),
2640        });
2641        if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
2642            let track_name = track_name.clone();
2643            tokio::task::spawn_blocking(move || {
2644                track.lock().preload_clips();
2645                tracing::debug!("Preloaded clips for track '{}' after AddClip", track_name);
2646            });
2647        }
2648
2649        false
2650    }
2651
2652    pub(crate) async fn handle_track_set_folder(&mut self, a: Action) -> bool {
2653        let Action::TrackSetFolder {
2654            ref track_name,
2655            is_folder,
2656        } = a
2657        else {
2658            return false;
2659        };
2660
2661        let track = match self.track_handle_or_err(track_name) {
2662            Ok(track) => track,
2663            Err(e) => {
2664                self.notify_clients(Err(e)).await;
2665                return true;
2666            }
2667        };
2668        if is_folder {
2669            let is_master = track.lock().is_master();
2670            if is_master {
2671                self.notify_clients(Err(format!(
2672                    "Track '{}' is the master track and cannot be made a folder",
2673                    track_name
2674                )))
2675                .await;
2676                return true;
2677            }
2678        }
2679        {
2680            let mut track = track.lock();
2681            track.is_folder = is_folder;
2682            track.ensure_default_audio_passthrough();
2683            track.ensure_default_midi_passthrough();
2684        }
2685        self.notify_clients(Ok(Action::TrackSetFolder {
2686            track_name: track_name.clone(),
2687            is_folder,
2688        }))
2689        .await;
2690
2691        false
2692    }
2693
2694    pub(crate) async fn handle_track_connect_audio(&mut self, a: Action) -> bool {
2695        let Action::TrackConnectAudio {
2696            ref track_name,
2697            ref from,
2698            from_port,
2699            ref to,
2700            to_port,
2701        } = a
2702        else {
2703            return false;
2704        };
2705
2706        if self
2707            .reject_if_track_frozen(track_name, "routing changes")
2708            .await
2709        {
2710            return true;
2711        }
2712        let track = match self.track_handle_or_err(track_name) {
2713            Ok(track) => track,
2714            Err(e) => {
2715                self.notify_clients(Err(e)).await;
2716                return true;
2717            }
2718        };
2719        if let Err(e) =
2720            track
2721                .lock()
2722                .connect_audio_connectable(from.clone(), from_port, to.clone(), to_port)
2723        {
2724            self.notify_clients(Err(e)).await;
2725            return true;
2726        }
2727
2728        false
2729    }
2730
2731    pub(crate) async fn handle_track_disconnect_audio(&mut self, a: Action) -> bool {
2732        let Action::TrackDisconnectAudio {
2733            ref track_name,
2734            ref from,
2735            from_port,
2736            ref to,
2737            to_port,
2738        } = a
2739        else {
2740            return false;
2741        };
2742
2743        if self
2744            .reject_if_track_frozen(track_name, "routing changes")
2745            .await
2746        {
2747            return true;
2748        }
2749        let track = match self.track_handle_or_err(track_name) {
2750            Ok(track) => track,
2751            Err(e) => {
2752                self.notify_clients(Err(e)).await;
2753                return true;
2754            }
2755        };
2756        if let Err(e) =
2757            track
2758                .lock()
2759                .disconnect_audio_connectable(from.clone(), from_port, to.clone(), to_port)
2760        {
2761            self.notify_clients(Err(e)).await;
2762            return true;
2763        }
2764
2765        false
2766    }
2767
2768    pub(crate) async fn handle_track_connect_midi(&mut self, a: Action) -> bool {
2769        let Action::TrackConnectMidi {
2770            ref track_name,
2771            ref from,
2772            from_port,
2773            ref to,
2774            to_port,
2775        } = a
2776        else {
2777            return false;
2778        };
2779
2780        if self
2781            .reject_if_track_frozen(track_name, "routing changes")
2782            .await
2783        {
2784            return true;
2785        }
2786        let track = match self.track_handle_or_err(track_name) {
2787            Ok(track) => track,
2788            Err(e) => {
2789                self.notify_clients(Err(e)).await;
2790                return true;
2791            }
2792        };
2793        if let Err(e) =
2794            track
2795                .lock()
2796                .connect_midi_connectable(from.clone(), from_port, to.clone(), to_port)
2797        {
2798            self.notify_clients(Err(e)).await;
2799            return true;
2800        }
2801
2802        false
2803    }
2804
2805    pub(crate) async fn handle_track_disconnect_midi(&mut self, a: Action) -> bool {
2806        let Action::TrackDisconnectMidi {
2807            ref track_name,
2808            ref from,
2809            from_port,
2810            ref to,
2811            to_port,
2812        } = a
2813        else {
2814            return false;
2815        };
2816
2817        if self
2818            .reject_if_track_frozen(track_name, "routing changes")
2819            .await
2820        {
2821            return true;
2822        }
2823        let track = match self.track_handle_or_err(track_name) {
2824            Ok(track) => track,
2825            Err(e) => {
2826                self.notify_clients(Err(e)).await;
2827                return true;
2828            }
2829        };
2830        if let Err(e) =
2831            track
2832                .lock()
2833                .disconnect_midi_connectable(from.clone(), from_port, to.clone(), to_port)
2834        {
2835            self.notify_clients(Err(e)).await;
2836            return true;
2837        }
2838
2839        false
2840    }
2841
2842    pub(crate) async fn handle_set_clip_pitch_correction(&mut self, a: Action) -> bool {
2843        let Action::SetClipPitchCorrection {
2844            ref track_name,
2845            clip_index,
2846            ref preview_name,
2847            ref source_name,
2848            source_offset,
2849            source_length,
2850            ref pitch_correction_points,
2851            pitch_correction_frame_likeness,
2852            pitch_correction_inertia_ms,
2853            pitch_correction_formant_compensation,
2854        } = a
2855        else {
2856            return false;
2857        };
2858
2859        self.set_clip_pitch_correction(
2860            track_name,
2861            clip_index,
2862            preview_name.clone(),
2863            source_name.clone(),
2864            source_offset,
2865            source_length,
2866            pitch_correction_points.clone(),
2867            pitch_correction_frame_likeness,
2868            pitch_correction_inertia_ms,
2869            pitch_correction_formant_compensation,
2870        );
2871
2872        false
2873    }
2874
2875    pub(crate) async fn handle_track_remove_audio_output(&mut self, a: Action) -> bool {
2876        let Action::TrackRemoveAudioOutput(ref name) = a else {
2877            return false;
2878        };
2879
2880        let track = match self.track_handle_or_err(name) {
2881            Ok(track) => track,
2882            Err(e) => {
2883                self.notify_clients(Err(e)).await;
2884                return true;
2885            }
2886        };
2887        let (hw_outputs, track_inputs) = {
2888            let state = self.state.lock();
2889            let hw_outputs = self.all_hw_output_audio_ports();
2890            let track_inputs = state
2891                .tracks
2892                .iter()
2893                .filter(|(track_name, _)| *track_name != name)
2894                .flat_map(|(_, handle)| handle.lock().audio.ins.clone())
2895                .collect::<Vec<_>>();
2896            (hw_outputs, track_inputs)
2897        };
2898        if let Err(e) = track.lock().remove_audio_output(&hw_outputs, &track_inputs) {
2899            self.notify_clients(Err(e)).await;
2900            return true;
2901        }
2902
2903        false
2904    }
2905
2906    pub(crate) async fn handle_track_toggle_folder(&mut self, a: Action) -> bool {
2907        let Action::TrackToggleFolder { ref track_name } = a else {
2908            return false;
2909        };
2910
2911        let track = match self.track_handle_or_err(track_name) {
2912            Ok(track) => track,
2913            Err(e) => {
2914                self.notify_clients(Err(e)).await;
2915                return true;
2916            }
2917        };
2918        {
2919            let t = track.lock();
2920            t.folder_open.fetch_not(Ordering::Relaxed);
2921        }
2922        self.notify_clients(Ok(Action::TrackToggleFolder {
2923            track_name: track_name.clone(),
2924        }))
2925        .await;
2926
2927        self.notify_clients(Ok(Action::TrackSetFolder {
2928            track_name: track_name.clone(),
2929            is_folder: track.lock().is_folder,
2930        }))
2931        .await;
2932
2933        false
2934    }
2935
2936    pub(crate) async fn handle_add_grouped_clip(&mut self, a: Action) -> bool {
2937        let Action::AddGroupedClip {
2938            ref track_name,
2939            kind,
2940            ref audio_clip,
2941            ref midi_clip,
2942        } = a
2943        else {
2944            return false;
2945        };
2946
2947        self.add_grouped_clip_to_track(track_name, kind, audio_clip.clone(), midi_clip.clone());
2948        if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
2949            let track_name = track_name.clone();
2950            tokio::task::spawn_blocking(move || {
2951                track.lock().preload_clips();
2952                tracing::debug!(
2953                    "Preloaded clips for track '{}' after AddGroupedClip",
2954                    track_name
2955                );
2956            });
2957        }
2958
2959        false
2960    }
2961
2962    pub(crate) async fn handle_track_add_audio_input(&mut self, a: Action) -> bool {
2963        let Action::TrackAddAudioInput(ref name) = a else {
2964            return false;
2965        };
2966
2967        let track = match self.track_handle_or_err(name) {
2968            Ok(track) => track,
2969            Err(e) => {
2970                self.notify_clients(Err(e)).await;
2971                return true;
2972            }
2973        };
2974        if let Err(e) = track.lock().add_audio_input() {
2975            self.notify_clients(Err(e)).await;
2976            return true;
2977        }
2978
2979        false
2980    }
2981
2982    pub(crate) async fn handle_track_add_audio_output(&mut self, a: Action) -> bool {
2983        let Action::TrackAddAudioOutput(ref name) = a else {
2984            return false;
2985        };
2986
2987        let track = match self.track_handle_or_err(name) {
2988            Ok(track) => track,
2989            Err(e) => {
2990                self.notify_clients(Err(e)).await;
2991                return true;
2992            }
2993        };
2994        if let Err(e) = track.lock().add_audio_output() {
2995            self.notify_clients(Err(e)).await;
2996            return true;
2997        }
2998
2999        false
3000    }
3001
3002    pub(crate) async fn handle_track_remove_audio_input(&mut self, a: Action) -> bool {
3003        let Action::TrackRemoveAudioInput(ref name) = a else {
3004            return false;
3005        };
3006
3007        let track = match self.track_handle_or_err(name) {
3008            Ok(track) => track,
3009            Err(e) => {
3010                self.notify_clients(Err(e)).await;
3011                return true;
3012            }
3013        };
3014        if let Err(e) = track.lock().remove_audio_input() {
3015            self.notify_clients(Err(e)).await;
3016            return true;
3017        }
3018
3019        false
3020    }
3021
3022    pub(crate) async fn handle_track_midi_cc(&mut self, a: Action) -> bool {
3023        let Action::TrackMidiCc {
3024            ref track_name,
3025            channel,
3026            cc,
3027            value,
3028        } = a
3029        else {
3030            return false;
3031        };
3032
3033        if let Some(track) = self.state.lock().tracks.get(track_name) {
3034            track
3035                .lock()
3036                .rt
3037                .pending_automation_midi_events
3038                .push(MidiEvent::new(
3039                    0,
3040                    vec![0xB0 | channel.min(15), cc.min(127), value.min(127)],
3041                ));
3042        }
3043
3044        false
3045    }
3046
3047    pub(crate) async fn handle_track_toggle_arm(&mut self, a: Action) -> bool {
3048        let Action::TrackToggleArm(ref name) = a else {
3049            return false;
3050        };
3051
3052        if self.reject_if_track_frozen(name, "arming/disarming").await {
3053            return true;
3054        }
3055        if let Some(track) = self.state.lock().tracks.get(name).cloned() {
3056            track.lock().arm();
3057            let armed = track.lock().armed();
3058            if !armed && self.audio_recordings.contains_key(name) {
3059                self.flush_track_recording(name).await;
3060            }
3061        } else {
3062            tracing::warn!(
3063                "TrackToggleArm for '{}' but track not found in engine",
3064                name
3065            );
3066        }
3067
3068        false
3069    }
3070
3071    pub(crate) async fn handle_track_set_midi_lane_channel(&mut self, a: Action) -> bool {
3072        let Action::TrackSetMidiLaneChannel {
3073            ref track_name,
3074            lane,
3075            channel,
3076        } = a
3077        else {
3078            return false;
3079        };
3080
3081        let track = match self.track_handle_or_err(track_name) {
3082            Ok(track) => track,
3083            Err(e) => {
3084                self.notify_clients(Err(e)).await;
3085                return true;
3086            }
3087        };
3088        track.lock().set_midi_lane_channel(lane, channel);
3089
3090        false
3091    }
3092
3093    pub(crate) async fn handle_track_set_mpe_zone(&mut self, a: Action) -> bool {
3094        let Action::TrackSetMpeZone {
3095            ref track_name,
3096            manager_channel,
3097            member_count,
3098        } = a
3099        else {
3100            return false;
3101        };
3102
3103        let track = match self.track_handle_or_err(track_name) {
3104            Ok(track) => track,
3105            Err(e) => {
3106                self.notify_clients(Err(e)).await;
3107                return true;
3108            }
3109        };
3110        track.lock().set_mpe_zone(manager_channel, member_count);
3111
3112        false
3113    }
3114
3115    pub(crate) async fn handle_track_set_mpe_pitch_bend_sensitivity(&mut self, a: Action) -> bool {
3116        let Action::TrackSetMpePitchBendSensitivity {
3117            ref track_name,
3118            channel,
3119            semitones,
3120        } = a
3121        else {
3122            return false;
3123        };
3124
3125        let track = match self.track_handle_or_err(track_name) {
3126            Ok(track) => track,
3127            Err(e) => {
3128                self.notify_clients(Err(e)).await;
3129                return true;
3130            }
3131        };
3132        track
3133            .lock()
3134            .set_mpe_pitch_bend_sensitivity(channel, semitones);
3135
3136        false
3137    }
3138
3139    pub(crate) async fn handle_track_set_frozen(&mut self, a: Action) -> bool {
3140        let Action::TrackSetFrozen {
3141            ref track_name,
3142            frozen,
3143        } = a
3144        else {
3145            return false;
3146        };
3147
3148        let track = match self.track_handle_or_err(track_name) {
3149            Ok(track) => track,
3150            Err(e) => {
3151                self.notify_clients(Err(e)).await;
3152                return true;
3153            }
3154        };
3155        track.lock().set_frozen(frozen);
3156
3157        false
3158    }
3159
3160    pub(crate) async fn handle_track_clear_default_passthrough(&mut self, a: Action) -> bool {
3161        let Action::TrackClearDefaultPassthrough { ref track_name } = a else {
3162            return false;
3163        };
3164
3165        if self
3166            .reject_if_track_frozen(track_name, "plugin graph editing")
3167            .await
3168        {
3169            return true;
3170        }
3171        let track = match self.track_handle_or_err(track_name) {
3172            Ok(track) => track,
3173            Err(e) => {
3174                self.notify_clients(Err(e)).await;
3175                return true;
3176            }
3177        };
3178        track.lock().clear_default_passthrough();
3179
3180        false
3181    }
3182
3183    pub(crate) async fn handle_set_clip_fade(&mut self, a: Action) -> bool {
3184        let Action::SetClipFade {
3185            ref track_name,
3186            clip_index,
3187            kind,
3188            fade_enabled,
3189            fade_in_samples,
3190            fade_out_samples,
3191        } = a
3192        else {
3193            return false;
3194        };
3195
3196        self.set_clip_fade(
3197            track_name,
3198            clip_index,
3199            kind,
3200            fade_enabled,
3201            fade_in_samples,
3202            fade_out_samples,
3203        );
3204
3205        false
3206    }
3207}
3208
3209#[cfg(test)]
3210mod tests {
3211    use super::*;
3212
3213    fn audio_clip(start: usize, len: usize) -> AudioClip {
3214        AudioClip::new("clip.wav".to_string(), start, start.saturating_add(len))
3215    }
3216
3217    #[test]
3218    fn cross_section_fades_match_non_aligned_overlap_length() {
3219        let mut existing = vec![audio_clip(0, 100)];
3220        let mut inserted = audio_clip(80, 100);
3221
3222        Engine::apply_audio_cross_section_fades(&mut existing, &mut inserted);
3223
3224        assert!(existing[0].fade_enabled);
3225        assert_eq!(existing[0].fade_out_samples, 20);
3226        assert!(inserted.fade_enabled);
3227        assert_eq!(inserted.fade_in_samples, 20);
3228    }
3229
3230    #[test]
3231    fn cross_section_fades_ignore_aligned_starts_and_ends() {
3232        let mut same_start = vec![audio_clip(0, 100)];
3233        let mut inserted_same_start = audio_clip(0, 40);
3234        Engine::apply_audio_cross_section_fades(&mut same_start, &mut inserted_same_start);
3235        assert_eq!(same_start[0].fade_in_samples, 240);
3236        assert_eq!(inserted_same_start.fade_in_samples, 240);
3237
3238        let mut same_end = vec![audio_clip(0, 100)];
3239        let mut inserted_same_end = audio_clip(60, 40);
3240        Engine::apply_audio_cross_section_fades(&mut same_end, &mut inserted_same_end);
3241        assert_eq!(same_end[0].fade_out_samples, 240);
3242        assert_eq!(inserted_same_end.fade_out_samples, 240);
3243    }
3244}