Skip to main content

maolan_engine/engine/
topology.rs

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