Skip to main content

maolan_engine/engine/
topology.rs

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