Skip to main content

maolan_engine/engine/
topology.rs

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