Skip to main content

maolan_engine/
engine.rs

1use midly::{
2    Arena, Format, Header, MetaMessage, Smf, Timing, TrackEvent, TrackEventKind,
3    live::LiveEvent,
4    num::{u15, u24, u28},
5};
6#[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
7use std::fs::read_dir;
8use std::{
9    collections::{HashMap, VecDeque},
10    fs::File,
11    path::{Path, PathBuf},
12    sync::{
13        Arc,
14        atomic::{AtomicBool, Ordering},
15    },
16    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
17};
18use tokio::sync::mpsc::{Receiver, Sender, channel};
19use tokio::task::JoinHandle;
20use tracing::error;
21
22type HwDeviceInfo = (usize, usize, usize, ((usize, usize), (usize, usize)));
23
24#[cfg(target_os = "linux")]
25use crate::hw::alsa::{HwDriver, HwOptions, MidiHub};
26#[cfg(target_os = "macos")]
27use crate::hw::coreaudio::{HwDriver, HwOptions, MidiHub};
28#[cfg(unix)]
29use crate::hw::jack::JackRuntime;
30#[cfg(target_os = "windows")]
31use crate::hw::options::HwOptions;
32#[cfg(target_os = "freebsd")]
33use crate::hw::oss as hw;
34#[cfg(target_os = "freebsd")]
35use crate::hw::oss::{HwDriver, HwOptions, MidiHub};
36#[cfg(target_os = "openbsd")]
37use crate::hw::sndio::{HwDriver, HwOptions, MidiHub};
38#[cfg(target_os = "windows")]
39use crate::hw::wasapi::{self, HwDriver, MidiHub};
40#[cfg(target_os = "linux")]
41use crate::workers::alsa_worker::HwWorker;
42#[cfg(target_os = "macos")]
43use crate::workers::coreaudio_worker::HwWorker;
44#[cfg(target_os = "freebsd")]
45use crate::workers::oss_worker::HwWorker;
46#[cfg(target_os = "openbsd")]
47use crate::workers::sndio_worker::HwWorker;
48#[cfg(target_os = "windows")]
49use crate::workers::wasapi_worker::HwWorker;
50use crate::{
51    audio::clip::AudioClip,
52    audio::io::AudioIO,
53    history::{History, UndoEntry, create_inverse_actions, should_record},
54    hw::{
55        config,
56        traits::{HwDevice, HwWorkerDriver},
57    },
58    kind::Kind,
59    message::{
60        Action, HwMidiEvent, LaunchQuantization, Message, MidiControllerData, MidiNoteData,
61        PluginKind, ProcessTask, SessionAction, SessionSlotState,
62    },
63    midi::clip::MIDIClip,
64    midi::io::{MIDIIO, MidiEvent},
65    mutex::UnsafeMutex,
66    osc::OscServer,
67    routing,
68    state::State,
69    track::Track,
70    workers::worker::Worker,
71};
72
73#[derive(Debug)]
74struct WorkerData {
75    tx: Sender<Message>,
76    handle: JoinHandle<()>,
77}
78
79impl WorkerData {
80    pub fn new(tx: Sender<Message>, handle: JoinHandle<()>) -> Self {
81        Self { tx, handle }
82    }
83}
84
85#[derive(Debug, Clone)]
86struct RecordingSession {
87    start_sample: usize,
88    samples: Vec<f32>,
89    channels: usize,
90    file_name: String,
91
92    stripe_peaks: Vec<Vec<[f32; 2]>>,
93
94    current_stripe_frames: usize,
95}
96
97const RECORDING_STRIPE_FRAMES: usize = 256;
98
99#[derive(Debug, Clone)]
100struct MidiRecordingSession {
101    start_sample: usize,
102    events: Vec<(u64, Vec<u8>)>,
103    file_name: String,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Hash)]
107struct MidiHwInRoute {
108    device: String,
109    to_track: String,
110    to_port: usize,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Hash)]
114struct MidiHwOutRoute {
115    from_track: String,
116    from_port: usize,
117    device: String,
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, Hash)]
121struct MidiHwThruRoute {
122    from_device: String,
123    to_device: String,
124}
125
126struct OfflineBounceJob {
127    cancel: Arc<AtomicBool>,
128}
129
130#[cfg(unix)]
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132enum JackTransportPlaySync {
133    Start,
134    Stop,
135}
136
137#[derive(Clone, Copy)]
138#[cfg(unix)]
139struct AudioOpenRequest<'a> {
140    device: &'a str,
141    input_device: Option<&'a str>,
142    sample_rate_hz: i32,
143    bits: i32,
144    exclusive: bool,
145    period_frames: usize,
146    nperiods: usize,
147    sync_mode: bool,
148}
149
150struct ClipAddRequest<'a> {
151    clip_id: &'a str,
152    name: &'a str,
153    track_name: &'a str,
154    start: usize,
155    length: usize,
156    offset: usize,
157    input_channel: usize,
158    muted: bool,
159    peaks_file: Option<String>,
160    kind: Kind,
161    fade_enabled: bool,
162    fade_in_samples: usize,
163    fade_out_samples: usize,
164    source_name: Option<String>,
165    source_offset: Option<usize>,
166    source_length: Option<usize>,
167    preview_name: Option<String>,
168    pitch_correction_points: Vec<crate::message::PitchCorrectionPointData>,
169    pitch_correction_frame_likeness: Option<f32>,
170    pitch_correction_inertia_ms: Option<u16>,
171    pitch_correction_formant_compensation: Option<bool>,
172    plugin_graph_json: Option<serde_json::Value>,
173}
174
175#[cfg(unix)]
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177struct JackTransportSyncDecision {
178    play_sync: Option<JackTransportPlaySync>,
179    position_sync: Option<usize>,
180}
181
182#[derive(Clone, Debug, PartialEq, Eq)]
183enum MidiLearnSlot {
184    Track(String, crate::message::TrackMidiLearnTarget),
185    Global(crate::message::GlobalMidiLearnTarget),
186    Session(crate::message::SessionMidiLearnTarget),
187}
188
189pub struct Engine {
190    clients: Vec<Sender<Message>>,
191    rx: Receiver<Message>,
192    state: Arc<UnsafeMutex<State>>,
193    tx: Sender<Message>,
194    workers: Vec<WorkerData>,
195    hw_driver: Option<Arc<UnsafeMutex<HwDriver>>>,
196    #[cfg(unix)]
197    jack_runtime: Option<Arc<UnsafeMutex<JackRuntime>>>,
198    midi_hub: Arc<UnsafeMutex<MidiHub>>,
199    hw_worker: Option<WorkerData>,
200    osc_server: Option<OscServer>,
201    pending_hw_midi_events: Vec<MidiEvent>,
202    pending_hw_midi_events_by_device: HashMap<String, Vec<MidiEvent>>,
203    pending_hw_midi_out_events: Vec<MidiEvent>,
204    pending_hw_midi_out_events_by_device: Vec<HwMidiEvent>,
205    active_hw_notes_by_track: HashMap<String, std::collections::HashSet<(String, u8, u8)>>,
206    active_hw_notes_cycle_start: HashMap<String, std::collections::HashSet<(String, u8, u8)>>,
207    midi_hw_in_routes: Vec<MidiHwInRoute>,
208    midi_hw_out_routes: Vec<MidiHwOutRoute>,
209    midi_hw_thru_routes: Vec<MidiHwThruRoute>,
210    ready_workers: Vec<usize>,
211    pending_requests: VecDeque<Action>,
212    awaiting_hwfinished: bool,
213    handling_hwfinished: bool,
214    track_process_epoch: usize,
215    transport_panic_flush_pending: bool,
216    transport_restart_pending: bool,
217    notified_loop_wrap_sample: Option<usize>,
218    transport_sample: usize,
219
220    hw_input_latency_frames: usize,
221
222    hw_output_latency_frames: usize,
223    loop_enabled: bool,
224    loop_range_samples: Option<(usize, usize)>,
225    metronome_enabled: bool,
226    tempo_bpm: f64,
227    tsig_num: u16,
228    tsig_denom: u16,
229    punch_enabled: bool,
230    punch_range_samples: Option<(usize, usize)>,
231    audio_recordings: std::collections::HashMap<String, RecordingSession>,
232    midi_recordings: std::collections::HashMap<String, MidiRecordingSession>,
233    completed_audio_recordings: Vec<(String, RecordingSession)>,
234    completed_midi_recordings: Vec<(String, MidiRecordingSession)>,
235    playing: bool,
236    clip_playback_enabled: bool,
237    record_enabled: bool,
238    step_recording_enabled: bool,
239    session_dir: Option<PathBuf>,
240    hw_out_level_db: f32,
241    hw_out_balance: f32,
242    hw_out_muted: bool,
243    last_hw_out_meter_publish: Option<Instant>,
244    #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
245    last_hw_out_meter_linear: Vec<f32>,
246    hw_out_peak_hold_linear: Vec<f32>,
247    #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
248    hw_out_meter_publish_phase: bool,
249    last_track_meter_publish: Option<Instant>,
250    last_session_report_publish: Option<Instant>,
251    session_report_state: HashMap<(String, usize), SessionSlotState>,
252    track_meter_linear_by_track: HashMap<String, Vec<f32>>,
253    task_processing_started_at: HashMap<String, Instant>,
254    cycle_tasks: Vec<ProcessTask>,
255    cycle_task_deps: HashMap<String, Vec<String>>,
256    cycle_tasks_running: Vec<ProcessTask>,
257    cycle_tasks_finished: Vec<ProcessTask>,
258    latest_hw_out_meter_db: Arc<Vec<f32>>,
259    latest_track_meter_snapshot: Arc<Vec<(String, Vec<f32>)>>,
260    history: History,
261    history_group: Option<UndoEntry>,
262    history_suspended: bool,
263    offline_bounce_jobs: HashMap<String, OfflineBounceJob>,
264    pending_midi_learn: Option<(String, crate::message::TrackMidiLearnTarget, Option<String>)>,
265    pending_global_midi_learn: Option<crate::message::GlobalMidiLearnTarget>,
266    pending_session_midi_learn: Option<crate::message::SessionMidiLearnTarget>,
267    global_midi_learn_play_pause: Option<crate::message::MidiLearnBinding>,
268    global_midi_learn_stop: Option<crate::message::MidiLearnBinding>,
269    global_midi_learn_record_toggle: Option<crate::message::MidiLearnBinding>,
270    session_midi_learn_slots: HashMap<(String, usize), crate::message::MidiLearnBinding>,
271    session_midi_learn_scenes: HashMap<usize, crate::message::MidiLearnBinding>,
272    session_midi_learn_stop_track: HashMap<String, crate::message::MidiLearnBinding>,
273    session_midi_learn_stop_all: Option<crate::message::MidiLearnBinding>,
274    midi_cc_gate: HashMap<(String, u8, u8), bool>,
275    modulators: Vec<crate::modulator::Modulator>,
276    modulator_values: Option<Arc<std::collections::HashMap<usize, f32>>>,
277}
278
279type MidiEditParseResult = (
280    Vec<MidiNoteData>,
281    Vec<MidiControllerData>,
282    Vec<(u64, Vec<u8>)>,
283);
284
285impl Engine {
286    pub fn state(&self) -> Arc<UnsafeMutex<State>> {
287        self.state.clone()
288    }
289
290    const METRONOME_TRACK: &'static str = "metronome";
291    const METRONOME_DEFAULT_LEVEL_DB: f32 = -10.0;
292    const MIDI_CC_ALL_SOUND_OFF: u8 = 120;
293    const MIDI_CC_SUSTAIN_PEDAL: u8 = 64;
294
295    fn default_clip_plugin_graph_json(audio_ins: usize, audio_outs: usize) -> serde_json::Value {
296        let connections = (0..audio_ins.min(audio_outs))
297            .map(|port| {
298                serde_json::json!({
299                    "from_node": "TrackInput",
300                    "from_port": port,
301                    "to_node": "TrackOutput",
302                    "to_port": port,
303                    "kind": "Audio",
304                })
305            })
306            .collect::<Vec<_>>();
307        serde_json::json!({
308            "plugins": [],
309            "connections": connections,
310        })
311    }
312
313    fn meter_linear_to_db(peak: f32) -> f32 {
314        if peak <= 1.0e-6 {
315            -90.0
316        } else {
317            (20.0 * peak.log10()).clamp(-90.0, 20.0)
318        }
319    }
320
321    fn note_off_events_for_track(&mut self, track_name: &str) -> Vec<HwMidiEvent> {
322        let Some(active) = self.active_hw_notes_by_track.remove(track_name) else {
323            return vec![];
324        };
325        let mut channels = std::collections::HashSet::<(String, u8)>::new();
326        let mut events = Vec::with_capacity(active.len() * 2);
327        for (device, channel, pitch) in active {
328            channels.insert((device.clone(), channel));
329            events.push(HwMidiEvent {
330                device,
331                event: MidiEvent::new(0, vec![0x80 | channel.min(15), pitch.min(127), 64]),
332            });
333        }
334        for (device, channel) in channels {
335            events.push(HwMidiEvent {
336                device,
337                event: MidiEvent::new(
338                    0,
339                    vec![0xB0 | channel.min(15), Self::MIDI_CC_SUSTAIN_PEDAL, 0],
340                ),
341            });
342        }
343        events
344    }
345
346    fn set_clip_plugin_graph_json(
347        &mut self,
348        track_name: &str,
349        clip_index: usize,
350        plugin_graph_json: Option<serde_json::Value>,
351    ) {
352        if let Some(track) = self.state.lock().tracks.get(track_name) {
353            let track = track.lock();
354            if let Some(clip) = track.audio.clips.get_mut(clip_index) {
355                clip.plugin_graph_json = plugin_graph_json;
356            }
357        }
358    }
359
360    fn update_active_hw_notes_for_track(&mut self, track_name: &str, device: &str, data: &[u8]) {
361        let Some(status) = data.first().copied() else {
362            return;
363        };
364        let channel = status & 0x0F;
365        match status & 0xF0 {
366            0x80 => {
367                if let Some(&pitch) = data.get(1)
368                    && let Some(active) = self.active_hw_notes_by_track.get_mut(track_name)
369                {
370                    active.remove(&(device.to_string(), channel, pitch));
371                    if active.is_empty() {
372                        self.active_hw_notes_by_track.remove(track_name);
373                    }
374                }
375            }
376            0x90 => {
377                let Some(&pitch) = data.get(1) else {
378                    return;
379                };
380                let velocity = data.get(2).copied().unwrap_or(0);
381                if velocity == 0 {
382                    if let Some(active) = self.active_hw_notes_by_track.get_mut(track_name) {
383                        active.remove(&(device.to_string(), channel, pitch));
384                        if active.is_empty() {
385                            self.active_hw_notes_by_track.remove(track_name);
386                        }
387                    }
388                } else {
389                    self.active_hw_notes_by_track
390                        .entry(track_name.to_string())
391                        .or_default()
392                        .insert((device.to_string(), channel, pitch));
393                }
394            }
395            _ => {}
396        }
397    }
398
399    fn note_off_events_for_all_active_tracks(&mut self) -> Vec<HwMidiEvent> {
400        let track_names: Vec<String> = self.active_hw_notes_by_track.keys().cloned().collect();
401        let mut events = Vec::new();
402        for track_name in track_names {
403            events.extend(self.note_off_events_for_track(&track_name));
404        }
405        events
406    }
407
408    fn panic_events_for_all_hw_midi_outputs(&self) -> Vec<HwMidiEvent> {
409        let mut active_channels = std::collections::HashSet::<(String, u8)>::new();
410        for active in self.active_hw_notes_by_track.values() {
411            for (device, channel, _pitch) in active {
412                active_channels.insert((device.clone(), *channel));
413            }
414        }
415        let mut events = Vec::with_capacity(active_channels.len());
416        for (device, channel) in active_channels {
417            events.push(HwMidiEvent {
418                device,
419                event: MidiEvent::new(0, vec![0xB0 | channel, Self::MIDI_CC_ALL_SOUND_OFF, 0]),
420            });
421        }
422        events
423    }
424
425    fn note_off_events_for_active_snapshot(
426        &self,
427        snapshot: &HashMap<String, std::collections::HashSet<(String, u8, u8)>>,
428        frame: u32,
429    ) -> Vec<HwMidiEvent> {
430        let mut channels = std::collections::HashSet::<(String, u8)>::new();
431        let mut events = Vec::new();
432        for active in snapshot.values() {
433            for (device, channel, pitch) in active {
434                channels.insert((device.clone(), *channel));
435                events.push(HwMidiEvent {
436                    device: device.clone(),
437                    event: MidiEvent::new(
438                        frame,
439                        vec![0x80 | (*channel).min(15), (*pitch).min(127), 64],
440                    ),
441                });
442            }
443        }
444        for (device, channel) in channels {
445            events.push(HwMidiEvent {
446                device,
447                event: MidiEvent::new(
448                    frame,
449                    vec![0xB0 | channel.min(15), Self::MIDI_CC_SUSTAIN_PEDAL, 0],
450                ),
451            });
452        }
453        events
454    }
455
456    fn parse_midi_clip_for_edit(
457        path: &Path,
458        sample_rate: f64,
459        clip_start: usize,
460    ) -> Result<MidiEditParseResult, String> {
461        let bytes = std::fs::read(path).map_err(|e| e.to_string())?;
462        let smf = Smf::parse(&bytes).map_err(|e| e.to_string())?;
463        let Timing::Metrical(ppq) = smf.header.timing else {
464            return Ok((vec![], vec![], vec![]));
465        };
466        let ppq = u64::from(ppq.as_int().max(1));
467
468        let mut tempo_changes: Vec<(u64, u32)> = vec![(0, 500_000)];
469        for track in &smf.tracks {
470            let mut tick = 0_u64;
471            for event in track {
472                tick = tick.saturating_add(event.delta.as_int() as u64);
473                if let TrackEventKind::Meta(MetaMessage::Tempo(us_per_q)) = event.kind {
474                    tempo_changes.push((tick, us_per_q.as_int()));
475                }
476            }
477        }
478        tempo_changes.sort_by_key(|(tick, _)| *tick);
479        let mut normalized_tempos: Vec<(u64, u32)> = Vec::with_capacity(tempo_changes.len());
480        for (tick, tempo) in tempo_changes {
481            if let Some(last) = normalized_tempos.last_mut()
482                && last.0 == tick
483            {
484                last.1 = tempo;
485            } else {
486                normalized_tempos.push((tick, tempo));
487            }
488        }
489        let tempo_changes = normalized_tempos;
490
491        let ticks_to_samples = |tick: u64| -> usize {
492            let mut total_us: u128 = 0;
493            let mut prev_tick = 0_u64;
494            let mut current_tempo_us = 500_000_u32;
495            for (change_tick, tempo_us) in &tempo_changes {
496                if *change_tick > tick {
497                    break;
498                }
499                let seg_ticks = change_tick.saturating_sub(prev_tick);
500                total_us = total_us.saturating_add(
501                    u128::from(seg_ticks).saturating_mul(u128::from(current_tempo_us))
502                        / u128::from(ppq),
503                );
504                prev_tick = *change_tick;
505                current_tempo_us = *tempo_us;
506            }
507            let rem = tick.saturating_sub(prev_tick);
508            total_us = total_us.saturating_add(
509                u128::from(rem).saturating_mul(u128::from(current_tempo_us)) / u128::from(ppq),
510            );
511            ((total_us as f64 / 1_000_000.0) * sample_rate).round() as usize
512        };
513
514        let mut notes = Vec::<MidiNoteData>::new();
515        let mut controllers = Vec::<MidiControllerData>::new();
516        let mut passthrough_events = Vec::<(u64, Vec<u8>)>::new();
517        let mut active_notes: HashMap<(u8, u8), Vec<(u64, u8)>> = HashMap::new();
518
519        for track in &smf.tracks {
520            let mut tick = 0_u64;
521            for event in track {
522                tick = tick.saturating_add(event.delta.as_int() as u64);
523                match event.kind {
524                    TrackEventKind::Midi { channel, message } => {
525                        let channel_u8 = channel.as_int();
526                        match message {
527                            midly::MidiMessage::NoteOn { key, vel } => {
528                                let pitch = key.as_int();
529                                let velocity = vel.as_int();
530                                if velocity == 0 {
531                                    if let Some(starts) = active_notes.get_mut(&(channel_u8, pitch))
532                                        && let Some((start_tick, start_vel)) = starts.pop()
533                                    {
534                                        let start_sample = ticks_to_samples(start_tick);
535                                        let end_sample = ticks_to_samples(tick);
536                                        notes.push(MidiNoteData {
537                                            start_sample,
538                                            length_samples: end_sample
539                                                .saturating_sub(start_sample)
540                                                .max(1),
541                                            pitch,
542                                            velocity: start_vel,
543                                            channel: channel_u8,
544                                        });
545                                    }
546                                } else {
547                                    active_notes
548                                        .entry((channel_u8, pitch))
549                                        .or_default()
550                                        .push((tick, velocity));
551                                }
552                            }
553                            midly::MidiMessage::NoteOff { key, .. } => {
554                                let pitch = key.as_int();
555                                if let Some(starts) = active_notes.get_mut(&(channel_u8, pitch))
556                                    && let Some((start_tick, start_vel)) = starts.pop()
557                                {
558                                    let start_sample = ticks_to_samples(start_tick);
559                                    let end_sample = ticks_to_samples(tick);
560                                    notes.push(MidiNoteData {
561                                        start_sample,
562                                        length_samples: end_sample
563                                            .saturating_sub(start_sample)
564                                            .max(1),
565                                        pitch,
566                                        velocity: start_vel,
567                                        channel: channel_u8,
568                                    });
569                                }
570                            }
571                            midly::MidiMessage::Controller { controller, value } => {
572                                controllers.push(MidiControllerData {
573                                    sample: ticks_to_samples(tick),
574                                    controller: controller.as_int(),
575                                    value: value.as_int(),
576                                    channel: channel_u8,
577                                });
578                            }
579                            _ => {
580                                let mut data = Vec::with_capacity(3);
581                                if (LiveEvent::Midi { channel, message })
582                                    .write(&mut data)
583                                    .is_ok()
584                                {
585                                    passthrough_events.push((ticks_to_samples(tick) as u64, data));
586                                }
587                            }
588                        }
589                    }
590                    TrackEventKind::SysEx(payload) => {
591                        let mut data = Vec::with_capacity(payload.len() + 2);
592                        data.push(0xF0);
593                        data.extend_from_slice(payload);
594                        if data.last().copied() != Some(0xF7) {
595                            data.push(0xF7);
596                        }
597                        passthrough_events.push((ticks_to_samples(tick) as u64, data));
598                    }
599                    TrackEventKind::Escape(payload) => {
600                        let mut data = Vec::with_capacity(payload.len() + 1);
601                        data.push(0xF7);
602                        data.extend_from_slice(payload);
603                        passthrough_events.push((ticks_to_samples(tick) as u64, data));
604                    }
605                    _ => {}
606                }
607            }
608        }
609
610        for ((channel, pitch), starts) in active_notes {
611            for (start_tick, velocity) in starts {
612                let start_sample = ticks_to_samples(start_tick);
613                let end_sample = ticks_to_samples(start_tick.saturating_add(ppq / 8));
614                notes.push(MidiNoteData {
615                    start_sample,
616                    length_samples: end_sample.saturating_sub(start_sample).max(1),
617                    pitch,
618                    velocity,
619                    channel,
620                });
621            }
622        }
623
624        notes.sort_by_key(|n| (n.start_sample, n.pitch));
625        controllers.sort_by_key(|c| (c.sample, c.controller));
626        passthrough_events.sort_by_key(|(sample, _)| *sample);
627
628        let min_sample = notes
629            .iter()
630            .map(|n| n.start_sample)
631            .chain(controllers.iter().map(|c| c.sample))
632            .chain(passthrough_events.iter().map(|(s, _)| *s as usize))
633            .min()
634            .unwrap_or(0);
635        if min_sample >= clip_start && clip_start > 0 {
636            for note in &mut notes {
637                note.start_sample = note.start_sample.saturating_sub(clip_start);
638            }
639            for ctrl in &mut controllers {
640                ctrl.sample = ctrl.sample.saturating_sub(clip_start);
641            }
642            for (sample, _) in &mut passthrough_events {
643                *sample = sample.saturating_sub(clip_start as u64);
644            }
645        }
646
647        Ok((notes, controllers, passthrough_events))
648    }
649
650    fn midi_events_from_notes_and_controllers(
651        notes: &[MidiNoteData],
652        controllers: &[MidiControllerData],
653    ) -> Vec<(u64, Vec<u8>)> {
654        let mut events: Vec<(u64, u8, Vec<u8>)> = Vec::new();
655        for note in notes {
656            let channel = note.channel.min(15);
657            let pitch = note.pitch.min(127);
658            let velocity = note.velocity.min(127);
659            let start = note.start_sample as u64;
660            let end = note.start_sample.saturating_add(note.length_samples).max(1) as u64;
661            events.push((start, 2, vec![0x90 | channel, pitch, velocity]));
662            events.push((end, 0, vec![0x80 | channel, pitch, 64]));
663        }
664        for ctrl in controllers {
665            let channel = ctrl.channel.min(15);
666            let controller = ctrl.controller.min(127);
667            let value = ctrl.value.min(127);
668            events.push((
669                ctrl.sample as u64,
670                1,
671                vec![0xB0 | channel, controller, value],
672            ));
673        }
674        events.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
675        events
676            .into_iter()
677            .map(|(sample, _, data)| (sample, data))
678            .collect()
679    }
680
681    fn is_track_frozen(&self, track_name: &str) -> bool {
682        self.state
683            .lock()
684            .tracks
685            .get(track_name)
686            .map(|track| track.lock().frozen())
687            .unwrap_or(false)
688    }
689
690    async fn reject_if_track_frozen(&mut self, track_name: &str, operation: &str) -> bool {
691        if self.is_track_frozen(track_name) {
692            self.notify_clients(Err(format!(
693                "Track '{track_name}' is frozen; {operation} is blocked"
694            )))
695            .await;
696            true
697        } else {
698            false
699        }
700    }
701
702    fn apply_midi_edit_action(&mut self, action: &Action) -> Result<(), String> {
703        let (track_name, clip_index) = match action {
704            Action::ModifyMidiNotes {
705                track_name,
706                clip_index,
707                ..
708            }
709            | Action::InsertMidiNotes {
710                track_name,
711                clip_index,
712                ..
713            }
714            | Action::DeleteMidiNotes {
715                track_name,
716                clip_index,
717                ..
718            }
719            | Action::ModifyMidiControllers {
720                track_name,
721                clip_index,
722                ..
723            }
724            | Action::InsertMidiControllers {
725                track_name,
726                clip_index,
727                ..
728            }
729            | Action::DeleteMidiControllers {
730                track_name,
731                clip_index,
732                ..
733            }
734            | Action::SetMidiSysExEvents {
735                track_name,
736                clip_index,
737                ..
738            } => (track_name, *clip_index),
739            _ => return Ok(()),
740        };
741
742        let track_handle = self
743            .state
744            .lock()
745            .tracks
746            .get(track_name)
747            .cloned()
748            .ok_or_else(|| format!("Track not found: {track_name}"))?;
749        let (clip_name, clip_path, sample_rate, clip_start) = {
750            let track = track_handle.lock();
751            if clip_index >= track.midi.clips.len() {
752                return Err(format!(
753                    "Invalid MIDI clip index {clip_index} for '{track_name}'"
754                ));
755            }
756            let clip = &track.midi.clips[clip_index];
757            let clip_name = clip.name.clone();
758            let clip_path = track.resolve_clip_path(&clip_name);
759            (clip_name, clip_path, track.sample_rate, clip.start)
760        };
761
762        let (mut notes, mut controllers, mut passthrough_events) =
763            Self::parse_midi_clip_for_edit(&clip_path, sample_rate, clip_start)?;
764
765        match action {
766            Action::ModifyMidiNotes {
767                note_indices,
768                new_notes,
769                ..
770            } => {
771                for (idx, new_note) in note_indices.iter().zip(new_notes.iter()) {
772                    if let Some(note) = notes.get_mut(*idx) {
773                        *note = new_note.clone();
774                    }
775                }
776            }
777            Action::DeleteMidiNotes { note_indices, .. } => {
778                let mut indices = note_indices.clone();
779                indices.sort_unstable();
780                indices.dedup();
781                for idx in indices.into_iter().rev() {
782                    if idx < notes.len() {
783                        notes.remove(idx);
784                    }
785                }
786            }
787            Action::InsertMidiNotes {
788                notes: inserted, ..
789            } => {
790                let mut sorted = inserted.clone();
791                sorted.sort_unstable_by_key(|(idx, _)| *idx);
792                for (idx, note) in sorted {
793                    let at = idx.min(notes.len());
794                    notes.insert(at, note);
795                }
796            }
797            Action::ModifyMidiControllers {
798                controller_indices,
799                new_controllers,
800                ..
801            } => {
802                for (idx, new_ctrl) in controller_indices.iter().zip(new_controllers.iter()) {
803                    if let Some(ctrl) = controllers.get_mut(*idx) {
804                        *ctrl = new_ctrl.clone();
805                    }
806                }
807            }
808            Action::DeleteMidiControllers {
809                controller_indices, ..
810            } => {
811                let mut indices = controller_indices.clone();
812                indices.sort_unstable();
813                indices.dedup();
814                for idx in indices.into_iter().rev() {
815                    if idx < controllers.len() {
816                        controllers.remove(idx);
817                    }
818                }
819            }
820            Action::InsertMidiControllers {
821                controllers: inserted,
822                ..
823            } => {
824                let mut sorted = inserted.clone();
825                sorted.sort_unstable_by_key(|(idx, _)| *idx);
826                for (idx, ctrl) in sorted {
827                    let at = idx.min(controllers.len());
828                    controllers.insert(at, ctrl);
829                }
830            }
831            Action::SetMidiSysExEvents {
832                new_sysex_events, ..
833            } => {
834                passthrough_events
835                    .retain(|(_, data)| !matches!(data.first(), Some(0xF0) | Some(0xF7)));
836                passthrough_events.extend(
837                    new_sysex_events
838                        .iter()
839                        .map(|ev| (ev.sample as u64, ev.data.clone())),
840                );
841            }
842            _ => {}
843        }
844
845        notes.sort_by_key(|n| (n.start_sample, n.pitch));
846        controllers.sort_by_key(|c| (c.sample, c.controller));
847        passthrough_events.sort_by_key(|(sample, _)| *sample);
848        let mut events = Self::midi_events_from_notes_and_controllers(&notes, &controllers);
849        events.extend(passthrough_events);
850        events.sort_by_key(|(sample, _)| *sample);
851        Self::write_midi_file(&clip_path, sample_rate.max(1.0) as u32, &events)?;
852        track_handle.lock().invalidate_midi_clip_cache(&clip_name);
853        Ok(())
854    }
855
856    const METER_PUBLISH_INTERVAL: Duration = Duration::from_millis(50);
857    const SESSION_RUNTIME_REPORT_INTERVAL: Duration = Duration::from_millis(50);
858    const TRACK_PROCESS_TIMEOUT: Duration = Duration::from_millis(250);
859    #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
860    const HW_OUT_METER_LINEAR_EPSILON: f32 = 0.0025;
861
862    #[cfg(all(unix, not(target_os = "macos")))]
863    fn session_plugins_dir(&self) -> Option<PathBuf> {
864        self.session_dir.as_ref().map(|d| d.join("plugins"))
865    }
866
867    fn session_audio_dir(&self) -> Option<PathBuf> {
868        self.session_dir.as_ref().map(|d| d.join("audio"))
869    }
870
871    fn session_midi_dir(&self) -> Option<PathBuf> {
872        self.session_dir.as_ref().map(|d| d.join("midi"))
873    }
874
875    fn session_peaks_dir(&self) -> Option<PathBuf> {
876        self.session_dir.as_ref().map(|d| d.join("peaks"))
877    }
878
879    fn ensure_session_subdirs(&self) {
880        if let Some(root) = &self.session_dir {
881            let _ = std::fs::create_dir_all(root.join("plugins"));
882            let _ = std::fs::create_dir_all(root.join("audio"));
883            let _ = std::fs::create_dir_all(root.join("midi"));
884            let _ = std::fs::create_dir_all(root.join("peaks"));
885        }
886    }
887
888    fn finalize_midi_hw_devices(mut devices: Vec<String>) -> Vec<String> {
889        devices.sort();
890        devices.dedup();
891        devices
892    }
893
894    #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"))]
895    fn discover_midi_hw_devices_from_dir(path: &str, prefixes: &[&str]) -> Vec<String> {
896        let devices = read_dir(path)
897            .map(|rd| {
898                rd.filter_map(Result::ok)
899                    .map(|e| e.path())
900                    .filter_map(|path| {
901                        let name = path.file_name()?.to_str()?;
902                        prefixes
903                            .iter()
904                            .any(|prefix| name.starts_with(prefix))
905                            .then(|| path.to_string_lossy().into_owned())
906                    })
907                    .collect()
908            })
909            .unwrap_or_default();
910        Self::finalize_midi_hw_devices(devices)
911    }
912
913    fn discover_midi_hw_devices() -> Vec<String> {
914        #[cfg(target_os = "freebsd")]
915        let devices = Self::discover_midi_hw_devices_from_dir("/dev", &["umidi", "midi"]);
916        #[cfg(target_os = "linux")]
917        let devices = Self::discover_midi_hw_devices_from_dir("/dev/snd", &["midiC"]);
918        #[cfg(target_os = "openbsd")]
919        let devices = Self::discover_midi_hw_devices_from_dir("/dev", &["midi"]);
920        #[cfg(target_os = "windows")]
921        let devices = {
922            let mut devices = wasapi::list_midi_input_devices();
923            devices.extend(wasapi::list_midi_output_devices());
924            Self::finalize_midi_hw_devices(devices)
925        };
926        #[cfg(target_os = "macos")]
927        let devices = {
928            let mut devices = Vec::new();
929            for source in coremidi::Sources {
930                if let Some(name) = source.display_name() {
931                    devices.push(name);
932                }
933            }
934            for dest in coremidi::Destinations {
935                if let Some(name) = dest.display_name() {
936                    devices.push(name);
937                }
938            }
939            Self::finalize_midi_hw_devices(devices)
940        };
941        devices
942    }
943
944    pub fn new(rx: Receiver<Message>, tx: Sender<Message>) -> Self {
945        Self {
946            rx,
947            tx,
948            clients: vec![],
949            state: Arc::new(UnsafeMutex::new(State::default())),
950            workers: vec![],
951            hw_driver: None,
952            #[cfg(unix)]
953            jack_runtime: None,
954            midi_hub: Arc::new(UnsafeMutex::new(MidiHub::default())),
955            hw_worker: None,
956            osc_server: None,
957            pending_hw_midi_events: vec![],
958            pending_hw_midi_events_by_device: HashMap::new(),
959            pending_hw_midi_out_events: vec![],
960            pending_hw_midi_out_events_by_device: vec![],
961            active_hw_notes_by_track: HashMap::new(),
962            active_hw_notes_cycle_start: HashMap::new(),
963            midi_hw_in_routes: vec![],
964            midi_hw_out_routes: vec![],
965            midi_hw_thru_routes: vec![],
966            ready_workers: vec![],
967            pending_requests: VecDeque::new(),
968            awaiting_hwfinished: false,
969            handling_hwfinished: false,
970            track_process_epoch: 0,
971            transport_panic_flush_pending: false,
972            transport_restart_pending: false,
973            notified_loop_wrap_sample: None,
974            transport_sample: 0,
975            hw_input_latency_frames: 0,
976            hw_output_latency_frames: 0,
977            loop_enabled: false,
978            loop_range_samples: None,
979            metronome_enabled: false,
980            tempo_bpm: 120.0,
981            tsig_num: 4,
982            tsig_denom: 4,
983            punch_enabled: false,
984            punch_range_samples: None,
985            audio_recordings: std::collections::HashMap::new(),
986            midi_recordings: std::collections::HashMap::new(),
987            completed_audio_recordings: Vec::new(),
988            completed_midi_recordings: Vec::new(),
989            playing: false,
990            clip_playback_enabled: true,
991            record_enabled: false,
992            step_recording_enabled: false,
993            session_dir: None,
994            hw_out_level_db: 0.0,
995            hw_out_balance: 0.0,
996            hw_out_muted: false,
997            last_hw_out_meter_publish: None,
998            #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
999            last_hw_out_meter_linear: vec![],
1000            hw_out_peak_hold_linear: vec![],
1001            #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
1002            hw_out_meter_publish_phase: false,
1003            last_track_meter_publish: None,
1004            last_session_report_publish: None,
1005            session_report_state: HashMap::new(),
1006            track_meter_linear_by_track: HashMap::new(),
1007            task_processing_started_at: HashMap::new(),
1008            cycle_tasks: Vec::new(),
1009            cycle_task_deps: HashMap::new(),
1010            cycle_tasks_running: Vec::new(),
1011            cycle_tasks_finished: Vec::new(),
1012            latest_hw_out_meter_db: Arc::new(Vec::new()),
1013            latest_track_meter_snapshot: Arc::new(Vec::new()),
1014            history: History::default(),
1015            history_group: None,
1016            history_suspended: false,
1017            offline_bounce_jobs: HashMap::new(),
1018            pending_midi_learn: None,
1019            pending_global_midi_learn: None,
1020            pending_session_midi_learn: None,
1021            global_midi_learn_play_pause: None,
1022            global_midi_learn_stop: None,
1023            global_midi_learn_record_toggle: None,
1024            session_midi_learn_slots: HashMap::new(),
1025            session_midi_learn_scenes: HashMap::new(),
1026            session_midi_learn_stop_track: HashMap::new(),
1027            session_midi_learn_stop_all: None,
1028            midi_cc_gate: HashMap::new(),
1029            modulators: Vec::new(),
1030            modulator_values: None,
1031        }
1032    }
1033
1034    fn hw_driver_cycle_samples(&self) -> Option<usize> {
1035        self.hw_driver.as_ref().map(|o| o.lock().cycle_samples())
1036    }
1037
1038    #[cfg(unix)]
1039    fn jack_cycle_samples(&self) -> Option<usize> {
1040        self.jack_runtime.as_ref().map(|j| j.lock().buffer_size)
1041    }
1042
1043    #[cfg(not(unix))]
1044    fn jack_cycle_samples(&self) -> Option<usize> {
1045        None
1046    }
1047
1048    fn current_cycle_samples(&self) -> usize {
1049        self.hw_driver_cycle_samples()
1050            .or_else(|| self.jack_cycle_samples())
1051            .unwrap_or(0)
1052    }
1053
1054    fn sample_rate(&self) -> f64 {
1055        if let Some(hw) = &self.hw_driver {
1056            hw.lock().sample_rate() as f64
1057        } else {
1058            #[cfg(unix)]
1059            {
1060                self.jack_runtime
1061                    .as_ref()
1062                    .map(|j| j.lock().sample_rate as f64)
1063                    .unwrap_or(48_000.0)
1064            }
1065            #[cfg(not(unix))]
1066            {
1067                48_000.0
1068            }
1069        }
1070    }
1071
1072    fn compute_modulator_values(
1073        &self,
1074        sample: usize,
1075    ) -> Arc<std::collections::HashMap<usize, f32>> {
1076        let sample_rate = self.sample_rate();
1077        let values: std::collections::HashMap<usize, f32> = self
1078            .modulators
1079            .iter()
1080            .filter(|m| m.enabled)
1081            .map(|m| (m.id, m.value_at(sample, sample_rate)))
1082            .collect();
1083        Arc::new(values)
1084    }
1085
1086    fn apply_modulators(&mut self, sample: usize) -> Vec<Action> {
1087        use crate::modulator::ModulatorTarget;
1088        let values = self.compute_modulator_values(sample);
1089        self.modulator_values = Some(values.clone());
1090        let mut echoes = Vec::new();
1091        let mut per_track: HashMap<String, (Option<f32>, Option<f32>)> = HashMap::new();
1092        let mut clap_params: HashMap<(String, usize, u32), f64> = HashMap::new();
1093        let mut vst3_params: HashMap<(String, usize, u32), f32> = HashMap::new();
1094        #[cfg(all(unix, not(target_os = "macos")))]
1095        let mut lv2_params: HashMap<(String, usize, u32), f32> = HashMap::new();
1096        let mut midi_cc_events: HashMap<String, Vec<MidiEvent>> = HashMap::new();
1097
1098        let map_f32 = |value: f32, min: f32, max: f32| -> f32 {
1099            crate::modulator::map_value(value, min, max)
1100        };
1101        let map_f64 = |value: f32, min: f64, max: f64| -> f64 {
1102            crate::modulator::map_value_f64(value, min, max)
1103        };
1104
1105        for m in &self.modulators {
1106            if !m.enabled {
1107                continue;
1108            }
1109            let Some(&value) = values.get(&m.id) else {
1110                continue;
1111            };
1112            for target in &m.targets {
1113                match target {
1114                    ModulatorTarget::TrackVolume {
1115                        track_name,
1116                        min,
1117                        max,
1118                    } => {
1119                        let clamped = map_f32(value, *min, *max);
1120                        per_track.entry(track_name.clone()).or_default().0 = Some(clamped);
1121                    }
1122                    ModulatorTarget::TrackBalance {
1123                        track_name,
1124                        min,
1125                        max,
1126                    } => {
1127                        let clamped = map_f32(value, *min, *max);
1128                        per_track.entry(track_name.clone()).or_default().1 = Some(clamped);
1129                    }
1130                    ModulatorTarget::HwOutVolume { min, max } => {
1131                        let clamped = map_f32(value, *min, *max);
1132                        if (self.hw_out_level_db - clamped).abs() > f32::EPSILON {
1133                            self.hw_out_level_db = clamped;
1134                            echoes
1135                                .push(Action::TrackAutomationLevel("hw:out".to_string(), clamped));
1136                        }
1137                    }
1138                    ModulatorTarget::HwOutBalance { min, max } => {
1139                        let next = map_f32(value, *min, *max).clamp(-1.0, 1.0);
1140                        if (self.hw_out_balance - next).abs() > f32::EPSILON {
1141                            self.hw_out_balance = next;
1142                            echoes.push(Action::TrackAutomationBalance("hw:out".to_string(), next));
1143                        }
1144                    }
1145                    ModulatorTarget::ClapParameter {
1146                        track_name,
1147                        instance_id,
1148                        param_id,
1149                        min,
1150                        max,
1151                    } => {
1152                        let param_value = map_f64(value, *min, *max);
1153                        clap_params
1154                            .insert((track_name.clone(), *instance_id, *param_id), param_value);
1155                    }
1156                    ModulatorTarget::Vst3Parameter {
1157                        track_name,
1158                        instance_id,
1159                        param_id,
1160                        min,
1161                        max,
1162                    } => {
1163                        let param_value = map_f32(value, *min, *max);
1164                        vst3_params
1165                            .insert((track_name.clone(), *instance_id, *param_id), param_value);
1166                    }
1167                    #[cfg(all(unix, not(target_os = "macos")))]
1168                    ModulatorTarget::Lv2Parameter {
1169                        track_name,
1170                        instance_id,
1171                        index,
1172                        min,
1173                        max,
1174                    } => {
1175                        let param_value = map_f32(value, *min, *max);
1176                        lv2_params.insert((track_name.clone(), *instance_id, *index), param_value);
1177                    }
1178                    ModulatorTarget::MidiCc {
1179                        track_name,
1180                        channel,
1181                        cc,
1182                    } => {
1183                        let cc_value = (value * 127.0).round() as u8;
1184                        midi_cc_events
1185                            .entry(track_name.clone())
1186                            .or_default()
1187                            .push(MidiEvent::new(
1188                                0,
1189                                vec![0xB0 | (*channel).min(15), (*cc).min(127), cc_value],
1190                            ));
1191                    }
1192                }
1193            }
1194        }
1195        for (track_name, (level, balance)) in per_track {
1196            if let Some(level) = level
1197                && let Some(track) = self.state.lock().tracks.get(&track_name).cloned()
1198            {
1199                let t = track.lock();
1200                if (t.level() - level).abs() > f32::EPSILON {
1201                    t.set_level(level);
1202                    echoes.push(Action::TrackAutomationLevel(track_name.clone(), level));
1203                }
1204            }
1205            if let Some(balance) = balance
1206                && let Some(track) = self.state.lock().tracks.get(&track_name).cloned()
1207            {
1208                let t = track.lock();
1209                let next = balance.clamp(-1.0, 1.0);
1210                if (t.balance - next).abs() > f32::EPSILON {
1211                    t.set_balance(next);
1212                    echoes.push(Action::TrackAutomationBalance(track_name.clone(), next));
1213                }
1214            }
1215        }
1216
1217        for (track_name, events) in midi_cc_events {
1218            if let Some(track) = self.state.lock().tracks.get(&track_name).cloned() {
1219                track.lock().pending_modulator_midi_events.extend(events);
1220            }
1221        }
1222
1223        let state = self.state.lock();
1224        for ((track_name, instance_id, param_id), value) in clap_params {
1225            if let Some(track) = state.tracks.get(&track_name).cloned()
1226                && track
1227                    .lock()
1228                    .set_clap_parameter(instance_id, param_id, value)
1229                    .is_ok()
1230            {
1231                echoes.push(Action::TrackSetClapParameter {
1232                    track_name,
1233                    instance_id,
1234                    param_id,
1235                    value,
1236                });
1237            }
1238        }
1239        for ((track_name, instance_id, param_id), value) in vst3_params {
1240            if let Some(track) = state.tracks.get(&track_name).cloned()
1241                && track
1242                    .lock()
1243                    .set_vst3_parameter(instance_id, param_id, value)
1244                    .is_ok()
1245            {
1246                echoes.push(Action::TrackSetVst3Parameter {
1247                    track_name,
1248                    instance_id,
1249                    param_id,
1250                    value,
1251                });
1252            }
1253        }
1254        #[cfg(all(unix, not(target_os = "macos")))]
1255        for ((track_name, instance_id, index), value) in lv2_params {
1256            if let Some(track) = state.tracks.get(&track_name).cloned()
1257                && track
1258                    .lock()
1259                    .set_lv2_control_value(instance_id, index as usize, f64::from(value))
1260                    .is_ok()
1261            {
1262                echoes.push(Action::TrackSetLv2ControlValue {
1263                    track_name,
1264                    instance_id,
1265                    index,
1266                    value,
1267                });
1268            }
1269        }
1270
1271        echoes
1272    }
1273
1274    fn session_end_sample(&self) -> usize {
1275        self.state
1276            .lock()
1277            .tracks
1278            .values()
1279            .map(|track| {
1280                let track = track.lock();
1281                let audio_end = track
1282                    .audio
1283                    .clips
1284                    .iter()
1285                    .map(|clip| clip.end)
1286                    .max()
1287                    .unwrap_or(0);
1288                let midi_end = track
1289                    .midi
1290                    .clips
1291                    .iter()
1292                    .map(|clip| clip.end)
1293                    .max()
1294                    .unwrap_or(0);
1295                audio_end.max(midi_end)
1296            })
1297            .max()
1298            .unwrap_or(0)
1299    }
1300
1301    async fn ensure_metronome_track(&mut self) {
1302        if self.state.lock().tracks.contains_key(Self::METRONOME_TRACK) {
1303            return;
1304        }
1305        let (cycle_samples, sample_rate_hz, output_channels): (usize, f64, usize) =
1306            if let Some(hw) = &self.hw_driver {
1307                let hw = hw.lock();
1308                (
1309                    hw.cycle_samples(),
1310                    hw.sample_rate() as f64,
1311                    hw.output_channels(),
1312                )
1313            } else {
1314                #[cfg(unix)]
1315                {
1316                    if let Some(jack) = &self.jack_runtime {
1317                        let jack = jack.lock();
1318                        (
1319                            jack.buffer_size,
1320                            jack.sample_rate as f64,
1321                            jack.audio_outs().len(),
1322                        )
1323                    } else {
1324                        return;
1325                    }
1326                }
1327                #[cfg(not(unix))]
1328                {
1329                    return;
1330                }
1331            };
1332        if output_channels == 0 {
1333            return;
1334        }
1335        self.state.lock().tracks.insert(
1336            Self::METRONOME_TRACK.to_string(),
1337            Arc::new(UnsafeMutex::new(Box::new(Track::new(
1338                Self::METRONOME_TRACK.to_string(),
1339                0,
1340                1,
1341                0,
1342                0,
1343                cycle_samples.max(1),
1344                sample_rate_hz.max(1.0),
1345            )))),
1346        );
1347        if let Some(track) = self.state.lock().tracks.get(Self::METRONOME_TRACK).cloned() {
1348            track.lock().set_level(Self::METRONOME_DEFAULT_LEVEL_DB);
1349            track.lock().set_metronome_enabled(self.metronome_enabled);
1350        }
1351        self.notify_clients(Ok(Action::AddTrack {
1352            name: Self::METRONOME_TRACK.to_string(),
1353            audio_ins: 0,
1354            midi_ins: 0,
1355            audio_outs: 1,
1356            midi_outs: 0,
1357            folder: false,
1358        }))
1359        .await;
1360        self.notify_clients(Ok(Action::TrackLevel(
1361            Self::METRONOME_TRACK.to_string(),
1362            Self::METRONOME_DEFAULT_LEVEL_DB,
1363        )))
1364        .await;
1365    }
1366
1367    fn open_hw_driver(
1368        device: &str,
1369        _input_device: Option<&str>,
1370        sample_rate_hz: i32,
1371        bits: i32,
1372        hw_opts: HwOptions,
1373    ) -> Result<HwDriver, String> {
1374        #[cfg(any(target_os = "windows", target_os = "freebsd", target_os = "linux"))]
1375        {
1376            HwDriver::new_with_options(device, _input_device, sample_rate_hz, bits, hw_opts)
1377                .map_err(|e| e.to_string())
1378        }
1379        #[cfg(target_os = "openbsd")]
1380        {
1381            HwDriver::new_with_options(device, sample_rate_hz, bits, hw_opts)
1382                .map_err(|e| e.to_string())
1383        }
1384    }
1385
1386    fn hw_profile_backend_label(_device: &str) -> &'static str {
1387        #[cfg(target_os = "windows")]
1388        let label = "WASAPI";
1389        #[cfg(target_os = "linux")]
1390        let label = "ALSA";
1391        #[cfg(target_os = "freebsd")]
1392        let label = "OSS";
1393        #[cfg(target_os = "openbsd")]
1394        let label = "sndio";
1395        #[cfg(target_os = "macos")]
1396        let label = "CoreAudio";
1397        label
1398    }
1399
1400    #[cfg(target_os = "freebsd")]
1401    fn maybe_start_freebsd_sync_group(&self) {
1402        if let Some(oss) = &self.hw_driver {
1403            let in_fd = oss.lock().input_fd();
1404            let out_fd = oss.lock().output_fd();
1405            let mut group = 0;
1406            let in_group = hw::add_to_sync_group(in_fd, group, true);
1407            if in_group > 0 {
1408                group = in_group;
1409            }
1410            let out_group = hw::add_to_sync_group(out_fd, group, false);
1411            if out_group > 0 {
1412                group = out_group;
1413            }
1414            let sync_started = if group > 0 {
1415                hw::start_sync_group(in_fd, group).is_ok()
1416            } else {
1417                false
1418            };
1419            if !sync_started {
1420                let _ = oss.lock().start_input_trigger();
1421                let _ = oss.lock().start_output_trigger();
1422            }
1423        }
1424    }
1425
1426    #[cfg(not(target_os = "freebsd"))]
1427    fn maybe_start_freebsd_sync_group(&self) {}
1428
1429    async fn open_discovered_midi_hw_devices(&mut self) {
1430        for device in Self::discover_midi_hw_devices() {
1431            let (opened_in, opened_out) = {
1432                let midi_hub = self.midi_hub.lock();
1433                let opened_in = midi_hub.open_input(&device).is_ok();
1434                let opened_out = midi_hub.open_output(&device).is_ok();
1435                (opened_in, opened_out)
1436            };
1437
1438            if opened_in {
1439                self.notify_clients(Ok(Action::OpenMidiInputDevice(device.clone())))
1440                    .await;
1441            }
1442            if opened_out {
1443                self.notify_clients(Ok(Action::OpenMidiOutputDevice(device.clone())))
1444                    .await;
1445            }
1446        }
1447    }
1448
1449    #[cfg(unix)]
1450    async fn maybe_open_jack_runtime(&mut self, request: AudioOpenRequest<'_>) -> Option<()> {
1451        if !request.device.eq_ignore_ascii_case("jack") {
1452            return None;
1453        }
1454        match JackRuntime::new(
1455            "maolan",
1456            crate::hw::jack::Config::default(),
1457            self.tx.clone(),
1458        ) {
1459            Ok(runtime) => {
1460                let input_channels = runtime.input_channels();
1461                let output_channels = runtime.output_channels();
1462                let midi_inputs = runtime.midi_input_devices();
1463                let midi_outputs = runtime.midi_output_devices();
1464                let rate = runtime.sample_rate;
1465                if let Some(worker) = self.hw_worker.take() {
1466                    if let Some(hw) = &self.hw_driver {
1467                        hw.lock().request_stop();
1468                    }
1469                    let _ = worker.tx.send(Message::Request(Action::Quit)).await;
1470                    let _ = worker.handle.await;
1471                }
1472                self.hw_driver = None;
1473                self.jack_runtime = Some(Arc::new(UnsafeMutex::new(runtime)));
1474                self.publish_hw_infos(input_channels, output_channels, rate)
1475                    .await;
1476                for device in midi_inputs {
1477                    self.notify_clients(Ok(Action::OpenMidiInputDevice(device)))
1478                        .await;
1479                }
1480                for device in midi_outputs {
1481                    self.notify_clients(Ok(Action::OpenMidiOutputDevice(device)))
1482                        .await;
1483                }
1484                self.notify_clients(Ok(Action::OpenAudioDevice {
1485                    device: request.device.to_string(),
1486                    input_device: request.input_device.map(ToOwned::to_owned),
1487                    sample_rate_hz: request.sample_rate_hz,
1488                    bits: request.bits,
1489                    exclusive: request.exclusive,
1490                    period_frames: request.period_frames,
1491                    nperiods: request.nperiods,
1492                    sync_mode: request.sync_mode,
1493                    actual_period_frames: request.period_frames,
1494                    input_channels,
1495                    output_channels,
1496                    bytes_per_frame: 0,
1497                }))
1498                .await;
1499                self.awaiting_hwfinished = true;
1500            }
1501            Err(e) => {
1502                error!("Failed to open JACK runtime: {e}");
1503                self.notify_clients(Err(e)).await;
1504            }
1505        }
1506        Some(())
1507    }
1508
1509    fn hw_driver_input_audio_port(&self, from_port: usize) -> Option<Arc<AudioIO>> {
1510        self.hw_driver
1511            .as_ref()
1512            .and_then(|h| h.lock().input_port(from_port))
1513    }
1514
1515    fn hw_driver_output_audio_port(&self, to_port: usize) -> Option<Arc<AudioIO>> {
1516        self.hw_driver
1517            .as_ref()
1518            .and_then(|h| h.lock().output_port(to_port))
1519    }
1520
1521    #[cfg(unix)]
1522    fn jack_input_audio_port(&self, from_port: usize) -> Option<Arc<AudioIO>> {
1523        self.jack_runtime
1524            .as_ref()
1525            .and_then(|j| j.lock().input_audio_port(from_port))
1526    }
1527
1528    #[cfg(not(unix))]
1529    fn jack_input_audio_port(&self, _from_port: usize) -> Option<Arc<AudioIO>> {
1530        None
1531    }
1532
1533    #[cfg(unix)]
1534    fn jack_output_audio_port(&self, to_port: usize) -> Option<Arc<AudioIO>> {
1535        self.jack_runtime
1536            .as_ref()
1537            .and_then(|j| j.lock().output_audio_port(to_port))
1538    }
1539
1540    #[cfg(not(unix))]
1541    fn jack_output_audio_port(&self, _to_port: usize) -> Option<Arc<AudioIO>> {
1542        None
1543    }
1544
1545    fn normalize_transport_sample(&self, sample: usize) -> usize {
1546        if self.loop_enabled
1547            && let Some((loop_start, loop_end)) = self.loop_range_samples
1548            && loop_end > loop_start
1549            && sample >= loop_end
1550        {
1551            let loop_len = loop_end - loop_start;
1552            return loop_start + (sample - loop_start) % loop_len;
1553        }
1554        sample
1555    }
1556
1557    fn scheduled_loop_wrap_for_next_cycle(&self) -> Option<(usize, usize, usize)> {
1558        if !self.playing || !self.loop_enabled {
1559            return None;
1560        }
1561        let (loop_start, loop_end) = self.loop_range_samples?;
1562        if loop_end <= loop_start || self.transport_sample >= loop_end {
1563            return None;
1564        }
1565        let cycle_samples = self.current_cycle_samples();
1566        if cycle_samples == 0 {
1567            return None;
1568        }
1569        let next = self.transport_sample.saturating_add(cycle_samples);
1570        if next < loop_end {
1571            return None;
1572        }
1573        let after_frames = loop_end.saturating_sub(self.transport_sample);
1574        Some((
1575            after_frames,
1576            loop_start,
1577            self.normalize_transport_sample(next),
1578        ))
1579    }
1580
1581    #[cfg(unix)]
1582    fn jack_transport_sync_decision(
1583        current_playing: bool,
1584        current_sample: usize,
1585        jack_playing: bool,
1586        normalized_frame: usize,
1587        cycle_samples: usize,
1588    ) -> JackTransportSyncDecision {
1589        let play_sync = match (current_playing, jack_playing) {
1590            (false, true) => Some(JackTransportPlaySync::Start),
1591            (true, false) => Some(JackTransportPlaySync::Stop),
1592            _ => None,
1593        };
1594        let position_drift = normalized_frame.abs_diff(current_sample);
1595        let position_changed = normalized_frame != current_sample;
1596        let should_sync_position = position_changed
1597            && (!jack_playing || play_sync.is_some() || position_drift > cycle_samples.max(1));
1598
1599        JackTransportSyncDecision {
1600            play_sync,
1601            position_sync: should_sync_position.then_some(normalized_frame),
1602        }
1603    }
1604
1605    #[cfg(unix)]
1606    async fn sync_from_jack_transport(&mut self) {
1607        let Some(jack) = self.jack_runtime.clone() else {
1608            return;
1609        };
1610        let Ok((jack_state, jack_frame)) = jack.lock().transport_state_and_frame() else {
1611            return;
1612        };
1613
1614        let jack_playing = matches!(
1615            jack_state,
1616            jack::TransportState::Rolling | jack::TransportState::Starting
1617        );
1618        let normalized_frame = self.normalize_transport_sample(jack_frame);
1619        let decision = Self::jack_transport_sync_decision(
1620            self.playing,
1621            self.transport_sample,
1622            jack_playing,
1623            normalized_frame,
1624            self.current_cycle_samples(),
1625        );
1626
1627        if let Some(play_sync) = decision.play_sync {
1628            self.playing = matches!(play_sync, JackTransportPlaySync::Start);
1629            if matches!(play_sync, JackTransportPlaySync::Start) {
1630                self.transport_restart_pending = false;
1631                self.transport_panic_flush_pending = false;
1632                self.invalidate_track_cycle_state();
1633                self.notify_clients(Ok(Action::Play)).await;
1634            } else {
1635                self.transport_panic_flush_pending = false;
1636                self.transport_restart_pending = false;
1637                let panic_events = self.note_off_events_for_all_active_tracks();
1638                self.pending_hw_midi_out_events_by_device
1639                    .extend(panic_events);
1640                self.flush_recordings().await;
1641                self.notify_clients(Ok(Action::Stop)).await;
1642            }
1643        }
1644
1645        if let Some(sample) = decision.position_sync {
1646            self.transport_sample = sample;
1647            self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
1648                .await;
1649        }
1650    }
1651
1652    fn cycle_segments(&self, frames: usize) -> Vec<(usize, usize, usize)> {
1653        if frames == 0 {
1654            return vec![];
1655        }
1656        if !self.loop_enabled {
1657            return vec![(
1658                self.transport_sample,
1659                self.transport_sample.saturating_add(frames),
1660                0,
1661            )];
1662        }
1663        let Some((loop_start, loop_end)) = self.loop_range_samples else {
1664            return vec![(
1665                self.transport_sample,
1666                self.transport_sample.saturating_add(frames),
1667                0,
1668            )];
1669        };
1670        if loop_end <= loop_start {
1671            return vec![(
1672                self.transport_sample,
1673                self.transport_sample.saturating_add(frames),
1674                0,
1675            )];
1676        }
1677        let mut segments = Vec::new();
1678        let mut remaining = frames;
1679        let mut out_offset = 0usize;
1680        let mut current = self.transport_sample;
1681        while remaining > 0 {
1682            let take = loop_end.saturating_sub(current).min(remaining);
1683            if take == 0 {
1684                current = loop_start;
1685                continue;
1686            }
1687            segments.push((current, current.saturating_add(take), out_offset));
1688            out_offset = out_offset.saturating_add(take);
1689            remaining -= take;
1690            current = if remaining > 0 {
1691                loop_start
1692            } else {
1693                current.saturating_add(take)
1694            };
1695        }
1696        segments
1697    }
1698
1699    fn recording_segments_for_cycle(&self, frames: usize) -> Vec<(usize, usize, usize)> {
1700        let segments = self.cycle_segments(frames);
1701        let comp = self.hw_input_latency_frames;
1702        let segments: Vec<_> = if comp > 0 {
1703            segments
1704                .into_iter()
1705                .map(|(start, end, offset)| {
1706                    (start.saturating_sub(comp), end.saturating_sub(comp), offset)
1707                })
1708                .collect()
1709        } else {
1710            segments
1711        };
1712        if !self.punch_enabled {
1713            return segments;
1714        }
1715        let Some((punch_start, punch_end)) = self.punch_range_samples else {
1716            return vec![];
1717        };
1718        if punch_end <= punch_start {
1719            return vec![];
1720        }
1721        let mut clipped = Vec::new();
1722        for (segment_start, segment_end, frame_offset) in segments {
1723            let start = segment_start.max(punch_start);
1724            let end = segment_end.min(punch_end);
1725            if end <= start {
1726                continue;
1727            }
1728            let clipped_offset = frame_offset.saturating_add(start.saturating_sub(segment_start));
1729            clipped.push((start, end, clipped_offset));
1730        }
1731        clipped
1732    }
1733
1734    fn hw_device_info<D: HwDevice>(d: &D) -> HwDeviceInfo {
1735        (
1736            d.input_channels(),
1737            d.output_channels(),
1738            d.sample_rate() as usize,
1739            d.latency_ranges(),
1740        )
1741    }
1742
1743    async fn publish_hw_infos(
1744        &mut self,
1745        input_channels: usize,
1746        output_channels: usize,
1747        rate: usize,
1748    ) {
1749        self.notify_clients(Ok(Action::HWInfo {
1750            channels: input_channels,
1751            rate,
1752            input: true,
1753        }))
1754        .await;
1755        self.notify_clients(Ok(Action::HWInfo {
1756            channels: output_channels,
1757            rate,
1758            input: false,
1759        }))
1760        .await;
1761    }
1762
1763    #[cfg(unix)]
1764    fn jack_runtime_is_some(&self) -> bool {
1765        self.jack_runtime.is_some()
1766    }
1767
1768    #[cfg(not(unix))]
1769    fn jack_runtime_is_some(&self) -> bool {
1770        false
1771    }
1772
1773    fn can_schedule_hw_cycle(&self) -> bool {
1774        self.playing && (self.hw_worker.is_some() || self.jack_runtime_is_some())
1775    }
1776
1777    async fn ensure_hw_worker_running(&mut self) {
1778        if self.hw_worker.is_some() || self.hw_driver.is_none() {
1779            return;
1780        }
1781        let (tx, rx) = channel::<Message>(32);
1782        let hw = self.hw_driver.clone().unwrap();
1783        let midi_hub = self.midi_hub.clone();
1784        let tx_engine = self.tx.clone();
1785        let handler = tokio::spawn(async move {
1786            let worker = HwWorker::new(hw, midi_hub, rx, tx_engine);
1787            worker.work().await;
1788        });
1789        self.hw_worker = Some(WorkerData::new(tx, handler));
1790    }
1791
1792    fn build_hw_options(
1793        exclusive: bool,
1794        period_frames: usize,
1795        nperiods: usize,
1796        sync_mode: bool,
1797    ) -> HwOptions {
1798        HwOptions {
1799            exclusive,
1800            period_frames: period_frames.max(1).next_power_of_two(),
1801            nperiods: nperiods.max(1),
1802            sync_mode,
1803            ..Default::default()
1804        }
1805    }
1806
1807    async fn open_non_jack_audio_device(
1808        &mut self,
1809        device: &str,
1810        input_device: Option<&str>,
1811        sample_rate_hz: i32,
1812        bits: i32,
1813        hw_opts: HwOptions,
1814    ) -> Result<(), String> {
1815        let hw_profile_enabled = config::env_flag(config::HW_PROFILE_ENV);
1816        let d = Self::open_hw_driver(device, input_device, sample_rate_hz, bits, hw_opts)?;
1817        let (in_channels, out_channels, rate, (in_lat, out_lat)) = Self::hw_device_info(&d);
1818        if hw_profile_enabled {
1819            let label = Self::hw_profile_backend_label(device);
1820            error!(
1821                "{} config: exclusive={}, period={}, nperiods={}, ignore_hwbuf={}, sync_mode={}, in_latency_extra={}, out_latency_extra={}, input_range={:?}, output_range={:?}",
1822                label,
1823                hw_opts.exclusive,
1824                hw_opts.period_frames,
1825                hw_opts.nperiods,
1826                hw_opts.ignore_hwbuf,
1827                hw_opts.sync_mode,
1828                hw_opts.input_latency_frames,
1829                hw_opts.output_latency_frames,
1830                in_lat,
1831                out_lat
1832            );
1833        }
1834        self.hw_input_latency_frames = in_lat.0;
1835        self.hw_output_latency_frames = out_lat.0;
1836        #[cfg(unix)]
1837        {
1838            self.jack_runtime = None;
1839        }
1840        self.hw_driver = Some(Arc::new(UnsafeMutex::new(d)));
1841        self.publish_hw_infos(in_channels, out_channels, rate).await;
1842        Ok(())
1843    }
1844
1845    async fn finalize_open_audio_device(&mut self) {
1846        self.maybe_start_freebsd_sync_group();
1847        if self.metronome_enabled {
1848            self.ensure_metronome_track().await;
1849        }
1850        if self.hw_worker.is_none() && self.hw_driver.is_some() {
1851            self.ensure_hw_worker_running().await;
1852            self.request_hw_cycle().await;
1853        }
1854        self.open_discovered_midi_hw_devices().await;
1855    }
1856
1857    fn hw_input_audio_port(&self, from_port: usize) -> Option<Arc<AudioIO>> {
1858        self.hw_driver_input_audio_port(from_port)
1859            .or_else(|| self.jack_input_audio_port(from_port))
1860    }
1861
1862    fn hw_output_audio_port(&self, to_port: usize) -> Option<Arc<AudioIO>> {
1863        self.hw_driver_output_audio_port(to_port)
1864            .or_else(|| self.jack_output_audio_port(to_port))
1865    }
1866
1867    fn all_hw_output_audio_ports(&self) -> Vec<Arc<AudioIO>> {
1868        if let Some(driver) = &self.hw_driver {
1869            let count = driver.lock().output_channels();
1870            return (0..count)
1871                .filter_map(|idx| self.hw_driver_output_audio_port(idx))
1872                .collect();
1873        }
1874        #[cfg(unix)]
1875        if let Some(jack) = &self.jack_runtime {
1876            return jack.lock().audio_outs();
1877        }
1878        Vec::new()
1879    }
1880
1881    fn all_hw_input_audio_ports(&self) -> Vec<Arc<AudioIO>> {
1882        if let Some(driver) = &self.hw_driver {
1883            let count = driver.lock().input_channels();
1884            return (0..count)
1885                .filter_map(|idx| self.hw_driver_input_audio_port(idx))
1886                .collect();
1887        }
1888        #[cfg(unix)]
1889        if let Some(jack) = &self.jack_runtime {
1890            return jack.lock().audio_ins();
1891        }
1892        Vec::new()
1893    }
1894
1895    #[cfg(unix)]
1896    fn audio_ports_connected(source: &Arc<AudioIO>, target: &Arc<AudioIO>) -> bool {
1897        source
1898            .connections
1899            .lock()
1900            .iter()
1901            .any(|conn| Arc::ptr_eq(conn, target))
1902    }
1903
1904    fn resolve_audio_route_ports(
1905        &self,
1906        from_track: &str,
1907        from_port: usize,
1908        to_track: &str,
1909        to_port: usize,
1910    ) -> (Option<Arc<AudioIO>>, Option<Arc<AudioIO>>) {
1911        let state = self.state.lock();
1912        let from_is_child_of_to = state
1913            .tracks
1914            .get(from_track)
1915            .and_then(|t| t.lock().parent_track.as_deref())
1916            == Some(to_track);
1917        let to_is_child_of_from = state
1918            .tracks
1919            .get(to_track)
1920            .and_then(|t| t.lock().parent_track.as_deref())
1921            == Some(from_track);
1922
1923        let from_audio_io = if from_track == "hw:in" {
1924            self.hw_input_audio_port(from_port)
1925        } else {
1926            state.tracks.get(from_track).and_then(|t| {
1927                let t = t.lock();
1928                if t.is_folder {
1929                    if to_is_child_of_from {
1930                        // Folder input -> child input.
1931                        t.audio.ins.get(from_port).cloned()
1932                    } else {
1933                        // Folder output -> external target.
1934                        t.audio.outs.get(from_port).cloned()
1935                    }
1936                } else {
1937                    t.audio.outs.get(from_port).cloned()
1938                }
1939            })
1940        };
1941        let to_audio_io = if to_track == "hw:out" {
1942            self.hw_output_audio_port(to_port)
1943        } else {
1944            state.tracks.get(to_track).and_then(|t| {
1945                let t = t.lock();
1946                if t.is_folder {
1947                    if from_is_child_of_to {
1948                        // Child output -> folder output.
1949                        t.audio.outs.get(to_port).cloned()
1950                    } else {
1951                        // External source -> folder input.
1952                        t.audio.ins.get(to_port).cloned()
1953                    }
1954                } else {
1955                    t.audio.ins.get(to_port).cloned()
1956                }
1957            })
1958        };
1959        (from_audio_io, to_audio_io)
1960    }
1961
1962    async fn disconnect_audio_route_and_notify(&mut self, action: Action) -> Result<(), String> {
1963        let Action::Disconnect {
1964            from_track,
1965            from_port,
1966            to_track,
1967            to_port,
1968            kind,
1969        } = &action
1970        else {
1971            return Err("disconnect_audio_route_and_notify requires Disconnect action".to_string());
1972        };
1973        if *kind != Kind::Audio {
1974            return Err("disconnect_audio_route_and_notify only supports audio routes".to_string());
1975        }
1976        let (from_audio_io, to_audio_io) =
1977            self.resolve_audio_route_ports(from_track, *from_port, to_track, *to_port);
1978        match (from_audio_io, to_audio_io) {
1979            (Some(source), Some(target)) => {
1980                crate::audio::io::AudioIO::disconnect(&source, &target)
1981                    .map_err(|e| format!("Disconnect failed: {e}"))?;
1982                self.notify_clients(Ok(action)).await;
1983                Ok(())
1984            }
1985            _ => Err(format!(
1986                "Disconnect failed: Port not found ({} -> {})",
1987                from_track, to_track
1988            )),
1989        }
1990    }
1991
1992    #[cfg(unix)]
1993    fn disconnect_actions_for_removed_hw_input(
1994        &self,
1995        removed_port: usize,
1996        removed_io: &Arc<AudioIO>,
1997    ) -> Vec<Action> {
1998        let mut actions = Vec::new();
1999        {
2000            let state = self.state.lock();
2001            for (track_name, track) in &state.tracks {
2002                let track = track.lock();
2003                for (to_port, target) in track.audio.ins.iter().enumerate() {
2004                    if Self::audio_ports_connected(removed_io, target) {
2005                        actions.push(Action::Disconnect {
2006                            from_track: "hw:in".to_string(),
2007                            from_port: removed_port,
2008                            to_track: track_name.clone(),
2009                            to_port,
2010                            kind: Kind::Audio,
2011                        });
2012                    }
2013                }
2014            }
2015        }
2016        for (to_port, target) in self.all_hw_output_audio_ports().into_iter().enumerate() {
2017            if Self::audio_ports_connected(removed_io, &target) {
2018                actions.push(Action::Disconnect {
2019                    from_track: "hw:in".to_string(),
2020                    from_port: removed_port,
2021                    to_track: "hw:out".to_string(),
2022                    to_port,
2023                    kind: Kind::Audio,
2024                });
2025            }
2026        }
2027        actions
2028    }
2029
2030    #[cfg(unix)]
2031    fn disconnect_actions_for_removed_hw_output(
2032        &self,
2033        removed_port: usize,
2034        removed_io: &Arc<AudioIO>,
2035    ) -> Vec<Action> {
2036        let mut actions = Vec::new();
2037        {
2038            let state = self.state.lock();
2039            for (track_name, track) in &state.tracks {
2040                let track = track.lock();
2041                for (from_port, source) in track.audio.outs.iter().enumerate() {
2042                    if Self::audio_ports_connected(source, removed_io) {
2043                        actions.push(Action::Disconnect {
2044                            from_track: track_name.clone(),
2045                            from_port,
2046                            to_track: "hw:out".to_string(),
2047                            to_port: removed_port,
2048                            kind: Kind::Audio,
2049                        });
2050                    }
2051                }
2052            }
2053        }
2054        #[cfg(unix)]
2055        if let Some(jack) = &self.jack_runtime {
2056            for (from_port, source) in jack.lock().audio_ins().into_iter().enumerate() {
2057                if Self::audio_ports_connected(&source, removed_io) {
2058                    actions.push(Action::Disconnect {
2059                        from_track: "hw:in".to_string(),
2060                        from_port,
2061                        to_track: "hw:out".to_string(),
2062                        to_port: removed_port,
2063                        kind: Kind::Audio,
2064                    });
2065                }
2066            }
2067        }
2068        actions
2069    }
2070
2071    #[cfg(unix)]
2072    fn reindex_notifications_for_removed_hw_input(&self, removed_port: usize) -> Vec<Action> {
2073        let mut actions = Vec::new();
2074        #[cfg(unix)]
2075        if let Some(jack) = &self.jack_runtime {
2076            let jack = jack.lock();
2077            for from_port in (removed_port + 1)..jack.input_channels() {
2078                let Some(source) = jack.input_audio_port(from_port) else {
2079                    continue;
2080                };
2081                {
2082                    let state = self.state.lock();
2083                    for (track_name, track) in &state.tracks {
2084                        let track = track.lock();
2085                        for (to_port, target) in track.audio.ins.iter().enumerate() {
2086                            if Self::audio_ports_connected(&source, target) {
2087                                actions.push(Action::Disconnect {
2088                                    from_track: "hw:in".to_string(),
2089                                    from_port,
2090                                    to_track: track_name.clone(),
2091                                    to_port,
2092                                    kind: Kind::Audio,
2093                                });
2094                                actions.push(Action::Connect {
2095                                    from_track: "hw:in".to_string(),
2096                                    from_port: from_port - 1,
2097                                    to_track: track_name.clone(),
2098                                    to_port,
2099                                    kind: Kind::Audio,
2100                                });
2101                            }
2102                        }
2103                    }
2104                }
2105                for (to_port, target) in self.all_hw_output_audio_ports().into_iter().enumerate() {
2106                    if Self::audio_ports_connected(&source, &target) {
2107                        actions.push(Action::Disconnect {
2108                            from_track: "hw:in".to_string(),
2109                            from_port,
2110                            to_track: "hw:out".to_string(),
2111                            to_port,
2112                            kind: Kind::Audio,
2113                        });
2114                        actions.push(Action::Connect {
2115                            from_track: "hw:in".to_string(),
2116                            from_port: from_port - 1,
2117                            to_track: "hw:out".to_string(),
2118                            to_port,
2119                            kind: Kind::Audio,
2120                        });
2121                    }
2122                }
2123            }
2124        }
2125        actions
2126    }
2127
2128    #[cfg(unix)]
2129    fn reindex_notifications_for_removed_hw_output(&self, removed_port: usize) -> Vec<Action> {
2130        let mut actions = Vec::new();
2131        #[cfg(unix)]
2132        if let Some(jack) = &self.jack_runtime {
2133            let jack = jack.lock();
2134            for to_port in (removed_port + 1)..jack.output_channels() {
2135                let Some(target) = jack.output_audio_port(to_port) else {
2136                    continue;
2137                };
2138                {
2139                    let state = self.state.lock();
2140                    for (track_name, track) in &state.tracks {
2141                        let track = track.lock();
2142                        for (from_port, source) in track.audio.outs.iter().enumerate() {
2143                            if Self::audio_ports_connected(source, &target) {
2144                                actions.push(Action::Disconnect {
2145                                    from_track: track_name.clone(),
2146                                    from_port,
2147                                    to_track: "hw:out".to_string(),
2148                                    to_port,
2149                                    kind: Kind::Audio,
2150                                });
2151                                actions.push(Action::Connect {
2152                                    from_track: track_name.clone(),
2153                                    from_port,
2154                                    to_track: "hw:out".to_string(),
2155                                    to_port: to_port - 1,
2156                                    kind: Kind::Audio,
2157                                });
2158                            }
2159                        }
2160                    }
2161                }
2162                for (from_port, source) in jack.audio_ins().into_iter().enumerate() {
2163                    if Self::audio_ports_connected(&source, &target) {
2164                        actions.push(Action::Disconnect {
2165                            from_track: "hw:in".to_string(),
2166                            from_port,
2167                            to_track: "hw:out".to_string(),
2168                            to_port,
2169                            kind: Kind::Audio,
2170                        });
2171                        actions.push(Action::Connect {
2172                            from_track: "hw:in".to_string(),
2173                            from_port,
2174                            to_track: "hw:out".to_string(),
2175                            to_port: to_port - 1,
2176                            kind: Kind::Audio,
2177                        });
2178                    }
2179                }
2180            }
2181        }
2182        actions
2183    }
2184
2185    fn midi_hw_in_device(track: &str) -> Option<&str> {
2186        track.strip_prefix("midi:hw:in:")
2187    }
2188
2189    fn midi_hw_out_device(track: &str) -> Option<&str> {
2190        track.strip_prefix("midi:hw:out:")
2191    }
2192
2193    fn midi_binding_matches(
2194        a: &crate::message::MidiLearnBinding,
2195        b: &crate::message::MidiLearnBinding,
2196    ) -> bool {
2197        if a.channel != b.channel || a.cc != b.cc {
2198            return false;
2199        }
2200        match (&a.device, &b.device) {
2201            (Some(ad), Some(bd)) => ad == bd,
2202            _ => true,
2203        }
2204    }
2205
2206    fn midi_learn_slot_conflicts(
2207        &self,
2208        binding: &crate::message::MidiLearnBinding,
2209        ignore: Option<MidiLearnSlot>,
2210    ) -> Vec<String> {
2211        let mut conflicts = Vec::<String>::new();
2212        let state = self.state.lock();
2213        let mut push_conflict = |slot: MidiLearnSlot, label: String| {
2214            if ignore.as_ref().is_some_and(|i| i == &slot) {
2215                return;
2216            }
2217            conflicts.push(label);
2218        };
2219        let check_global =
2220            |current: &Option<crate::message::MidiLearnBinding>,
2221             target: crate::message::GlobalMidiLearnTarget,
2222             label: &str,
2223             push_conflict: &mut dyn FnMut(MidiLearnSlot, String)| {
2224                if let Some(existing) = current
2225                    && Self::midi_binding_matches(binding, existing)
2226                {
2227                    push_conflict(MidiLearnSlot::Global(target), format!("Global {label}"));
2228                }
2229            };
2230        check_global(
2231            &self.global_midi_learn_play_pause,
2232            crate::message::GlobalMidiLearnTarget::PlayPause,
2233            "PlayPause",
2234            &mut push_conflict,
2235        );
2236        check_global(
2237            &self.global_midi_learn_stop,
2238            crate::message::GlobalMidiLearnTarget::Stop,
2239            "Stop",
2240            &mut push_conflict,
2241        );
2242        check_global(
2243            &self.global_midi_learn_record_toggle,
2244            crate::message::GlobalMidiLearnTarget::RecordToggle,
2245            "RecordToggle",
2246            &mut push_conflict,
2247        );
2248        for (track_name, track) in state.tracks.iter() {
2249            let t = track.lock();
2250            let mut check_track = |current: &Option<crate::message::MidiLearnBinding>,
2251                                   target: crate::message::TrackMidiLearnTarget,
2252                                   label: &str| {
2253                if let Some(existing) = current
2254                    && Self::midi_binding_matches(binding, existing)
2255                {
2256                    push_conflict(
2257                        MidiLearnSlot::Track(track_name.clone(), target),
2258                        format!("{track_name} {label}"),
2259                    );
2260                }
2261            };
2262            check_track(
2263                &t.midi_learn_volume,
2264                crate::message::TrackMidiLearnTarget::Volume,
2265                "Volume",
2266            );
2267            check_track(
2268                &t.midi_learn_balance,
2269                crate::message::TrackMidiLearnTarget::Balance,
2270                "Balance",
2271            );
2272            check_track(
2273                &t.midi_learn_mute,
2274                crate::message::TrackMidiLearnTarget::Mute,
2275                "Mute",
2276            );
2277            check_track(
2278                &t.midi_learn_solo,
2279                crate::message::TrackMidiLearnTarget::Solo,
2280                "Solo",
2281            );
2282            check_track(
2283                &t.midi_learn_arm,
2284                crate::message::TrackMidiLearnTarget::Arm,
2285                "Arm",
2286            );
2287            check_track(
2288                &t.midi_learn_input_monitor,
2289                crate::message::TrackMidiLearnTarget::InputMonitor,
2290                "InputMonitor",
2291            );
2292            check_track(
2293                &t.midi_learn_disk_monitor,
2294                crate::message::TrackMidiLearnTarget::DiskMonitor,
2295                "DiskMonitor",
2296            );
2297        }
2298        for (key, existing) in &self.session_midi_learn_slots {
2299            if Self::midi_binding_matches(binding, existing) {
2300                push_conflict(
2301                    MidiLearnSlot::Session(crate::message::SessionMidiLearnTarget::Slot {
2302                        track_name: key.0.clone(),
2303                        scene_index: key.1,
2304                    }),
2305                    format!("{} Slot {}", key.0, key.1 + 1),
2306                );
2307            }
2308        }
2309        for (scene_index, existing) in &self.session_midi_learn_scenes {
2310            if Self::midi_binding_matches(binding, existing) {
2311                push_conflict(
2312                    MidiLearnSlot::Session(crate::message::SessionMidiLearnTarget::Scene(
2313                        *scene_index,
2314                    )),
2315                    format!("Scene {}", scene_index + 1),
2316                );
2317            }
2318        }
2319        for (track_name, existing) in &self.session_midi_learn_stop_track {
2320            if Self::midi_binding_matches(binding, existing) {
2321                push_conflict(
2322                    MidiLearnSlot::Session(crate::message::SessionMidiLearnTarget::StopTrack(
2323                        track_name.clone(),
2324                    )),
2325                    format!("{track_name} Stop"),
2326                );
2327            }
2328        }
2329        if let Some(existing) = self.session_midi_learn_stop_all.as_ref()
2330            && Self::midi_binding_matches(binding, existing)
2331        {
2332            push_conflict(
2333                MidiLearnSlot::Session(crate::message::SessionMidiLearnTarget::StopAll),
2334                "Stop All Clips".to_string(),
2335            );
2336        }
2337        conflicts
2338    }
2339
2340    async fn handle_incoming_hw_cc(&mut self, device: &str, channel: u8, cc: u8, value: u8) {
2341        let gate_key = (device.to_string(), channel, cc);
2342        let high = value >= 64;
2343        let prev_high = self.midi_cc_gate.get(&gate_key).copied().unwrap_or(false);
2344        self.midi_cc_gate.insert(gate_key, high);
2345        let rising = high && !prev_high;
2346
2347        if let Some((track_name, target, armed_device)) = self.pending_midi_learn.clone() {
2348            let binding = crate::message::MidiLearnBinding {
2349                device: armed_device.or(Some(device.to_string())),
2350                channel,
2351                cc,
2352            };
2353            let conflicts = self.midi_learn_slot_conflicts(
2354                &binding,
2355                Some(MidiLearnSlot::Track(track_name.clone(), target)),
2356            );
2357            if !conflicts.is_empty() {
2358                self.pending_midi_learn = None;
2359                self.notify_clients(Err(format!(
2360                    "MIDI learn conflict for '{}' {:?}: {}",
2361                    track_name,
2362                    target,
2363                    conflicts.join(", ")
2364                )))
2365                .await;
2366                return;
2367            }
2368            if let Some(track) = self.state.lock().tracks.get(&track_name) {
2369                match target {
2370                    crate::message::TrackMidiLearnTarget::Volume => {
2371                        track.lock().midi_learn_volume = Some(binding.clone());
2372                    }
2373                    crate::message::TrackMidiLearnTarget::Balance => {
2374                        track.lock().midi_learn_balance = Some(binding.clone());
2375                    }
2376                    crate::message::TrackMidiLearnTarget::Mute => {
2377                        track.lock().midi_learn_mute = Some(binding.clone());
2378                    }
2379                    crate::message::TrackMidiLearnTarget::Solo => {
2380                        track.lock().midi_learn_solo = Some(binding.clone());
2381                    }
2382                    crate::message::TrackMidiLearnTarget::Arm => {
2383                        track.lock().midi_learn_arm = Some(binding.clone());
2384                    }
2385                    crate::message::TrackMidiLearnTarget::InputMonitor => {
2386                        track.lock().midi_learn_input_monitor = Some(binding.clone());
2387                    }
2388                    crate::message::TrackMidiLearnTarget::DiskMonitor => {
2389                        track.lock().midi_learn_disk_monitor = Some(binding.clone());
2390                    }
2391                }
2392                self.pending_midi_learn = None;
2393                self.notify_clients(Ok(Action::TrackSetMidiLearnBinding {
2394                    track_name: track_name.clone(),
2395                    target,
2396                    binding: Some(binding),
2397                }))
2398                .await;
2399            } else {
2400                self.pending_midi_learn = None;
2401            }
2402        }
2403        if let Some(target) = self.pending_global_midi_learn.take() {
2404            let binding = crate::message::MidiLearnBinding {
2405                device: Some(device.to_string()),
2406                channel,
2407                cc,
2408            };
2409            let conflicts =
2410                self.midi_learn_slot_conflicts(&binding, Some(MidiLearnSlot::Global(target)));
2411            if !conflicts.is_empty() {
2412                self.notify_clients(Err(format!(
2413                    "Global MIDI learn conflict for {:?}: {}",
2414                    target,
2415                    conflicts.join(", ")
2416                )))
2417                .await;
2418                return;
2419            }
2420            match target {
2421                crate::message::GlobalMidiLearnTarget::PlayPause => {
2422                    self.global_midi_learn_play_pause = Some(binding.clone());
2423                }
2424                crate::message::GlobalMidiLearnTarget::Stop => {
2425                    self.global_midi_learn_stop = Some(binding.clone());
2426                }
2427                crate::message::GlobalMidiLearnTarget::RecordToggle => {
2428                    self.global_midi_learn_record_toggle = Some(binding.clone());
2429                }
2430            }
2431            self.notify_clients(Ok(Action::SetGlobalMidiLearnBinding {
2432                target,
2433                binding: Some(binding),
2434            }))
2435            .await;
2436        }
2437        if let Some(target) = self.pending_session_midi_learn.take() {
2438            let binding = crate::message::MidiLearnBinding {
2439                device: Some(device.to_string()),
2440                channel,
2441                cc,
2442            };
2443            let conflicts = self
2444                .midi_learn_slot_conflicts(&binding, Some(MidiLearnSlot::Session(target.clone())));
2445            if !conflicts.is_empty() {
2446                self.notify_clients(Err(format!(
2447                    "Session MIDI learn conflict for {:?}: {}",
2448                    target,
2449                    conflicts.join(", ")
2450                )))
2451                .await;
2452                return;
2453            }
2454            match target {
2455                crate::message::SessionMidiLearnTarget::Slot {
2456                    ref track_name,
2457                    scene_index,
2458                } => {
2459                    self.session_midi_learn_slots
2460                        .insert((track_name.clone(), scene_index), binding.clone());
2461                }
2462                crate::message::SessionMidiLearnTarget::Scene(scene_index) => {
2463                    self.session_midi_learn_scenes
2464                        .insert(scene_index, binding.clone());
2465                }
2466                crate::message::SessionMidiLearnTarget::StopTrack(ref track_name) => {
2467                    self.session_midi_learn_stop_track
2468                        .insert(track_name.clone(), binding.clone());
2469                }
2470                crate::message::SessionMidiLearnTarget::StopAll => {
2471                    self.session_midi_learn_stop_all = Some(binding.clone());
2472                }
2473            }
2474            self.notify_clients(Ok(Action::SetSessionMidiLearnBinding {
2475                target,
2476                binding: Some(binding),
2477            }))
2478            .await;
2479        }
2480
2481        let mut mapped_actions = Vec::<Action>::new();
2482        for (track_name, track) in self.state.lock().tracks.iter() {
2483            let t = track.lock();
2484            if let Some(binding) = t.midi_learn_volume.as_ref() {
2485                let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2486                if device_matches && binding.channel == channel && binding.cc == cc {
2487                    let level = -90.0 + (value as f32 / 127.0) * 110.0;
2488                    mapped_actions.push(Action::TrackLevel(track_name.clone(), level));
2489                }
2490            }
2491            if let Some(binding) = t.midi_learn_balance.as_ref() {
2492                let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2493                if device_matches && binding.channel == channel && binding.cc == cc {
2494                    let balance = (value as f32 / 127.0) * 2.0 - 1.0;
2495                    mapped_actions.push(Action::TrackBalance(track_name.clone(), balance));
2496                }
2497            }
2498            if let Some(binding) = t.midi_learn_mute.as_ref() {
2499                let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2500                if device_matches && binding.channel == channel && binding.cc == cc {
2501                    let wanted = value >= 64;
2502                    if t.muted != wanted {
2503                        mapped_actions.push(Action::TrackToggleMute(track_name.clone()));
2504                    }
2505                }
2506            }
2507            if let Some(binding) = t.midi_learn_solo.as_ref() {
2508                let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2509                if device_matches && binding.channel == channel && binding.cc == cc {
2510                    let wanted = value >= 64;
2511                    if t.soloed != wanted {
2512                        mapped_actions.push(Action::TrackToggleSolo(track_name.clone()));
2513                    }
2514                }
2515            }
2516            if let Some(binding) = t.midi_learn_arm.as_ref() {
2517                let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2518                if device_matches && binding.channel == channel && binding.cc == cc {
2519                    let wanted = value >= 64;
2520                    if t.armed != wanted {
2521                        mapped_actions.push(Action::TrackToggleArm(track_name.clone()));
2522                    }
2523                }
2524            }
2525            if let Some(binding) = t.midi_learn_input_monitor.as_ref() {
2526                let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2527                if device_matches && binding.channel == channel && binding.cc == cc {
2528                    let wanted = value >= 64;
2529                    if t.input_monitor.first() != Some(&wanted) {
2530                        mapped_actions.push(Action::TrackToggleInputMonitor {
2531                            track_name: track_name.clone(),
2532                            lane: 0,
2533                        });
2534                    }
2535                }
2536            }
2537            if let Some(binding) = t.midi_learn_disk_monitor.as_ref() {
2538                let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2539                if device_matches && binding.channel == channel && binding.cc == cc {
2540                    let wanted = value >= 64;
2541                    if t.disk_monitor.first() != Some(&wanted) {
2542                        mapped_actions.push(Action::TrackToggleDiskMonitor {
2543                            track_name: track_name.clone(),
2544                            lane: 0,
2545                        });
2546                    }
2547                }
2548            }
2549        }
2550        let device_matches =
2551            |binding: &crate::message::MidiLearnBinding| binding.device.as_deref() == Some(device);
2552        let mut mapped_global_actions = Vec::<Action>::new();
2553        if let Some(binding) = self.global_midi_learn_play_pause.as_ref()
2554            && device_matches(binding)
2555            && binding.channel == channel
2556            && binding.cc == cc
2557            && rising
2558        {
2559            mapped_global_actions.push(if self.playing {
2560                Action::Stop
2561            } else {
2562                Action::Play
2563            });
2564        }
2565        if let Some(binding) = self.global_midi_learn_stop.as_ref()
2566            && device_matches(binding)
2567            && binding.channel == channel
2568            && binding.cc == cc
2569            && rising
2570            && self.playing
2571        {
2572            mapped_global_actions.push(Action::Stop);
2573        }
2574        if let Some(binding) = self.global_midi_learn_record_toggle.as_ref()
2575            && device_matches(binding)
2576            && binding.channel == channel
2577            && binding.cc == cc
2578            && rising
2579        {
2580            mapped_global_actions.push(Action::SetRecordEnabled(!self.record_enabled));
2581        }
2582        if rising {
2583            for (key, binding) in &self.session_midi_learn_slots {
2584                let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2585                if device_matches && binding.channel == channel && binding.cc == cc {
2586                    mapped_global_actions.push(Action::SessionMidiLearnTriggered {
2587                        target: crate::message::SessionMidiLearnTarget::Slot {
2588                            track_name: key.0.clone(),
2589                            scene_index: key.1,
2590                        },
2591                    });
2592                }
2593            }
2594            for (scene_index, binding) in &self.session_midi_learn_scenes {
2595                let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2596                if device_matches && binding.channel == channel && binding.cc == cc {
2597                    mapped_global_actions.push(Action::SessionMidiLearnTriggered {
2598                        target: crate::message::SessionMidiLearnTarget::Scene(*scene_index),
2599                    });
2600                }
2601            }
2602            for (track_name, binding) in &self.session_midi_learn_stop_track {
2603                let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2604                if device_matches && binding.channel == channel && binding.cc == cc {
2605                    mapped_global_actions.push(Action::SessionMidiLearnTriggered {
2606                        target: crate::message::SessionMidiLearnTarget::StopTrack(
2607                            track_name.clone(),
2608                        ),
2609                    });
2610                }
2611            }
2612            if let Some(binding) = self.session_midi_learn_stop_all.as_ref() {
2613                let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2614                if device_matches && binding.channel == channel && binding.cc == cc {
2615                    mapped_global_actions.push(Action::SessionMidiLearnTriggered {
2616                        target: crate::message::SessionMidiLearnTarget::StopAll,
2617                    });
2618                }
2619            }
2620        }
2621        for action in mapped_actions {
2622            match action {
2623                Action::TrackLevel(ref track_name, level) => {
2624                    if let Some(track) = self.state.lock().tracks.get(track_name) {
2625                        track.lock().set_level(level);
2626                        self.notify_clients(Ok(Action::TrackLevel(track_name.clone(), level)))
2627                            .await;
2628                    }
2629                }
2630                Action::TrackBalance(ref track_name, balance) => {
2631                    if let Some(track) = self.state.lock().tracks.get(track_name) {
2632                        track.lock().set_balance(balance);
2633                        self.notify_clients(Ok(Action::TrackBalance(track_name.clone(), balance)))
2634                            .await;
2635                    }
2636                }
2637                Action::TrackToggleMute(ref track_name) => {
2638                    if let Some(track) = self.state.lock().tracks.get(track_name) {
2639                        track.lock().mute();
2640                        self.notify_clients(Ok(Action::TrackToggleMute(track_name.clone())))
2641                            .await;
2642                    }
2643                }
2644                Action::TrackTogglePhase(ref track_name) => {
2645                    if let Some(track) = self.state.lock().tracks.get(track_name) {
2646                        track.lock().invert_phase();
2647                        self.notify_clients(Ok(Action::TrackTogglePhase(track_name.clone())))
2648                            .await;
2649                    }
2650                }
2651                Action::TrackToggleSolo(ref track_name) => {
2652                    if let Some(track) = self.state.lock().tracks.get(track_name) {
2653                        track.lock().solo();
2654                        self.notify_clients(Ok(Action::TrackToggleSolo(track_name.clone())))
2655                            .await;
2656                    }
2657                }
2658                Action::TrackToggleMaster(ref track_name) => {
2659                    if let Some(track) = self.state.lock().tracks.get(track_name) {
2660                        let can_toggle = {
2661                            let t = track.lock();
2662                            t.is_master || !t.is_folder
2663                        };
2664                        if can_toggle {
2665                            track.lock().toggle_master();
2666                            self.notify_clients(Ok(Action::TrackToggleMaster(track_name.clone())))
2667                                .await;
2668                        }
2669                    }
2670                }
2671                Action::TrackToggleArm(ref track_name) => {
2672                    if let Some(track) = self.state.lock().tracks.get(track_name) {
2673                        track.lock().arm();
2674                        self.notify_clients(Ok(Action::TrackToggleArm(track_name.clone())))
2675                            .await;
2676                    }
2677                }
2678                Action::TrackToggleInputMonitor {
2679                    ref track_name,
2680                    lane,
2681                } => {
2682                    if let Some(track) = self.state.lock().tracks.get(track_name) {
2683                        track.lock().toggle_input_monitor(lane);
2684                        self.notify_clients(Ok(Action::TrackToggleInputMonitor {
2685                            track_name: track_name.clone(),
2686                            lane,
2687                        }))
2688                        .await;
2689                    }
2690                }
2691                Action::TrackToggleDiskMonitor {
2692                    ref track_name,
2693                    lane,
2694                } => {
2695                    if let Some(track) = self.state.lock().tracks.get(track_name) {
2696                        track.lock().toggle_disk_monitor(lane);
2697                        self.notify_clients(Ok(Action::TrackToggleDiskMonitor {
2698                            track_name: track_name.clone(),
2699                            lane,
2700                        }))
2701                        .await;
2702                    }
2703                }
2704                Action::TrackToggleMidiInputMonitor {
2705                    ref track_name,
2706                    lane,
2707                } => {
2708                    if let Some(track) = self.state.lock().tracks.get(track_name) {
2709                        track.lock().toggle_midi_input_monitor(lane);
2710                        self.notify_clients(Ok(Action::TrackToggleMidiInputMonitor {
2711                            track_name: track_name.clone(),
2712                            lane,
2713                        }))
2714                        .await;
2715                    }
2716                }
2717                Action::TrackToggleMidiDiskMonitor {
2718                    ref track_name,
2719                    lane,
2720                } => {
2721                    if let Some(track) = self.state.lock().tracks.get(track_name) {
2722                        track.lock().toggle_midi_disk_monitor(lane);
2723                        self.notify_clients(Ok(Action::TrackToggleMidiDiskMonitor {
2724                            track_name: track_name.clone(),
2725                            lane,
2726                        }))
2727                        .await;
2728                    }
2729                }
2730                _ => {}
2731            }
2732        }
2733        for action in mapped_global_actions {
2734            self.handle_request_inner(action, false).await;
2735        }
2736    }
2737
2738    fn upstream_audio_track_names(
2739        &self,
2740        seeds: &std::collections::HashSet<String>,
2741    ) -> std::collections::HashSet<String> {
2742        let state = self.state.lock();
2743        let mut output_to_track: std::collections::HashMap<
2744            *const crate::audio::io::AudioIO,
2745            String,
2746        > = std::collections::HashMap::new();
2747        for (name, track) in &state.tracks {
2748            let t = track.lock();
2749            for out in &t.audio.outs {
2750                output_to_track.insert(std::sync::Arc::as_ptr(out), name.clone());
2751            }
2752        }
2753        let mut upstream = std::collections::HashSet::new();
2754        let mut to_process: Vec<String> = seeds.iter().cloned().collect();
2755        let mut processed = std::collections::HashSet::new();
2756        while let Some(target_name) = to_process.pop() {
2757            if !processed.insert(target_name.clone()) {
2758                continue;
2759            }
2760            if let Some(target_track) = state.tracks.get(&target_name) {
2761                let tt = target_track.lock();
2762                for input in &tt.audio.ins {
2763                    for conn in input.connections.lock().iter() {
2764                        let conn_ptr = std::sync::Arc::as_ptr(conn);
2765                        if let Some(source_name) = output_to_track.get(&conn_ptr)
2766                            && source_name != &target_name
2767                            && !seeds.contains(source_name)
2768                        {
2769                            upstream.insert(source_name.clone());
2770                            to_process.push(source_name.clone());
2771                        }
2772                    }
2773                }
2774            }
2775        }
2776        upstream
2777    }
2778
2779    fn is_track_in_soloed_folder(
2780        &self,
2781        track: &Track,
2782        tracks: &std::collections::HashMap<String, Arc<UnsafeMutex<Box<Track>>>>,
2783    ) -> bool {
2784        let mut current = track.parent_track.as_deref();
2785        while let Some(parent_name) = current {
2786            if let Some(parent) = tracks.get(parent_name) {
2787                let p = parent.lock();
2788                if p.soloed {
2789                    return true;
2790                }
2791                current = p.parent_track.as_deref();
2792            } else {
2793                break;
2794            }
2795        }
2796        false
2797    }
2798
2799    fn folder_has_soloed_descendant(
2800        &self,
2801        folder_name: &str,
2802        tracks: &std::collections::HashMap<String, Arc<UnsafeMutex<Box<Track>>>>,
2803    ) -> bool {
2804        for track in tracks.values() {
2805            let t = track.lock();
2806            if !t.soloed {
2807                continue;
2808            }
2809            let mut current = t.parent_track.as_deref();
2810            while let Some(parent_name) = current {
2811                if parent_name == folder_name {
2812                    return true;
2813                }
2814                if let Some(parent) = tracks.get(parent_name) {
2815                    current = parent.lock().parent_track.as_deref();
2816                } else {
2817                    break;
2818                }
2819            }
2820        }
2821        false
2822    }
2823
2824    fn refresh_realtime_infection(&self) {
2825        let state = self.state.lock();
2826        let live_seeds: std::collections::HashSet<String> = state
2827            .tracks
2828            .iter()
2829            .filter_map(|(name, track)| {
2830                let t = track.lock();
2831                if t.armed && t.input_monitor.iter().any(|&m| m) {
2832                    Some(name.clone())
2833                } else {
2834                    None
2835                }
2836            })
2837            .collect();
2838        let mut output_owner: std::collections::HashMap<*const crate::audio::io::AudioIO, String> =
2839            std::collections::HashMap::new();
2840        for (name, track) in state.tracks.iter() {
2841            let t = track.lock();
2842            for out in &t.audio.outs {
2843                output_owner.insert(std::sync::Arc::as_ptr(out), name.clone());
2844            }
2845        }
2846
2847        let mut infected = live_seeds.clone();
2848        let mut mixed_nodes = std::collections::HashSet::new();
2849        loop {
2850            let mut changed = false;
2851            for (name, track) in state.tracks.iter() {
2852                let t = track.lock();
2853                let mut upstream_owners = std::collections::HashSet::new();
2854                for input in &t.audio.ins {
2855                    for conn in input.connections.lock().iter() {
2856                        if let Some(owner) = output_owner.get(&std::sync::Arc::as_ptr(conn)) {
2857                            upstream_owners.insert(owner.clone());
2858                        }
2859                    }
2860                }
2861                if upstream_owners.is_empty() {
2862                    continue;
2863                }
2864                let has_realtime = upstream_owners
2865                    .iter()
2866                    .any(|owner| infected.contains(owner) || live_seeds.contains(owner));
2867                let has_playback = upstream_owners
2868                    .iter()
2869                    .any(|owner| !infected.contains(owner) && !live_seeds.contains(owner));
2870                if has_realtime && has_playback {
2871                    mixed_nodes.insert(name.clone());
2872                }
2873                if has_realtime && infected.insert(name.clone()) {
2874                    changed = true;
2875                }
2876            }
2877            if !changed {
2878                break;
2879            }
2880        }
2881
2882        for (name, track) in state.tracks.iter() {
2883            let forced = infected.contains(name) && !live_seeds.contains(name);
2884            let t = track.lock();
2885            t.set_shared_realtime_mixed(mixed_nodes.contains(name));
2886            t.set_force_realtime_domain(forced);
2887        }
2888    }
2889
2890    fn apply_mute_solo_policy(&mut self) {
2891        let mut newly_disabled_tracks = Vec::new();
2892        {
2893            let tracks = &self.state.lock().tracks;
2894            let soloed: std::collections::HashSet<String> = tracks
2895                .iter()
2896                .filter_map(|(name, t)| {
2897                    if t.lock().soloed {
2898                        Some(name.clone())
2899                    } else {
2900                        None
2901                    }
2902                })
2903                .collect();
2904            let any_soloed = !soloed.is_empty();
2905            let upstream = if any_soloed {
2906                self.upstream_audio_track_names(&soloed)
2907            } else {
2908                std::collections::HashSet::new()
2909            };
2910            for track in tracks.values() {
2911                let t = track.lock();
2912                let was_enabled = t.output_enabled;
2913                let in_soloed_folder = self.is_track_in_soloed_folder(t, tracks);
2914                let folder_with_soloed_child =
2915                    t.is_folder && self.folder_has_soloed_descendant(&t.name, tracks);
2916                let enabled = if t.is_master {
2917                    !t.muted
2918                } else if any_soloed {
2919                    (t.soloed
2920                        || upstream.contains(&t.name)
2921                        || in_soloed_folder
2922                        || folder_with_soloed_child)
2923                        && !t.muted
2924                } else {
2925                    !t.muted
2926                };
2927                t.set_output_enabled(enabled);
2928                if was_enabled && !enabled {
2929                    newly_disabled_tracks.push(t.name.clone());
2930                }
2931            }
2932        }
2933        let mut note_off_events = Vec::new();
2934        for track_name in newly_disabled_tracks {
2935            note_off_events.extend(self.note_off_events_for_track(&track_name));
2936        }
2937        if !note_off_events.is_empty() {
2938            self.pending_hw_midi_out_events_by_device
2939                .extend(note_off_events);
2940        }
2941    }
2942
2943    fn sanitize_file_stem(name: &str) -> String {
2944        let mut out = String::with_capacity(name.len());
2945        for c in name.chars() {
2946            if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
2947                out.push(c);
2948            } else {
2949                out.push('_');
2950            }
2951        }
2952        if out.is_empty() {
2953            "track".to_string()
2954        } else {
2955            out
2956        }
2957    }
2958
2959    fn next_recording_file_name(track_name: &str) -> String {
2960        let ts = SystemTime::now()
2961            .duration_since(UNIX_EPOCH)
2962            .map(|d| d.as_secs())
2963            .unwrap_or(0);
2964        format!("{}_{}.wav", Self::sanitize_file_stem(track_name), ts)
2965    }
2966
2967    fn next_midi_recording_file_name(track_name: &str) -> String {
2968        let ts = SystemTime::now()
2969            .duration_since(UNIX_EPOCH)
2970            .map(|d| d.as_secs())
2971            .unwrap_or(0);
2972        format!("{}_{}.mid", Self::sanitize_file_stem(track_name), ts)
2973    }
2974
2975    fn append_recorded_cycle(&mut self) {
2976        if !self.playing || !self.record_enabled {
2977            return;
2978        }
2979        for (name, track_handle) in &self.state.lock().tracks {
2980            let track = track_handle.lock();
2981            if !track.armed {
2982                continue;
2983            }
2984            let audio_channels = track.record_tap_outs.len();
2985            let audio_frames = track
2986                .record_tap_outs
2987                .first()
2988                .map(|ch| ch.len())
2989                .unwrap_or(0);
2990            let frames = audio_frames.max(self.current_cycle_samples());
2991            if frames == 0 {
2992                continue;
2993            }
2994            let segments = self.recording_segments_for_cycle(frames);
2995            for (segment_start, segment_end, frame_offset) in segments {
2996                let segment_len = segment_end.saturating_sub(segment_start);
2997                if segment_len == 0 {
2998                    continue;
2999                }
3000
3001                if audio_channels > 0 && audio_frames > 0 {
3002                    let audio_entry =
3003                        self.audio_recordings
3004                            .entry(name.clone())
3005                            .or_insert_with(|| RecordingSession {
3006                                start_sample: segment_start,
3007                                samples: Vec::with_capacity(segment_len * audio_channels * 2),
3008                                channels: audio_channels,
3009                                file_name: Self::next_recording_file_name(name),
3010                                stripe_peaks: vec![Vec::new(); audio_channels],
3011                                current_stripe_frames: 0,
3012                            });
3013                    if audio_entry.channels != audio_channels {
3014                        continue;
3015                    }
3016                    if let Some(entry) = self.audio_recordings.get_mut(name.as_str()) {
3017                        let from = frame_offset.min(audio_frames);
3018                        let to = frame_offset.saturating_add(segment_len).min(audio_frames);
3019                        for frame in from..to {
3020                            let is_new_stripe =
3021                                entry.current_stripe_frames % RECORDING_STRIPE_FRAMES == 0;
3022                            for ch in 0..audio_channels {
3023                                let sample = track.record_tap_outs[ch][frame].clamp(-1.0, 1.0);
3024                                if is_new_stripe {
3025                                    entry.stripe_peaks[ch].push([sample, sample]);
3026                                } else {
3027                                    let idx = entry.stripe_peaks[ch].len() - 1;
3028                                    entry.stripe_peaks[ch][idx][0] =
3029                                        entry.stripe_peaks[ch][idx][0].min(sample);
3030                                    entry.stripe_peaks[ch][idx][1] =
3031                                        entry.stripe_peaks[ch][idx][1].max(sample);
3032                                }
3033                                entry.samples.push(track.record_tap_outs[ch][frame]);
3034                            }
3035                            entry.current_stripe_frames += 1;
3036                        }
3037                    }
3038                }
3039
3040                let entry = self.midi_recordings.entry(name.clone()).or_insert_with(|| {
3041                    MidiRecordingSession {
3042                        start_sample: segment_start,
3043                        events: Vec::new(),
3044                        file_name: Self::next_midi_recording_file_name(name),
3045                    }
3046                });
3047                let from = frame_offset;
3048                let to = frame_offset.saturating_add(segment_len);
3049                for event in &track.record_tap_midi_in {
3050                    let frame = event.frame as usize;
3051                    if frame < from || frame >= to {
3052                        continue;
3053                    }
3054                    let abs_sample = segment_start as u64 + (frame - from) as u64;
3055                    entry.events.push((abs_sample, event.data.clone()));
3056                }
3057
3058                if self.punch_enabled
3059                    && let Some((_, punch_end)) = self.punch_range_samples
3060                    && segment_end == punch_end
3061                {
3062                    if let Some(done) = self.audio_recordings.remove(name.as_str()) {
3063                        self.completed_audio_recordings.push((name.clone(), done));
3064                    }
3065                    if let Some(done) = self.midi_recordings.remove(name.as_str()) {
3066                        self.completed_midi_recordings.push((name.clone(), done));
3067                    }
3068                } else if self.loop_enabled
3069                    && let Some((_, loop_end)) = self.loop_range_samples
3070                    && segment_end == loop_end
3071                {
3072                    if let Some(done) = self.audio_recordings.remove(name.as_str()) {
3073                        self.completed_audio_recordings.push((name.clone(), done));
3074                    }
3075                    if let Some(done) = self.midi_recordings.remove(name.as_str()) {
3076                        self.completed_midi_recordings.push((name.clone(), done));
3077                    }
3078                }
3079            }
3080        }
3081    }
3082
3083    async fn flush_completed_recordings(&mut self) {
3084        if self.completed_audio_recordings.is_empty() && self.completed_midi_recordings.is_empty() {
3085            return;
3086        }
3087        let Some(audio_dir) = self.session_audio_dir() else {
3088            self.completed_audio_recordings.clear();
3089            self.completed_midi_recordings.clear();
3090            return;
3091        };
3092        let Some(midi_dir) = self.session_midi_dir() else {
3093            self.completed_audio_recordings.clear();
3094            self.completed_midi_recordings.clear();
3095            return;
3096        };
3097        if std::fs::create_dir_all(&audio_dir).is_err()
3098            || std::fs::create_dir_all(&midi_dir).is_err()
3099        {
3100            self.completed_audio_recordings.clear();
3101            self.completed_midi_recordings.clear();
3102            return;
3103        }
3104        let rate = self
3105            .hw_driver
3106            .as_ref()
3107            .map(|o| o.lock().sample_rate())
3108            .unwrap_or(48_000);
3109        let completed_audio = std::mem::take(&mut self.completed_audio_recordings);
3110        for (track_name, rec) in completed_audio {
3111            self.flush_recording_entry(&audio_dir, rate, track_name, rec)
3112                .await;
3113        }
3114        let completed_midi = std::mem::take(&mut self.completed_midi_recordings);
3115        for (track_name, rec) in completed_midi {
3116            self.flush_midi_recording_entry(&midi_dir, rate as u32, track_name, rec)
3117                .await;
3118        }
3119    }
3120
3121    async fn flush_recordings(&mut self) {
3122        let Some(audio_dir) = self.session_audio_dir() else {
3123            if !self.audio_recordings.is_empty()
3124                || !self.midi_recordings.is_empty()
3125                || !self.completed_audio_recordings.is_empty()
3126                || !self.completed_midi_recordings.is_empty()
3127            {
3128                self.notify_clients(Err("Recording stopped: session path is not set".to_string()))
3129                    .await;
3130            }
3131            self.audio_recordings.clear();
3132            self.midi_recordings.clear();
3133            self.completed_audio_recordings.clear();
3134            self.completed_midi_recordings.clear();
3135            return;
3136        };
3137        if std::fs::create_dir_all(&audio_dir).is_err() {
3138            self.notify_clients(Err(format!(
3139                "Recording stopped: failed to create audio directory {}",
3140                audio_dir.display()
3141            )))
3142            .await;
3143            self.audio_recordings.clear();
3144            self.midi_recordings.clear();
3145            self.completed_audio_recordings.clear();
3146            self.completed_midi_recordings.clear();
3147            return;
3148        }
3149        let Some(midi_dir) = self.session_midi_dir() else {
3150            self.audio_recordings.clear();
3151            self.midi_recordings.clear();
3152            self.completed_audio_recordings.clear();
3153            self.completed_midi_recordings.clear();
3154            return;
3155        };
3156        if std::fs::create_dir_all(&midi_dir).is_err() {
3157            self.audio_recordings.clear();
3158            self.midi_recordings.clear();
3159            self.completed_audio_recordings.clear();
3160            self.completed_midi_recordings.clear();
3161            return;
3162        }
3163        let rate = self
3164            .hw_driver
3165            .as_ref()
3166            .map(|o| o.lock().sample_rate())
3167            .unwrap_or(48_000);
3168        let completed_audio = std::mem::take(&mut self.completed_audio_recordings);
3169        for (track_name, rec) in completed_audio {
3170            self.flush_recording_entry(&audio_dir, rate, track_name, rec)
3171                .await;
3172        }
3173        let completed_midi = std::mem::take(&mut self.completed_midi_recordings);
3174        for (track_name, rec) in completed_midi {
3175            self.flush_midi_recording_entry(&midi_dir, rate as u32, track_name, rec)
3176                .await;
3177        }
3178        let recordings = std::mem::take(&mut self.audio_recordings);
3179        for (track_name, rec) in recordings {
3180            self.flush_recording_entry(&audio_dir, rate, track_name, rec)
3181                .await;
3182        }
3183        let midi_recordings = std::mem::take(&mut self.midi_recordings);
3184        for (track_name, rec) in midi_recordings {
3185            self.flush_midi_recording_entry(&midi_dir, rate as u32, track_name, rec)
3186                .await;
3187        }
3188    }
3189
3190    fn compute_peaks_from_stripes(
3191        stripe_peaks: &[Vec<[f32; 2]>],
3192        total_frames: usize,
3193        channels: usize,
3194    ) -> serde_json::Value {
3195        const MAX_PEAK_BINS: usize = 32_768;
3196        if total_frames == 0 || stripe_peaks.is_empty() {
3197            return serde_json::json!({"peaks": []});
3198        }
3199        let target_bins = total_frames.clamp(1024, MAX_PEAK_BINS);
3200        let mut peaks = vec![vec![[0.0_f32, 0.0_f32]; target_bins]; channels];
3201        for (ch, channel_peaks) in peaks.iter_mut().enumerate() {
3202            let mut touched = vec![false; target_bins];
3203            let empty = Vec::new();
3204            let channel_stripes = stripe_peaks.get(ch).unwrap_or(&empty);
3205            for (stripe_idx, stripe) in channel_stripes.iter().enumerate() {
3206                let stripe_start = stripe_idx * RECORDING_STRIPE_FRAMES;
3207                let stripe_end = ((stripe_idx + 1) * RECORDING_STRIPE_FRAMES).min(total_frames);
3208                let start_bin = (stripe_start * target_bins) / total_frames.max(1);
3209                let end_bin = ((stripe_end.saturating_sub(1)) * target_bins / total_frames.max(1))
3210                    .min(target_bins - 1);
3211                for bin in start_bin..=end_bin {
3212                    if !touched[bin] {
3213                        channel_peaks[bin] = *stripe;
3214                        touched[bin] = true;
3215                    } else {
3216                        channel_peaks[bin][0] = channel_peaks[bin][0].min(stripe[0]);
3217                        channel_peaks[bin][1] = channel_peaks[bin][1].max(stripe[1]);
3218                    }
3219                }
3220            }
3221        }
3222        serde_json::json!({
3223            "peaks": peaks.iter().map(|ch| {
3224                ch.iter().map(|pair| serde_json::json!([pair[0], pair[1]])).collect::<Vec<_>>()
3225            }).collect::<Vec<_>>()
3226        })
3227    }
3228
3229    async fn flush_recording_entry(
3230        &mut self,
3231        audio_dir: &Path,
3232        rate: i32,
3233        track_name: String,
3234        rec: RecordingSession,
3235    ) {
3236        if rec.samples.is_empty() || rec.channels == 0 {
3237            return;
3238        }
3239
3240        let trim_frames = self.hw_output_latency_frames;
3241        let trim_samples = trim_frames * rec.channels;
3242        let samples = if trim_samples > 0 && rec.samples.len() > trim_samples {
3243            &rec.samples[trim_samples..]
3244        } else {
3245            &rec.samples[..]
3246        };
3247        if samples.is_empty() {
3248            return;
3249        }
3250        let file_path = audio_dir.join(&rec.file_name);
3251        let write_result =
3252            crate::audio_codec::write_wav_f32(&file_path, samples, rec.channels, rate as u32);
3253        if let Err(e) = write_result {
3254            tracing::error!("flush_recording_entry: WAV write failed: {}", e);
3255            self.notify_clients(Err(format!(
3256                "Failed to write recording {}: {}",
3257                file_path.display(),
3258                e
3259            )))
3260            .await;
3261            return;
3262        }
3263
3264        let total_frames = rec.current_stripe_frames;
3265        let peaks_json =
3266            Self::compute_peaks_from_stripes(&rec.stripe_peaks, total_frames, rec.channels);
3267        let peaks_file_name = format!("{}.json", rec.file_name);
3268        let peaks_rel = format!("peaks/{}", peaks_file_name);
3269        let peaks_path = self.session_peaks_dir().map(|d| d.join(&peaks_file_name));
3270        if let Some(peaks_dir) = self.session_peaks_dir() {
3271            let _ = std::fs::create_dir_all(&peaks_dir);
3272        }
3273        if let Some(ref path) = peaks_path
3274            && let Err(e) = std::fs::write(
3275                path,
3276                serde_json::to_string_pretty(&peaks_json).unwrap_or_default(),
3277            )
3278        {
3279            tracing::warn!("Failed to write peaks file {}: {}", path.display(), e);
3280        }
3281        let length = samples.len() / rec.channels;
3282        let start_sample = rec.start_sample.saturating_add(trim_frames);
3283        let clip_rel_name = format!("audio/{}", rec.file_name);
3284        let mut clip = AudioClip::new(
3285            clip_rel_name.clone(),
3286            start_sample,
3287            start_sample.saturating_add(length.max(1)),
3288        );
3289        let (audio_ins, audio_outs) = if let Some(track) = self.state.lock().tracks.get(&track_name)
3290        {
3291            let track = track.lock();
3292            let audio_ins = track.audio.ins.len();
3293            let audio_outs = track.audio.outs.len();
3294            track.audio.clips.push(clip.clone());
3295            (audio_ins, audio_outs)
3296        } else {
3297            tracing::warn!(
3298                "flush_recording_entry: track '{}' not found in engine state",
3299                track_name
3300            );
3301            (0, 0)
3302        };
3303        let clip_id = crate::message::generate_clip_id();
3304        clip.id.clone_from(&clip_id);
3305        self.notify_clients(Ok(Action::AddClip {
3306            clip_id,
3307            name: clip_rel_name,
3308            track_name: track_name.clone(),
3309            start: start_sample,
3310            length,
3311            offset: 0,
3312            input_channel: 0,
3313            muted: false,
3314            peaks_file: peaks_path.is_some().then_some(peaks_rel),
3315            kind: Kind::Audio,
3316            fade_enabled: clip.fade_enabled,
3317            fade_in_samples: clip.fade_in_samples,
3318            fade_out_samples: clip.fade_out_samples,
3319            source_name: None,
3320            source_offset: None,
3321            source_length: None,
3322            preview_name: None,
3323            pitch_correction_points: vec![],
3324            pitch_correction_frame_likeness: None,
3325            pitch_correction_inertia_ms: None,
3326            pitch_correction_formant_compensation: None,
3327            plugin_graph_json: Some(Self::default_clip_plugin_graph_json(audio_ins, audio_outs)),
3328        }))
3329        .await;
3330        if let Some(track) = self.state.lock().tracks.get(&track_name).cloned() {
3331            tokio::task::spawn_blocking(move || {
3332                track.lock().preload_clips();
3333                tracing::debug!("Preloaded clips for track '{}' after recording", track_name);
3334            });
3335        }
3336    }
3337
3338    async fn flush_track_recording(&mut self, track_name: &str) {
3339        let Some(audio_dir) = self.session_audio_dir() else {
3340            self.audio_recordings.remove(track_name);
3341            self.midi_recordings.remove(track_name);
3342            self.completed_audio_recordings
3343                .retain(|(name, _)| name != track_name);
3344            self.completed_midi_recordings
3345                .retain(|(name, _)| name != track_name);
3346            return;
3347        };
3348        let Some(midi_dir) = self.session_midi_dir() else {
3349            self.audio_recordings.remove(track_name);
3350            self.midi_recordings.remove(track_name);
3351            self.completed_audio_recordings
3352                .retain(|(name, _)| name != track_name);
3353            self.completed_midi_recordings
3354                .retain(|(name, _)| name != track_name);
3355            return;
3356        };
3357        if std::fs::create_dir_all(&audio_dir).is_err()
3358            || std::fs::create_dir_all(&midi_dir).is_err()
3359        {
3360            return;
3361        }
3362        let rate = self
3363            .hw_driver
3364            .as_ref()
3365            .map(|o| o.lock().sample_rate())
3366            .unwrap_or(48_000);
3367        let mut i = 0;
3368        while i < self.completed_audio_recordings.len() {
3369            if self.completed_audio_recordings[i].0 == track_name {
3370                let (name, rec) = self.completed_audio_recordings.remove(i);
3371                self.flush_recording_entry(&audio_dir, rate, name, rec)
3372                    .await;
3373            } else {
3374                i += 1;
3375            }
3376        }
3377        let mut j = 0;
3378        while j < self.completed_midi_recordings.len() {
3379            if self.completed_midi_recordings[j].0 == track_name {
3380                let (name, rec) = self.completed_midi_recordings.remove(j);
3381                self.flush_midi_recording_entry(&midi_dir, rate as u32, name, rec)
3382                    .await;
3383            } else {
3384                j += 1;
3385            }
3386        }
3387
3388        let Some(rec) = self.audio_recordings.remove(track_name) else {
3389            if let Some(mrec) = self.midi_recordings.remove(track_name) {
3390                self.flush_midi_recording_entry(
3391                    &midi_dir,
3392                    rate as u32,
3393                    track_name.to_string(),
3394                    mrec,
3395                )
3396                .await;
3397            }
3398            return;
3399        };
3400        self.flush_recording_entry(&audio_dir, rate, track_name.to_string(), rec)
3401            .await;
3402        if let Some(mrec) = self.midi_recordings.remove(track_name) {
3403            self.flush_midi_recording_entry(&midi_dir, rate as u32, track_name.to_string(), mrec)
3404                .await;
3405        }
3406    }
3407
3408    async fn flush_midi_recording_entry(
3409        &mut self,
3410        midi_dir: &Path,
3411        sample_rate: u32,
3412        track_name: String,
3413        mut rec: MidiRecordingSession,
3414    ) {
3415        if rec.events.is_empty() {
3416            return;
3417        }
3418        rec.events.sort_by_key(|(sample, _)| *sample);
3419        let clip_rel_name = format!("midi/{}", rec.file_name);
3420        let clip_len_samples = rec
3421            .events
3422            .last()
3423            .map(|(s, _)| s.saturating_sub(rec.start_sample as u64) as usize + 1)
3424            .unwrap_or(1);
3425
3426        for (sample, _) in &mut rec.events {
3427            *sample = sample.saturating_sub(rec.start_sample as u64);
3428        }
3429        let path = midi_dir.join(&rec.file_name);
3430        if let Err(e) = Self::write_midi_file(&path, sample_rate, &rec.events) {
3431            self.notify_clients(Err(format!(
3432                "Failed to write MIDI recording {}: {}",
3433                path.display(),
3434                e
3435            )))
3436            .await;
3437            return;
3438        }
3439        let mut clip = MIDIClip::new(
3440            clip_rel_name.clone(),
3441            rec.start_sample,
3442            rec.start_sample.saturating_add(clip_len_samples.max(1)),
3443        );
3444        clip.offset = 0;
3445        let clip_id = crate::message::generate_clip_id();
3446        clip.id.clone_from(&clip_id);
3447        if let Some(track) = self.state.lock().tracks.get(&track_name) {
3448            track.lock().midi.clips.push(clip);
3449        }
3450        self.notify_clients(Ok(Action::AddClip {
3451            clip_id,
3452            name: clip_rel_name,
3453            track_name: track_name.clone(),
3454            start: rec.start_sample,
3455            length: clip_len_samples,
3456            offset: 0,
3457            input_channel: 0,
3458            muted: false,
3459            peaks_file: None,
3460            kind: Kind::MIDI,
3461            fade_enabled: true,
3462            fade_in_samples: 240,
3463            fade_out_samples: 240,
3464            source_name: None,
3465            source_offset: None,
3466            source_length: None,
3467            preview_name: None,
3468            pitch_correction_points: vec![],
3469            pitch_correction_frame_likeness: None,
3470            pitch_correction_inertia_ms: None,
3471            pitch_correction_formant_compensation: None,
3472            plugin_graph_json: None,
3473        }))
3474        .await;
3475        if let Some(track) = self.state.lock().tracks.get(&track_name).cloned() {
3476            tokio::task::spawn_blocking(move || {
3477                track.lock().preload_clips();
3478                tracing::debug!(
3479                    "Preloaded clips for track '{}' after MIDI recording",
3480                    track_name
3481                );
3482            });
3483        }
3484    }
3485
3486    fn write_midi_file(
3487        path: &Path,
3488        sample_rate: u32,
3489        events: &[(u64, Vec<u8>)],
3490    ) -> Result<(), String> {
3491        let ppq: u16 = 480;
3492        let ticks_per_second: u64 = 960;
3493        let arena = Arena::new();
3494        let mut track_events: Vec<TrackEvent<'_>> = vec![TrackEvent {
3495            delta: u28::new(0),
3496            kind: TrackEventKind::Meta(MetaMessage::Tempo(u24::new(500_000))),
3497        }];
3498        let mut prev_ticks = 0_u64;
3499        for (sample, data) in events {
3500            let ticks = sample.saturating_mul(ticks_per_second) / sample_rate.max(1) as u64;
3501            let delta = ticks.saturating_sub(prev_ticks).min(u32::MAX as u64) as u32;
3502            prev_ticks = ticks;
3503            let Ok(live) = LiveEvent::parse(data) else {
3504                continue;
3505            };
3506            let kind = live.as_track_event(&arena);
3507            track_events.push(TrackEvent {
3508                delta: u28::new(delta),
3509                kind,
3510            });
3511        }
3512        track_events.push(TrackEvent {
3513            delta: u28::new(0),
3514            kind: TrackEventKind::Meta(MetaMessage::EndOfTrack),
3515        });
3516
3517        let smf = Smf {
3518            header: Header::new(Format::SingleTrack, Timing::Metrical(u15::new(ppq))),
3519            tracks: vec![track_events],
3520        };
3521        let mut file = File::create(path).map_err(|e| e.to_string())?;
3522        smf.write_std(&mut file).map_err(|e| e.to_string())
3523    }
3524
3525    pub async fn init(&mut self) {
3526        let max_threads = num_cpus::get();
3527        for id in 0..max_threads {
3528            let (tx, rx) = channel::<Message>(32);
3529            let tx_thread = self.tx.clone();
3530            let handler = tokio::spawn(async move {
3531                let wrk = Worker::new(id, rx, tx_thread, 8);
3532                wrk.await.work().await;
3533            });
3534            self.workers.push(WorkerData::new(tx.clone(), handler));
3535        }
3536    }
3537
3538    async fn notify_clients(&mut self, action: Result<Action, String>) {
3539        self.clients.retain(|client| !client.is_closed());
3540        for client in self.clients.iter() {
3541            if client
3542                .send(Message::Response(action.clone()))
3543                .await
3544                .is_err()
3545            {}
3546        }
3547    }
3548
3549    fn spawn_plugin_host_stderr_reader(&self, stderr: std::process::ChildStderr, source: String) {
3550        let tx = self.tx.clone();
3551        std::thread::spawn(move || {
3552            use std::io::{BufRead, BufReader};
3553            let reader = BufReader::new(stderr);
3554            for line in reader.lines() {
3555                if let Ok(line) = line
3556                    && !line.is_empty()
3557                {
3558                    let _ = tx.blocking_send(Message::Request(Action::Log {
3559                        source: source.clone(),
3560                        message: line,
3561                    }));
3562                }
3563            }
3564        });
3565    }
3566
3567    fn set_osc_enabled_with<F>(&mut self, enabled: bool, start_server: F) -> Result<(), String>
3568    where
3569        F: FnOnce(Sender<Message>) -> Result<OscServer, String>,
3570    {
3571        if enabled {
3572            if self.osc_server.is_none() {
3573                self.osc_server = Some(start_server(self.tx.clone())?);
3574            }
3575        } else if let Some(mut server) = self.osc_server.take() {
3576            server.stop();
3577        }
3578        Ok(())
3579    }
3580
3581    fn track_handle_by_name(&self, track_name: &str) -> Option<Arc<UnsafeMutex<Box<Track>>>> {
3582        self.state.lock().tracks.get(track_name).cloned()
3583    }
3584
3585    fn track_handle_or_err(
3586        &self,
3587        track_name: &str,
3588    ) -> Result<Arc<UnsafeMutex<Box<Track>>>, String> {
3589        self.track_handle_by_name(track_name)
3590            .ok_or_else(|| format!("Track not found: {track_name}"))
3591    }
3592
3593    fn add_clip_to_track(&self, request: ClipAddRequest<'_>) {
3594        if let Some(track) = self.state.lock().tracks.get(request.track_name) {
3595            let track = track.lock();
3596            if track.is_master || track.is_folder {
3597                return;
3598            }
3599            match request.kind {
3600                Kind::Audio => {
3601                    let mut clip = AudioClip::new(
3602                        request.name.to_string(),
3603                        request.start,
3604                        request.start.saturating_add(request.length.max(1)),
3605                    );
3606                    clip.id = request.clip_id.to_string();
3607                    clip.offset = request.offset;
3608                    let max_lane = track.audio.ins.len().saturating_sub(1);
3609                    clip.input_channel = request.input_channel.min(max_lane);
3610                    clip.muted = request.muted;
3611                    clip.peaks_file = request.peaks_file;
3612                    clip.fade_enabled = request.fade_enabled;
3613                    clip.fade_in_samples = request.fade_in_samples;
3614                    clip.fade_out_samples = request.fade_out_samples;
3615                    clip.pitch_correction_preview_name = request.preview_name;
3616                    clip.pitch_correction_source_name = request.source_name;
3617                    clip.pitch_correction_source_offset = request.source_offset;
3618                    clip.pitch_correction_source_length = request.source_length;
3619                    clip.pitch_correction_points = request.pitch_correction_points;
3620                    clip.pitch_correction_frame_likeness = request.pitch_correction_frame_likeness;
3621                    clip.pitch_correction_inertia_ms = request.pitch_correction_inertia_ms;
3622                    clip.pitch_correction_formant_compensation =
3623                        request.pitch_correction_formant_compensation;
3624                    clip.plugin_graph_json = request.plugin_graph_json;
3625                    track.audio.clips.push(clip);
3626                    #[cfg(unix)]
3627                    track.clip_pitch_shifters.clear();
3628                }
3629                Kind::MIDI => {
3630                    let mut clip = MIDIClip::new(
3631                        request.name.to_string(),
3632                        request.start,
3633                        request.start.saturating_add(request.length.max(1)),
3634                    );
3635                    clip.id = request.clip_id.to_string();
3636                    clip.offset = request.offset;
3637                    let max_lane = track.midi.ins.len().saturating_sub(1);
3638                    clip.input_channel = request.input_channel.min(max_lane);
3639                    clip.muted = request.muted;
3640                    track.midi.clips.push(clip);
3641                }
3642            }
3643        }
3644    }
3645
3646    fn audio_clip_from_data(data: &crate::message::AudioClipData) -> AudioClip {
3647        let mut clip = AudioClip::new(
3648            data.name.clone(),
3649            data.start,
3650            data.start.saturating_add(data.length.max(1)),
3651        );
3652        clip.id = data.id.clone();
3653        clip.offset = data.offset;
3654        clip.input_channel = data.input_channel;
3655        clip.muted = data.muted;
3656        clip.peaks_file = data.peaks_file.clone();
3657        clip.fade_enabled = data.fade_enabled;
3658        clip.fade_in_samples = data.fade_in_samples;
3659        clip.fade_out_samples = data.fade_out_samples;
3660        clip.pitch_correction_preview_name = data.preview_name.clone();
3661        clip.pitch_correction_source_name = data.source_name.clone();
3662        clip.pitch_correction_source_offset = data.source_offset;
3663        clip.pitch_correction_source_length = data.source_length;
3664        clip.pitch_correction_points = data.pitch_correction_points.clone();
3665        clip.pitch_correction_frame_likeness = data.pitch_correction_frame_likeness;
3666        clip.pitch_correction_inertia_ms = data.pitch_correction_inertia_ms;
3667        clip.pitch_correction_formant_compensation = data.pitch_correction_formant_compensation;
3668        clip.plugin_graph_json = data.plugin_graph_json.clone();
3669        clip.grouped_clips = data
3670            .grouped_clips
3671            .iter()
3672            .map(Self::audio_clip_from_data)
3673            .collect();
3674        for child in &mut clip.grouped_clips {
3675            child.fade_enabled = false;
3676            child.fade_in_samples = 0;
3677            child.fade_out_samples = 0;
3678        }
3679        clip
3680    }
3681
3682    fn midi_clip_from_data(data: &crate::message::MidiClipData) -> MIDIClip {
3683        let mut clip = MIDIClip::new(
3684            data.name.clone(),
3685            data.start,
3686            data.start.saturating_add(data.length.max(1)),
3687        );
3688        clip.id = data.id.clone();
3689        clip.offset = data.offset;
3690        clip.input_channel = data.input_channel;
3691        clip.muted = data.muted;
3692        clip.grouped_clips = data
3693            .grouped_clips
3694            .iter()
3695            .map(Self::midi_clip_from_data)
3696            .collect();
3697        clip
3698    }
3699
3700    fn add_grouped_clip_to_track(
3701        &self,
3702        track_name: &str,
3703        kind: Kind,
3704        audio_clip: Option<crate::message::AudioClipData>,
3705        midi_clip: Option<crate::message::MidiClipData>,
3706    ) {
3707        if let Some(track) = self.state.lock().tracks.get(track_name) {
3708            let track = track.lock();
3709            if track.is_master {
3710                return;
3711            }
3712            match kind {
3713                Kind::Audio => {
3714                    if let Some(mut clip) = audio_clip.map(|clip| Self::audio_clip_from_data(&clip))
3715                    {
3716                        let max_lane = track.audio.ins.len().saturating_sub(1);
3717                        clip.input_channel = clip.input_channel.min(max_lane);
3718                        track.audio.clips.push(clip);
3719                        #[cfg(unix)]
3720                        track.clip_pitch_shifters.clear();
3721                    }
3722                }
3723                Kind::MIDI => {
3724                    if let Some(mut clip) = midi_clip.map(|clip| Self::midi_clip_from_data(&clip)) {
3725                        let max_lane = track.midi.ins.len().saturating_sub(1);
3726                        clip.input_channel = clip.input_channel.min(max_lane);
3727                        track.midi.clips.push(clip);
3728                    }
3729                }
3730            }
3731        }
3732    }
3733
3734    fn remove_clips_from_track(&self, track_name: &str, kind: Kind, clip_indices: &[usize]) {
3735        if let Some(track) = self.state.lock().tracks.get(track_name) {
3736            let track = track.lock();
3737            let mut indices = clip_indices.to_vec();
3738            indices.sort_unstable();
3739            indices.dedup();
3740            match kind {
3741                Kind::Audio => {
3742                    for idx in indices.into_iter().rev() {
3743                        if idx < track.audio.clips.len() {
3744                            track.audio.clips.remove(idx);
3745                        }
3746                    }
3747                    #[cfg(unix)]
3748                    track.clip_pitch_shifters.clear();
3749                }
3750                Kind::MIDI => {
3751                    for idx in indices.into_iter().rev() {
3752                        if idx < track.midi.clips.len() {
3753                            track.midi.clips.remove(idx);
3754                        }
3755                    }
3756                }
3757            }
3758        }
3759    }
3760
3761    fn rename_clip_references(
3762        &self,
3763        track_name: &str,
3764        kind: Kind,
3765        clip_index: usize,
3766        new_name: &str,
3767    ) {
3768        let Some(track) = self.state.lock().tracks.get(track_name) else {
3769            return;
3770        };
3771        let track = track.lock();
3772        let old_name = match kind {
3773            Kind::Audio => {
3774                if clip_index >= track.audio.clips.len() {
3775                    return;
3776                }
3777                track.audio.clips[clip_index].name.clone()
3778            }
3779            Kind::MIDI => {
3780                if clip_index >= track.midi.clips.len() {
3781                    return;
3782                }
3783                track.midi.clips[clip_index].name.clone()
3784            }
3785        };
3786
3787        let new_file_name = match kind {
3788            Kind::Audio => format!("audio/{}.wav", new_name),
3789            Kind::MIDI => {
3790                let ext = std::path::Path::new(&old_name)
3791                    .extension()
3792                    .and_then(|e| e.to_str())
3793                    .map(|s| s.to_ascii_lowercase())
3794                    .filter(|e| e == "mid" || e == "midi")
3795                    .unwrap_or_else(|| "mid".to_string());
3796                format!("midi/{}.{}", new_name, ext)
3797            }
3798        };
3799        let _ = track;
3800
3801        for (_, other_track) in self.state.lock().tracks.iter() {
3802            let other_track = other_track.lock();
3803            match kind {
3804                Kind::Audio => {
3805                    for clip in &mut other_track.audio.clips {
3806                        if clip.name == old_name {
3807                            clip.name = new_file_name.clone();
3808                        }
3809                        if clip.pitch_correction_source_name.as_deref() == Some(old_name.as_str()) {
3810                            clip.pitch_correction_source_name = Some(new_file_name.clone());
3811                        }
3812                    }
3813                }
3814                Kind::MIDI => {
3815                    for clip in &mut other_track.midi.clips {
3816                        if clip.name == old_name {
3817                            clip.name = new_file_name.clone();
3818                        }
3819                    }
3820                }
3821            }
3822        }
3823    }
3824
3825    fn set_clip_fade(
3826        &self,
3827        track_name: &str,
3828        clip_index: usize,
3829        kind: Kind,
3830        fade_enabled: bool,
3831        fade_in_samples: usize,
3832        fade_out_samples: usize,
3833    ) {
3834        let Some(track) = self.state.lock().tracks.get(track_name) else {
3835            return;
3836        };
3837        let track = track.lock();
3838        match kind {
3839            Kind::Audio => {
3840                if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3841                    clip.fade_enabled = fade_enabled;
3842                    clip.fade_in_samples = fade_in_samples;
3843                    clip.fade_out_samples = fade_out_samples;
3844                }
3845            }
3846            Kind::MIDI => {}
3847        }
3848    }
3849
3850    fn set_clip_bounds(
3851        &self,
3852        track_name: &str,
3853        clip_index: usize,
3854        kind: Kind,
3855        start: usize,
3856        length: usize,
3857        offset: usize,
3858    ) {
3859        let Some(track) = self.state.lock().tracks.get(track_name) else {
3860            return;
3861        };
3862        let track = track.lock();
3863        match kind {
3864            Kind::Audio => {
3865                if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3866                    clip.start = start;
3867                    clip.end = start.saturating_add(length.max(1));
3868                    clip.offset = offset;
3869                    clip.pitch_correction_preview_name = None;
3870                    clip.pitch_correction_source_name = None;
3871                    clip.pitch_correction_source_offset = None;
3872                    clip.pitch_correction_source_length = None;
3873                    clip.pitch_correction_points.clear();
3874                    clip.pitch_correction_frame_likeness = None;
3875                    clip.pitch_correction_inertia_ms = None;
3876                    clip.pitch_correction_formant_compensation = None;
3877                }
3878                #[cfg(unix)]
3879                track.clip_pitch_shifters.clear();
3880            }
3881            Kind::MIDI => {
3882                if let Some(clip) = track.midi.clips.get_mut(clip_index) {
3883                    clip.start = start;
3884                    clip.end = start.saturating_add(length.max(1));
3885                    clip.offset = offset;
3886                }
3887            }
3888        }
3889    }
3890
3891    fn set_clip_source_name(&self, track_name: &str, clip_index: usize, kind: Kind, name: String) {
3892        let Some(track) = self.state.lock().tracks.get(track_name) else {
3893            return;
3894        };
3895        let track = track.lock();
3896        match kind {
3897            Kind::Audio => {
3898                if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3899                    clip.name = name;
3900                }
3901                #[cfg(unix)]
3902                track.clip_pitch_shifters.clear();
3903            }
3904            Kind::MIDI => {
3905                if let Some(clip) = track.midi.clips.get_mut(clip_index) {
3906                    clip.name = name;
3907                }
3908            }
3909        }
3910    }
3911
3912    fn set_clip_muted(&self, track_name: &str, clip_index: usize, kind: Kind, muted: bool) {
3913        let Some(track) = self.state.lock().tracks.get(track_name) else {
3914            return;
3915        };
3916        let track = track.lock();
3917        match kind {
3918            Kind::Audio => {
3919                if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3920                    clip.muted = muted;
3921                }
3922            }
3923            Kind::MIDI => {
3924                if let Some(clip) = track.midi.clips.get_mut(clip_index) {
3925                    clip.muted = muted;
3926                }
3927            }
3928        }
3929    }
3930
3931    #[allow(clippy::too_many_arguments)]
3932    fn set_clip_pitch_correction(
3933        &self,
3934        track_name: &str,
3935        clip_index: usize,
3936        preview_name: Option<String>,
3937        source_name: Option<String>,
3938        source_offset: Option<usize>,
3939        source_length: Option<usize>,
3940        pitch_correction_points: Vec<crate::message::PitchCorrectionPointData>,
3941        pitch_correction_frame_likeness: Option<f32>,
3942        pitch_correction_inertia_ms: Option<u16>,
3943        pitch_correction_formant_compensation: Option<bool>,
3944    ) {
3945        if let Some(track) = self.state.lock().tracks.get(track_name) {
3946            let track = track.lock();
3947            if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3948                clip.pitch_correction_preview_name = preview_name;
3949                clip.pitch_correction_source_name = source_name;
3950                clip.pitch_correction_source_offset = source_offset;
3951                clip.pitch_correction_source_length = source_length;
3952                clip.pitch_correction_points = pitch_correction_points;
3953                clip.pitch_correction_frame_likeness = pitch_correction_frame_likeness;
3954                clip.pitch_correction_inertia_ms = pitch_correction_inertia_ms;
3955                clip.pitch_correction_formant_compensation = pitch_correction_formant_compensation;
3956            }
3957            #[cfg(unix)]
3958            track.clip_pitch_shifters.clear();
3959        }
3960    }
3961
3962    async fn request_hw_cycle(&mut self) {
3963        if self.awaiting_hwfinished {
3964            tracing::debug!("request_hw_cycle skipped (already awaiting)");
3965            return;
3966        }
3967        tracing::debug!("request_hw_cycle sending TracksFinished");
3968        self.apply_hw_out_gain_and_meter().await;
3969        if let Some((after_frames, loop_start, cycle_end_sample)) =
3970            self.scheduled_loop_wrap_for_next_cycle()
3971        {
3972            self.notified_loop_wrap_sample = Some(cycle_end_sample);
3973            self.notify_clients(Ok(Action::TransportPositionAt {
3974                sample: loop_start,
3975                after_frames,
3976            }))
3977            .await;
3978        } else {
3979            self.notified_loop_wrap_sample = None;
3980        }
3981        if let Some(worker) = &self.hw_worker {
3982            if !self.pending_hw_midi_out_events_by_device.is_empty() {
3983                let out_events = std::mem::take(&mut self.pending_hw_midi_out_events_by_device);
3984                if let Err(e) = worker.tx.send(Message::HWMidiOutEvents(out_events)).await {
3985                    error!("Error sending HWMidiOutEvents {e}");
3986                }
3987            }
3988            match worker.tx.send(Message::TracksFinished).await {
3989                Ok(_) => {
3990                    self.awaiting_hwfinished = true;
3991                }
3992                Err(e) => {
3993                    error!("Error sending TracksFinished {e}");
3994                }
3995            }
3996        }
3997    }
3998
3999    async fn clear_hw_midi_output_state(&mut self, send_panic: bool) {
4000        self.pending_hw_midi_out_events.clear();
4001        self.pending_hw_midi_out_events_by_device.clear();
4002        {
4003            let state = self.state.lock();
4004            for track in state.tracks.values() {
4005                track.lock().take_hw_midi_out_events();
4006            }
4007        }
4008
4009        let panic_events = if send_panic {
4010            self.note_off_events_for_all_active_tracks()
4011        } else {
4012            vec![]
4013        };
4014
4015        if let Some(worker) = &self.hw_worker {
4016            if let Err(e) = worker.tx.send(Message::ClearHWMidiOutEvents).await {
4017                error!("Error clearing pending HWMidiOutEvents {e}");
4018            }
4019            if !panic_events.is_empty()
4020                && let Err(e) = worker.tx.send(Message::HWMidiOutEvents(panic_events)).await
4021            {
4022                error!("Error sending transport restart MIDI panic events {e}");
4023            }
4024        } else if !panic_events.is_empty() {
4025            self.pending_hw_midi_out_events_by_device
4026                .extend(panic_events);
4027        }
4028    }
4029
4030    fn invalidate_track_cycle_state(&mut self) {
4031        self.track_process_epoch = self.track_process_epoch.saturating_add(1);
4032        self.task_processing_started_at.clear();
4033        self.cycle_tasks.clear();
4034        self.cycle_task_deps.clear();
4035        self.cycle_tasks_running.clear();
4036        self.cycle_tasks_finished.clear();
4037        let state = self.state.lock();
4038        for track in state.tracks.values() {
4039            let t = track.lock();
4040            t.audio.finished = false;
4041            t.audio.processing = false;
4042        }
4043    }
4044
4045    fn force_stalled_task_completions(&mut self) {
4046        let now = Instant::now();
4047        let running: Vec<ProcessTask> = self.cycle_tasks_running.clone();
4048        for task in running {
4049            let key = Self::task_key(&task);
4050            let Some(started) = self.task_processing_started_at.get(&key).copied() else {
4051                continue;
4052            };
4053            if now.duration_since(started) < Self::TRACK_PROCESS_TIMEOUT {
4054                continue;
4055            }
4056            if Self::task_running_finished_contains(&self.cycle_tasks_finished, &task) {
4057                self.task_processing_started_at.remove(&key);
4058                continue;
4059            }
4060            let track = match &task {
4061                ProcessTask::Track(t)
4062                | ProcessTask::FolderInput(t)
4063                | ProcessTask::FolderOutput(t) => t.clone(),
4064                ProcessTask::Plugin { track, .. } => track.clone(),
4065            };
4066            {
4067                let t = track.lock();
4068                if t.audio.finished || !t.audio.processing {
4069                    self.task_processing_started_at.remove(&key);
4070                    continue;
4071                }
4072                for out in &t.audio.outs {
4073                    out.buffer.lock().fill(0.0);
4074                    *out.finished.lock() = true;
4075                }
4076                t.audio.processing = false;
4077                t.audio.finished = true;
4078            }
4079            self.cycle_tasks_running
4080                .retain(|t| Self::task_key(t) != key);
4081            self.cycle_tasks_finished.push(task.clone());
4082            self.task_processing_started_at.remove(&key);
4083            tracing::warn!(
4084                "Task '{}' exceeded process timeout ({} ms); forcing silent completion for cycle",
4085                Self::task_track_name(&task),
4086                Self::TRACK_PROCESS_TIMEOUT.as_millis()
4087            );
4088        }
4089    }
4090
4091    fn should_publish_hw_out_meters(&mut self) -> bool {
4092        let now = Instant::now();
4093        match self.last_hw_out_meter_publish {
4094            Some(last) if now.duration_since(last) < Self::METER_PUBLISH_INTERVAL => false,
4095            _ => {
4096                self.last_hw_out_meter_publish = Some(now);
4097                true
4098            }
4099        }
4100    }
4101
4102    fn should_publish_track_meters(&mut self) -> bool {
4103        let now = Instant::now();
4104        match self.last_track_meter_publish {
4105            Some(last) if now.duration_since(last) < Self::METER_PUBLISH_INTERVAL => false,
4106            _ => {
4107                self.last_track_meter_publish = Some(now);
4108                true
4109            }
4110        }
4111    }
4112
4113    fn should_publish_hw_out_linear(&mut self, peaks_linear: &[f32]) -> bool {
4114        #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
4115        {
4116            self.hw_out_meter_publish_phase = !self.hw_out_meter_publish_phase;
4117            if !self.hw_out_meter_publish_phase {
4118                return false;
4119            }
4120            let changed = if self.last_hw_out_meter_linear.len() != peaks_linear.len() {
4121                true
4122            } else {
4123                self.last_hw_out_meter_linear
4124                    .iter()
4125                    .zip(peaks_linear.iter())
4126                    .any(|(prev, next)| (prev - next).abs() >= Self::HW_OUT_METER_LINEAR_EPSILON)
4127            };
4128            if !changed {
4129                return false;
4130            }
4131            self.last_hw_out_meter_linear.clear();
4132            self.last_hw_out_meter_linear
4133                .extend_from_slice(peaks_linear);
4134            true
4135        }
4136        #[cfg(not(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd")))]
4137        {
4138            let _ = peaks_linear;
4139            false
4140        }
4141    }
4142
4143    async fn maybe_notify_hw_out_meter(&mut self, _meter_db: Vec<f32>) {
4144        {}
4145    }
4146
4147    fn collect_changed_track_meters(
4148        &mut self,
4149        _tracks: &[(String, Arc<UnsafeMutex<Box<Track>>>)],
4150    ) -> Vec<(String, Vec<f32>)> {
4151        Vec::new()
4152    }
4153
4154    async fn apply_hw_out_gain_and_meter(&mut self) {
4155        let gain = if self.hw_out_muted {
4156            0.0
4157        } else {
4158            10.0_f32.powf(self.hw_out_level_db / 20.0)
4159        };
4160        let should_notify_interval = self.should_publish_hw_out_meters();
4161        if let Some(oss) = self.hw_driver.clone() {
4162            let hw = oss.lock();
4163            hw.set_output_gain_balance(gain, self.hw_out_balance);
4164            if !should_notify_interval {
4165                return;
4166            }
4167        } else {
4168            #[cfg(unix)]
4169            {
4170                if let Some(jack) = self.jack_runtime.clone() {
4171                    jack.lock().set_output_gain_linear(gain);
4172                    jack.lock().set_output_balance(self.hw_out_balance);
4173                    if !should_notify_interval {
4174                        return;
4175                    }
4176                } else {
4177                    return;
4178                }
4179            }
4180            #[cfg(not(unix))]
4181            {
4182                return;
4183            }
4184        }
4185        let peaks_linear = if let Some(oss) = self.hw_driver.clone() {
4186            oss.lock().output_meter_linear(gain, self.hw_out_balance)
4187        } else {
4188            #[cfg(unix)]
4189            {
4190                if let Some(jack) = self.jack_runtime.clone() {
4191                    let outs = jack.lock().audio_outs();
4192                    let out_count = outs.len();
4193                    let b = if out_count == 2 {
4194                        self.hw_out_balance.clamp(-1.0, 1.0)
4195                    } else {
4196                        0.0
4197                    };
4198                    let mut meters_linear = Vec::with_capacity(out_count);
4199                    for (channel_idx, channel) in outs.iter().enumerate() {
4200                        let balance_gain = if out_count == 2 {
4201                            if channel_idx == 0 {
4202                                (1.0 - b).clamp(0.0, 1.0)
4203                            } else {
4204                                (1.0 + b).clamp(0.0, 1.0)
4205                            }
4206                        } else {
4207                            1.0
4208                        };
4209                        let buf = channel.buffer.lock();
4210                        let peak = crate::simd::peak_abs(buf) * gain * balance_gain;
4211                        meters_linear.push(peak);
4212                    }
4213                    meters_linear
4214                } else {
4215                    return;
4216                }
4217            }
4218            #[cfg(not(unix))]
4219            {
4220                return;
4221            }
4222        };
4223        if self.hw_out_peak_hold_linear.len() != peaks_linear.len() {
4224            self.hw_out_peak_hold_linear.resize(peaks_linear.len(), 0.0);
4225        }
4226        let mut held_peaks = Vec::with_capacity(peaks_linear.len());
4227        for (idx, peak_now) in peaks_linear.iter().copied().enumerate() {
4228            let held = self.hw_out_peak_hold_linear[idx] * 0.92;
4229            let next = peak_now.max(held);
4230            self.hw_out_peak_hold_linear[idx] = next;
4231            held_peaks.push(next);
4232        }
4233        let should_notify =
4234            should_notify_interval && self.should_publish_hw_out_linear(&held_peaks);
4235        let meter_db: Vec<f32> = held_peaks
4236            .into_iter()
4237            .map(Self::meter_linear_to_db)
4238            .collect();
4239        self.latest_hw_out_meter_db = Arc::new(meter_db.clone());
4240        if should_notify {
4241            self.maybe_notify_hw_out_meter(meter_db).await;
4242        }
4243    }
4244
4245    fn preload_track_clips_spawn(&self) {
4246        let tracks: Vec<_> = self.state.lock().tracks.values().cloned().collect();
4247        for track in tracks {
4248            tokio::task::spawn_blocking(move || {
4249                track.lock().preload_clips();
4250            });
4251        }
4252    }
4253
4254    async fn preload_track_clips(&self) {
4255        let tracks: Vec<_> = self.state.lock().tracks.values().cloned().collect();
4256        if tracks.is_empty() {
4257            return;
4258        }
4259        let mut handles = Vec::with_capacity(tracks.len());
4260        for track in tracks {
4261            handles.push(tokio::task::spawn_blocking(move || {
4262                track.lock().preload_clips();
4263            }));
4264        }
4265        for handle in handles {
4266            if let Err(e) = handle.await {
4267                tracing::warn!("Clip preload task panicked: {e}");
4268            }
4269        }
4270    }
4271
4272    fn build_task_graph(
4273        &self,
4274    ) -> (
4275        Vec<ProcessTask>,
4276        std::collections::HashMap<String, Vec<String>>,
4277    ) {
4278        let state = self.state.lock();
4279        let ordered: Vec<(String, Arc<UnsafeMutex<Box<Track>>>)> = state
4280            .tracks
4281            .iter()
4282            .map(|(name, track)| (name.clone(), track.clone()))
4283            .collect();
4284        let mut tasks = Vec::new();
4285        let mut deps = std::collections::HashMap::new();
4286
4287        for (_name, track) in &ordered {
4288            let t = track.lock();
4289            if t.parent_track.is_some() {
4290                continue;
4291            }
4292            self.append_track_tasks(track.clone(), None, &mut tasks, &mut deps);
4293        }
4294
4295        (tasks, deps)
4296    }
4297
4298    fn append_track_tasks(
4299        &self,
4300        track: Arc<UnsafeMutex<Box<Track>>>,
4301        predecessor: Option<String>,
4302        tasks: &mut Vec<ProcessTask>,
4303        deps: &mut std::collections::HashMap<String, Vec<String>>,
4304    ) -> (String, String) {
4305        use crate::message::ConnectableRef;
4306        let t = track.lock();
4307        if t.is_folder {
4308            let folder_input = ProcessTask::FolderInput(track.clone());
4309            let folder_input_key = Self::task_key(&folder_input);
4310            tasks.push(folder_input.clone());
4311            let folder_input_deps: Vec<_> = predecessor.into_iter().collect();
4312            deps.insert(folder_input_key.clone(), folder_input_deps);
4313
4314            let mut source_keys: std::collections::HashMap<ConnectableRef, String> =
4315                std::collections::HashMap::new();
4316            let mut target_keys: std::collections::HashMap<ConnectableRef, String> =
4317                std::collections::HashMap::new();
4318            source_keys.insert(ConnectableRef::TrackInput, folder_input_key.clone());
4319            target_keys.insert(ConnectableRef::TrackInput, folder_input_key.clone());
4320
4321            let mut plugin_keys: Vec<String> = Vec::new();
4322            for idx in 0..t.clap_plugins.len() {
4323                let plugin_task = ProcessTask::Plugin {
4324                    track: track.clone(),
4325                    kind: PluginKind::Clap,
4326                    index: idx,
4327                };
4328                let plugin_key = Self::task_key(&plugin_task);
4329                let id = t.clap_plugins[idx].id;
4330                source_keys.insert(ConnectableRef::ClapPlugin(id), plugin_key.clone());
4331                target_keys.insert(ConnectableRef::ClapPlugin(id), plugin_key.clone());
4332                tasks.push(plugin_task);
4333                deps.insert(plugin_key.clone(), vec![folder_input_key.clone()]);
4334                plugin_keys.push(plugin_key);
4335            }
4336            for idx in 0..t.vst3_plugins.len() {
4337                let plugin_task = ProcessTask::Plugin {
4338                    track: track.clone(),
4339                    kind: PluginKind::Vst3,
4340                    index: idx,
4341                };
4342                let plugin_key = Self::task_key(&plugin_task);
4343                let id = t.vst3_plugins[idx].id;
4344                source_keys.insert(ConnectableRef::Vst3Plugin(id), plugin_key.clone());
4345                target_keys.insert(ConnectableRef::Vst3Plugin(id), plugin_key.clone());
4346                tasks.push(plugin_task);
4347                deps.insert(plugin_key.clone(), vec![folder_input_key.clone()]);
4348                plugin_keys.push(plugin_key);
4349            }
4350            #[cfg(all(unix, not(target_os = "macos")))]
4351            for idx in 0..t.lv2_plugins.len() {
4352                let plugin_task = ProcessTask::Plugin {
4353                    track: track.clone(),
4354                    kind: PluginKind::Lv2,
4355                    index: idx,
4356                };
4357                let plugin_key = Self::task_key(&plugin_task);
4358                let id = t.lv2_plugins[idx].id;
4359                source_keys.insert(ConnectableRef::Lv2Plugin(id), plugin_key.clone());
4360                target_keys.insert(ConnectableRef::Lv2Plugin(id), plugin_key.clone());
4361                tasks.push(plugin_task);
4362                deps.insert(plugin_key.clone(), vec![folder_input_key.clone()]);
4363                plugin_keys.push(plugin_key);
4364            }
4365
4366            let mut child_keys = Vec::new();
4367            for child_track in &t.child_tracks {
4368                let (child_first, child_last) = self.append_track_tasks(
4369                    child_track.clone(),
4370                    Some(folder_input_key.clone()),
4371                    tasks,
4372                    deps,
4373                );
4374                let child_name = child_track.lock().name.clone();
4375                source_keys.insert(
4376                    ConnectableRef::ChildTrack(child_name.clone()),
4377                    child_last.clone(),
4378                );
4379                target_keys.insert(ConnectableRef::ChildTrack(child_name), child_first.clone());
4380                child_keys.push((child_first, child_last.clone()));
4381            }
4382
4383            let folder_output = ProcessTask::FolderOutput(track.clone());
4384            let folder_output_key = Self::task_key(&folder_output);
4385            source_keys.insert(ConnectableRef::TrackOutput, folder_output_key.clone());
4386            target_keys.insert(ConnectableRef::TrackOutput, folder_output_key.clone());
4387            tasks.push(folder_output.clone());
4388            let mut folder_output_deps = vec![folder_input_key.clone()];
4389            folder_output_deps.extend(plugin_keys);
4390            folder_output_deps.extend(child_keys.iter().map(|(_, last)| last.clone()));
4391            deps.insert(folder_output_key.clone(), folder_output_deps);
4392
4393            // Add cross-connectable dependencies based on the track's routing graph.
4394            // This includes child->plugin, plugin->folder output, plugin->plugin, etc.
4395            for conn in t.connectable_connections() {
4396                let Some(source_key) = source_keys.get(&conn.from) else {
4397                    continue;
4398                };
4399                let Some(target_key) = target_keys.get(&conn.to) else {
4400                    continue;
4401                };
4402                if source_key == target_key {
4403                    continue;
4404                }
4405                let entry = deps.entry(target_key.clone()).or_default();
4406                if !entry.contains(source_key) {
4407                    entry.push(source_key.clone());
4408                }
4409            }
4410
4411            (folder_input_key, folder_output_key)
4412        } else {
4413            let task = ProcessTask::Track(track.clone());
4414            let task_key = Self::task_key(&task);
4415            tasks.push(task.clone());
4416            deps.insert(
4417                task_key.clone(),
4418                predecessor.into_iter().collect::<Vec<_>>(),
4419            );
4420            (task_key.clone(), task_key)
4421        }
4422    }
4423
4424    fn task_track_name(task: &ProcessTask) -> String {
4425        match task {
4426            ProcessTask::Track(t) | ProcessTask::FolderInput(t) | ProcessTask::FolderOutput(t) => {
4427                t.lock().name.clone()
4428            }
4429            ProcessTask::Plugin { track, .. } => track.lock().name.clone(),
4430        }
4431    }
4432
4433    fn task_key(task: &ProcessTask) -> String {
4434        match task {
4435            ProcessTask::Track(t) => format!("Track:{:p}", std::sync::Arc::as_ptr(t)),
4436            ProcessTask::FolderInput(t) => {
4437                format!("FolderInput:{:p}", std::sync::Arc::as_ptr(t))
4438            }
4439            ProcessTask::FolderOutput(t) => {
4440                format!("FolderOutput:{:p}", std::sync::Arc::as_ptr(t))
4441            }
4442            ProcessTask::Plugin { track, kind, index } => format!(
4443                "Plugin:{:?}:{:p}:{}",
4444                kind,
4445                std::sync::Arc::as_ptr(track),
4446                index
4447            ),
4448        }
4449    }
4450
4451    fn task_running_finished_contains(haystack: &[ProcessTask], needle: &ProcessTask) -> bool {
4452        let needle_key = Self::task_key(needle);
4453        haystack.iter().any(|t| Self::task_key(t) == needle_key)
4454    }
4455
4456    fn task_ready(&self, task: &ProcessTask) -> bool {
4457        match task {
4458            ProcessTask::Track(t) | ProcessTask::FolderInput(t) => {
4459                let track = t.lock();
4460                let ready = track.audio.ready();
4461                if !ready {
4462                    let task_kind = match task {
4463                        ProcessTask::Track(_) => "Track",
4464                        ProcessTask::FolderInput(_) => "FolderInput",
4465                        _ => "?",
4466                    };
4467                    let mut input_status = Vec::new();
4468                    for (idx, input) in track.audio.ins.iter().enumerate() {
4469                        let finished = *input.finished.lock();
4470                        let conn_count = input.connection_count.load(Ordering::Relaxed);
4471                        let mut pending = Vec::new();
4472                        for conn in input.connections.lock().iter() {
4473                            pending.push(*conn.finished.lock());
4474                        }
4475                        input_status.push(format!(
4476                            "in{}: finished={} conns={} pending_finished={:?}",
4477                            idx, finished, conn_count, pending
4478                        ));
4479                    }
4480                    tracing::info!(
4481                        "task not ready for '{}' ({}): {}",
4482                        track.name,
4483                        task_kind,
4484                        input_status.join(", ")
4485                    );
4486                }
4487                ready
4488            }
4489            ProcessTask::Plugin { .. } | ProcessTask::FolderOutput(_) => true,
4490        }
4491    }
4492
4493    fn task_dependencies_satisfied(&self, task: &ProcessTask) -> bool {
4494        let key = Self::task_key(task);
4495        let Some(deps) = self.cycle_task_deps.get(&key) else {
4496            return true;
4497        };
4498        let finished_keys: std::collections::HashSet<String> = self
4499            .cycle_tasks_finished
4500            .iter()
4501            .map(Self::task_key)
4502            .collect();
4503        deps.iter().all(|d| finished_keys.contains(d))
4504    }
4505
4506    fn prepare_task_track(&self, task: &ProcessTask) {
4507        let track = match task {
4508            ProcessTask::Track(t) | ProcessTask::FolderInput(t) | ProcessTask::FolderOutput(t) => t,
4509            ProcessTask::Plugin { track, .. } => track,
4510        };
4511        let t = track.lock();
4512        t.set_transport_sample(self.transport_sample);
4513        t.set_loop_config(self.loop_enabled, self.loop_range_samples);
4514        t.set_transport_timing(self.tempo_bpm, self.tsig_num, self.tsig_denom);
4515        t.process_epoch = self.track_process_epoch;
4516        t.set_clip_playback_enabled(self.clip_playback_enabled && self.playing);
4517        t.set_record_tap_enabled(self.playing && self.record_enabled);
4518        t.audio.processing = true;
4519    }
4520
4521    async fn send_tasks(&mut self) -> bool {
4522        if !self.playing {
4523            return false;
4524        }
4525        self.refresh_realtime_infection();
4526        self.force_stalled_task_completions();
4527
4528        if self.cycle_tasks.is_empty() {
4529            let (tasks, deps) = self.build_task_graph();
4530            let task_names: Vec<String> = tasks.iter().map(Self::task_track_name).collect();
4531            tracing::debug!(
4532                "send_tasks rebuilt graph: {} tasks ({:?})",
4533                tasks.len(),
4534                task_names
4535            );
4536            self.cycle_tasks = tasks;
4537            self.cycle_task_deps = deps;
4538            self.cycle_tasks_running.clear();
4539            self.cycle_tasks_finished.clear();
4540        }
4541
4542        let mut finished = true;
4543        let mut dispatched = 0;
4544        loop {
4545            let next_task = {
4546                let mut next = None;
4547                tracing::debug!(
4548                    "selecting next: cycle={} running={} finished={}",
4549                    self.cycle_tasks.len(),
4550                    self.cycle_tasks_running.len(),
4551                    self.cycle_tasks_finished.len()
4552                );
4553                for task in &self.cycle_tasks {
4554                    let in_running =
4555                        Self::task_running_finished_contains(&self.cycle_tasks_running, task);
4556                    let in_finished =
4557                        Self::task_running_finished_contains(&self.cycle_tasks_finished, task);
4558                    tracing::debug!(
4559                        "checking task {} in_running={} in_finished={}",
4560                        Self::task_track_name(task),
4561                        in_running,
4562                        in_finished
4563                    );
4564                    if in_finished || in_running {
4565                        continue;
4566                    }
4567                    finished = false;
4568                    if !self.task_dependencies_satisfied(task) {
4569                        continue;
4570                    }
4571                    if !self.task_ready(task) {
4572                        continue;
4573                    }
4574                    next = Some(task.clone());
4575                    break;
4576                }
4577                next
4578            };
4579
4580            let Some(task) = next_task else {
4581                if !finished && dispatched == 0 {
4582                    tracing::info!(
4583                        "send_tasks returning finished={} (dispatched {})",
4584                        finished,
4585                        dispatched
4586                    );
4587                } else {
4588                    tracing::debug!(
4589                        "send_tasks returning finished={} (dispatched {})",
4590                        finished,
4591                        dispatched
4592                    );
4593                }
4594                return finished;
4595            };
4596            let Some(worker_index) = self.take_ready_worker_index() else {
4597                self.force_stalled_task_completions();
4598                tracing::debug!(
4599                    "send_tasks returning false (no ready worker; dispatched {})",
4600                    dispatched
4601                );
4602                return false;
4603            };
4604
4605            if Self::task_running_finished_contains(&self.cycle_tasks_finished, &task)
4606                || Self::task_running_finished_contains(&self.cycle_tasks_running, &task)
4607            {
4608                continue;
4609            }
4610            dispatched += 1;
4611            let task_key = Self::task_key(&task);
4612            tracing::info!(
4613                "send_tasks dispatching {} (running={} finished={})",
4614                Self::task_track_name(&task),
4615                self.cycle_tasks_running.len(),
4616                self.cycle_tasks_finished.len()
4617            );
4618            self.prepare_task_track(&task);
4619            self.cycle_tasks_running.push(task.clone());
4620            tracing::debug!(
4621                "inserted task {} -> running_size={}",
4622                Self::task_track_name(&task),
4623                self.cycle_tasks_running.len()
4624            );
4625            self.task_processing_started_at
4626                .insert(task_key.clone(), Instant::now());
4627            let worker = &self.workers[worker_index];
4628            if let Err(e) = worker.tx.send(Message::ProcessTask(task.clone())).await {
4629                self.cycle_tasks_running
4630                    .retain(|t| Self::task_key(t) != task_key);
4631                self.task_processing_started_at.remove(&task_key);
4632                self.notify_clients(Err(format!("Failed to send task to worker: {}", e)))
4633                    .await;
4634            }
4635        }
4636    }
4637
4638    async fn on_all_tracks_finished(&mut self) {
4639        if self.transport_restart_pending {
4640            let state = self.state.lock();
4641            for track in state.tracks.values() {
4642                track.lock().take_hw_midi_out_events();
4643            }
4644        } else if self.hw_worker.is_some() {
4645            self.active_hw_notes_cycle_start = self.active_hw_notes_by_track.clone();
4646            let mut out_events = self.collect_hw_midi_output_events_by_device();
4647            if self.loop_enabled
4648                && let Some((_, loop_end)) = self.loop_range_samples
4649            {
4650                let cycle_end = self
4651                    .transport_sample
4652                    .saturating_add(self.current_cycle_samples());
4653                if self.transport_sample < loop_end && cycle_end >= loop_end {
4654                    let wrap_frame = loop_end
4655                        .saturating_sub(self.transport_sample)
4656                        .min(self.current_cycle_samples())
4657                        as u32;
4658                    out_events.extend(self.note_off_events_for_active_snapshot(
4659                        &self.active_hw_notes_cycle_start,
4660                        wrap_frame,
4661                    ));
4662                    out_events.sort_by(|a, b| {
4663                        a.event
4664                            .frame
4665                            .cmp(&b.event.frame)
4666                            .then_with(|| a.device.cmp(&b.device))
4667                    });
4668                }
4669            }
4670            self.pending_hw_midi_out_events_by_device.extend(out_events);
4671        } else {
4672            self.pending_hw_midi_out_events = self.collect_hw_midi_output_events();
4673        }
4674        self.request_hw_cycle().await;
4675    }
4676
4677    fn take_ready_worker_index(&mut self) -> Option<usize> {
4678        while !self.ready_workers.is_empty() {
4679            let worker_index = self.ready_workers.remove(0);
4680            if worker_index < self.workers.len() {
4681                return Some(worker_index);
4682            }
4683        }
4684        None
4685    }
4686
4687    fn push_ready_worker(&mut self, worker_index: usize) {
4688        self.ready_workers.push(worker_index);
4689    }
4690
4691    async fn publish_track_meters(&mut self) {
4692        if !self.should_publish_track_meters() {
4693            return;
4694        }
4695        let tracks: Vec<(String, Arc<UnsafeMutex<Box<Track>>>)> = self
4696            .state
4697            .lock()
4698            .tracks
4699            .iter()
4700            .map(|(name, track)| (name.clone(), track.clone()))
4701            .collect();
4702        let mut snapshot = Vec::with_capacity(tracks.len());
4703        for (name, track) in &tracks {
4704            let linear = self
4705                .track_meter_linear_by_track
4706                .get(name)
4707                .cloned()
4708                .unwrap_or_else(|| track.lock().output_meter_linear());
4709            let output_db = linear
4710                .iter()
4711                .copied()
4712                .map(Self::meter_linear_to_db)
4713                .collect::<Vec<_>>();
4714            snapshot.push((name.clone(), output_db));
4715        }
4716        self.latest_track_meter_snapshot = Arc::new(snapshot);
4717        let meters = self.collect_changed_track_meters(&tracks);
4718        for (track_name, output_db) in meters {
4719            self.notify_clients(Ok(Action::TrackMeters {
4720                track_name,
4721                output_db,
4722            }))
4723            .await;
4724        }
4725    }
4726
4727    async fn publish_session_runtime_reports(&mut self) {
4728        if self
4729            .last_session_report_publish
4730            .is_some_and(|t| t.elapsed() < Self::SESSION_RUNTIME_REPORT_INTERVAL)
4731        {
4732            return;
4733        }
4734
4735        let mut current = HashMap::<(String, usize), (SessionSlotState, usize)>::new();
4736        {
4737            let state = self.state.lock();
4738            for (track_name, track) in &state.tracks {
4739                let track = track.lock();
4740                for launch in &track.pending_session_launches {
4741                    current.insert(
4742                        (track_name.clone(), launch.scene_index),
4743                        (SessionSlotState::Queued, 0),
4744                    );
4745                }
4746                for clip in &track.playing_session_clips {
4747                    current.insert(
4748                        (track_name.clone(), clip.scene_index),
4749                        (SessionSlotState::Playing, clip.play_position_samples),
4750                    );
4751                }
4752            }
4753        }
4754
4755        let previous_keys: Vec<(String, usize)> =
4756            self.session_report_state.keys().cloned().collect();
4757        for key in previous_keys {
4758            if current.contains_key(&key) {
4759                continue;
4760            }
4761            let (track_name, scene_index) = key;
4762            self.notify_clients(Ok(Action::SessionRuntimeReport {
4763                track_name,
4764                scene_index,
4765                state: SessionSlotState::Stopped,
4766                play_position_samples: 0,
4767            }))
4768            .await;
4769        }
4770
4771        for ((track_name, scene_index), (state, play_position_samples)) in &current {
4772            self.notify_clients(Ok(Action::SessionRuntimeReport {
4773                track_name: track_name.clone(),
4774                scene_index: *scene_index,
4775                state: *state,
4776                play_position_samples: *play_position_samples,
4777            }))
4778            .await;
4779        }
4780
4781        self.session_report_state = current.into_iter().map(|(k, (s, _))| (k, s)).collect();
4782        self.last_session_report_publish = Some(Instant::now());
4783    }
4784
4785    async fn publish_clap_state_dirty(&mut self) {
4786        let tracks: Vec<(String, Arc<UnsafeMutex<Box<Track>>>)> = self
4787            .state
4788            .lock()
4789            .tracks
4790            .iter()
4791            .map(|(name, track)| (name.clone(), track.clone()))
4792            .collect();
4793        for (track_name, track) in &tracks {
4794            let dirty = track.lock().take_dirty_clap_instances();
4795            for instance_id in dirty {
4796                self.notify_clients(Ok(Action::TrackClapStateDirty {
4797                    track_name: track_name.clone(),
4798                    instance_id,
4799                }))
4800                .await;
4801            }
4802        }
4803    }
4804
4805    fn reset_meters_after_stop(&mut self) {
4806        self.last_hw_out_meter_publish = None;
4807        self.last_track_meter_publish = None;
4808        self.hw_out_peak_hold_linear.fill(0.0);
4809        #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
4810        {
4811            self.last_hw_out_meter_linear.clear();
4812        }
4813        let hw_channels = self.latest_hw_out_meter_db.len();
4814        self.latest_hw_out_meter_db = Arc::new(vec![-90.0; hw_channels]);
4815
4816        let tracks: Vec<(String, Arc<UnsafeMutex<Box<Track>>>)> = self
4817            .state
4818            .lock()
4819            .tracks
4820            .iter()
4821            .map(|(name, track)| (name.clone(), track.clone()))
4822            .collect();
4823        self.track_meter_linear_by_track.clear();
4824        let mut snapshot = Vec::with_capacity(tracks.len());
4825        for (name, track) in tracks {
4826            let t = track.lock();
4827            t.clear_output_meters();
4828            let width = t.output_meter_linear().len();
4829            let zero_linear = vec![0.0; width];
4830            self.track_meter_linear_by_track
4831                .insert(name.clone(), zero_linear);
4832            snapshot.push((name, vec![-90.0; width]));
4833        }
4834        self.latest_track_meter_snapshot = Arc::new(snapshot);
4835    }
4836
4837    pub fn check_if_leads_to_kind(
4838        &self,
4839        kind: Kind,
4840        current_track_name: &str,
4841        target_track_name: &str,
4842    ) -> bool {
4843        routing::would_create_cycle(
4844            &target_track_name.to_string(),
4845            &current_track_name.to_string(),
4846            |track_name| self.connected_neighbors(kind, track_name),
4847        )
4848    }
4849
4850    fn connected_neighbors(&self, kind: Kind, current_track_name: &str) -> Vec<String> {
4851        let state = self.state.lock();
4852        let mut found_neighbors = Vec::new();
4853
4854        if let Some(current_track_handle) = state.tracks.get(current_track_name) {
4855            let current_track = current_track_handle.lock();
4856
4857            match kind {
4858                Kind::Audio => {
4859                    for out_port in &current_track.audio.outs {
4860                        let conns = out_port.connections.lock();
4861                        for conn in conns.iter() {
4862                            for (name, next_track_handle) in &state.tracks {
4863                                let next_track = next_track_handle.lock();
4864                                let is_connected =
4865                                    next_track.audio.ins.iter().any(|ins_port| {
4866                                        Arc::ptr_eq(&ins_port.buffer, &conn.buffer)
4867                                    });
4868
4869                                if is_connected {
4870                                    found_neighbors.push(name.clone());
4871                                }
4872                            }
4873                        }
4874                    }
4875                }
4876                Kind::MIDI => {
4877                    for out_port in &current_track.midi.outs {
4878                        let conns = out_port.lock().connections.clone();
4879                        for conn in conns.iter() {
4880                            for (name, next_track_handle) in &state.tracks {
4881                                let next_track = next_track_handle.lock();
4882                                let is_connected = next_track
4883                                    .midi
4884                                    .ins
4885                                    .iter()
4886                                    .any(|ins_port| Arc::ptr_eq(ins_port, conn));
4887
4888                                if is_connected {
4889                                    found_neighbors.push(name.clone());
4890                                }
4891                            }
4892                        }
4893                    }
4894                }
4895            }
4896        }
4897        found_neighbors
4898    }
4899
4900    async fn handle_request(&mut self, a: Action) {
4901        match a {
4902            Action::Log { source, message } => {
4903                self.notify_clients(Ok(Action::Log { source, message }))
4904                    .await;
4905            }
4906            Action::Undo => {
4907                let actions = match self.history.undo() {
4908                    Some(actions) => actions,
4909                    None => {
4910                        self.notify_clients(Ok(Action::Undo)).await;
4911                        self.notify_clients(Ok(Action::HistoryState {
4912                            dirty: self.history.is_dirty(),
4913                        }))
4914                        .await;
4915                        return;
4916                    }
4917                };
4918
4919                let was_suspended = self.history_suspended;
4920                self.history_suspended = true;
4921                for action in actions {
4922                    self.handle_request_inner(action, false).await;
4923                }
4924                self.history_suspended = was_suspended;
4925                self.notify_clients(Ok(Action::Undo)).await;
4926                self.notify_clients(Ok(Action::HistoryState {
4927                    dirty: self.history.is_dirty(),
4928                }))
4929                .await;
4930            }
4931            Action::Redo => {
4932                let actions = match self.history.redo() {
4933                    Some(actions) => actions,
4934                    None => {
4935                        self.notify_clients(Ok(Action::Redo)).await;
4936                        self.notify_clients(Ok(Action::HistoryState {
4937                            dirty: self.history.is_dirty(),
4938                        }))
4939                        .await;
4940                        return;
4941                    }
4942                };
4943
4944                let was_suspended = self.history_suspended;
4945                self.history_suspended = true;
4946                for action in actions {
4947                    self.handle_request_inner(action, false).await;
4948                }
4949                self.history_suspended = was_suspended;
4950                self.notify_clients(Ok(Action::Redo)).await;
4951                self.notify_clients(Ok(Action::HistoryState {
4952                    dirty: self.history.is_dirty(),
4953                }))
4954                .await;
4955            }
4956            Action::ApplyGroupedActions(actions) => {
4957                self.handle_request_inner(Action::BeginHistoryGroup, true)
4958                    .await;
4959                for action in actions {
4960                    self.handle_request_inner(action, true).await;
4961                }
4962                self.handle_request_inner(Action::EndHistoryGroup, true)
4963                    .await;
4964            }
4965            Action::Session(_) => {
4966                self.handle_request_inner(a, false).await;
4967            }
4968            other => {
4969                self.handle_request_inner(other, true).await;
4970            }
4971        }
4972    }
4973
4974    async fn handle_session_action(&mut self, action: SessionAction) {
4975        let sample_rate = self.sample_rate();
4976        let bpm = self.tempo_bpm;
4977        let tsig_num = self.tsig_num;
4978        let tsig_denom = self.tsig_denom;
4979        let quantize = |sample: usize, quantization: LaunchQuantization| -> usize {
4980            Track::quantize_sample_to_boundary(
4981                sample,
4982                quantization,
4983                bpm,
4984                tsig_num,
4985                tsig_denom,
4986                sample_rate,
4987            )
4988        };
4989
4990        match action {
4991            SessionAction::LaunchClip {
4992                track_name,
4993                scene_index,
4994                clip_id,
4995                launch_quantization,
4996                loop_enabled,
4997                loop_start_samples,
4998                loop_end_samples,
4999            } => {
5000                let Some(track) = self.track_handle_by_name(&track_name) else {
5001                    tracing::warn!("Session launch for unknown track '{}'", track_name);
5002                    return;
5003                };
5004                let track = track.lock();
5005                let clip_id = if clip_id.is_empty() {
5006                    track
5007                        .session_slots
5008                        .get(&scene_index)
5009                        .cloned()
5010                        .unwrap_or_default()
5011                } else {
5012                    clip_id
5013                };
5014                let kind = if track.audio.clips.iter().any(|c| c.id == clip_id) {
5015                    Kind::Audio
5016                } else if track.midi.clips.iter().any(|c| c.id == clip_id) {
5017                    Kind::MIDI
5018                } else {
5019                    tracing::warn!(
5020                        "Session launch for unknown clip '{}' on track '{}'",
5021                        clip_id,
5022                        track_name
5023                    );
5024                    return;
5025                };
5026                let launch_at_sample = quantize(self.transport_sample, launch_quantization);
5027                track.schedule_session_launch(crate::track::PendingSessionLaunch {
5028                    scene_index,
5029                    clip_id,
5030                    kind,
5031                    launch_at_sample,
5032                    loop_enabled,
5033                    loop_start_samples,
5034                    loop_end_samples,
5035                });
5036            }
5037            SessionAction::StopClip {
5038                track_name,
5039                scene_index,
5040                launch_quantization,
5041            } => {
5042                let Some(track) = self.track_handle_by_name(&track_name) else {
5043                    return;
5044                };
5045                let stop_at_sample = quantize(self.transport_sample, launch_quantization);
5046                track
5047                    .lock()
5048                    .schedule_session_stop(scene_index, stop_at_sample);
5049            }
5050            SessionAction::LaunchScene {
5051                scene_index,
5052                launch_quantization,
5053            } => {
5054                let launch_at_sample = quantize(self.transport_sample, launch_quantization);
5055                let tracks: Vec<_> = self.state.lock().tracks.values().cloned().collect();
5056                for track in tracks {
5057                    let track_lock = track.lock();
5058                    let Some(clip_id) = track_lock.session_slots.get(&scene_index).cloned() else {
5059                        continue;
5060                    };
5061                    let kind = if track_lock.audio.clips.iter().any(|c| c.id == clip_id) {
5062                        Kind::Audio
5063                    } else if track_lock.midi.clips.iter().any(|c| c.id == clip_id) {
5064                        Kind::MIDI
5065                    } else {
5066                        continue;
5067                    };
5068                    track_lock.schedule_session_launch(crate::track::PendingSessionLaunch {
5069                        scene_index,
5070                        clip_id,
5071                        kind,
5072                        launch_at_sample,
5073                        loop_enabled: true,
5074                        loop_start_samples: 0,
5075                        loop_end_samples: 0,
5076                    });
5077                }
5078            }
5079            SessionAction::StopScene {
5080                scene_index,
5081                launch_quantization,
5082            } => {
5083                let stop_at_sample = quantize(self.transport_sample, launch_quantization);
5084                let tracks: Vec<_> = self.state.lock().tracks.values().cloned().collect();
5085                for track in tracks {
5086                    track
5087                        .lock()
5088                        .schedule_session_stop(scene_index, stop_at_sample);
5089                }
5090            }
5091            SessionAction::StopAllClips => {
5092                let stop_at_sample = quantize(self.transport_sample, LaunchQuantization::Bar);
5093                let tracks: Vec<_> = self.state.lock().tracks.values().cloned().collect();
5094                for track in tracks {
5095                    let track = track.lock();
5096                    for clip in &mut track.playing_session_clips {
5097                        if clip.stop_at_sample.is_none() {
5098                            clip.stop_at_sample = Some(stop_at_sample);
5099                        }
5100                    }
5101                }
5102            }
5103        }
5104    }
5105
5106    fn find_audio_io_owner(
5107        &self,
5108        state: &crate::state::State,
5109        io: &std::sync::Arc<crate::audio::io::AudioIO>,
5110    ) -> Option<(String, usize)> {
5111        for (name, track) in &state.tracks {
5112            let t = track.lock();
5113            for (i, out) in t.audio.outs.iter().enumerate() {
5114                if std::sync::Arc::ptr_eq(out, io) {
5115                    return Some((name.clone(), i));
5116                }
5117            }
5118            for (i, inp) in t.audio.ins.iter().enumerate() {
5119                if std::sync::Arc::ptr_eq(inp, io) {
5120                    return Some((name.clone(), i));
5121                }
5122            }
5123        }
5124        None
5125    }
5126
5127    fn find_midi_io_owner(
5128        &self,
5129        state: &crate::state::State,
5130        io: &std::sync::Arc<crate::mutex::UnsafeMutex<Box<crate::midi::io::MIDIIO>>>,
5131    ) -> Option<(String, usize, bool)> {
5132        for (name, track) in &state.tracks {
5133            let t = track.lock();
5134            for (i, out) in t.midi.outs.iter().enumerate() {
5135                if std::sync::Arc::ptr_eq(out, io) {
5136                    return Some((name.clone(), i, false));
5137                }
5138            }
5139            for (i, inp) in t.midi.ins.iter().enumerate() {
5140                if std::sync::Arc::ptr_eq(inp, io) {
5141                    return Some((name.clone(), i, true));
5142                }
5143            }
5144        }
5145        None
5146    }
5147
5148    fn collect_descendant_track_names(&self, name: &str, out: &mut Vec<String>) {
5149        // Clone the child arcs while briefly holding the parent lock, then release it before
5150        // recursing so we never nest locks on the same thread.
5151        let child_arcs: Vec<Arc<UnsafeMutex<Box<Track>>>> = {
5152            let state = self.state.lock();
5153            if let Some(track) = state.tracks.get(name) {
5154                track.lock().child_tracks.clone()
5155            } else {
5156                Vec::new()
5157            }
5158        };
5159        for child in child_arcs {
5160            let child_name = { child.lock().name.clone() };
5161            self.collect_descendant_track_names(&child_name, out);
5162            out.push(child_name);
5163        }
5164    }
5165
5166    async fn remove_single_track(&mut self, name: &str) {
5167        let children: Vec<Arc<UnsafeMutex<Box<Track>>>> = {
5168            let state = self.state.lock();
5169            if let Some(removed) = state.tracks.get(name).cloned() {
5170                removed.lock().child_tracks.clone()
5171            } else {
5172                Vec::new()
5173            }
5174        };
5175        let parent_name: Option<String> = {
5176            let state = self.state.lock();
5177            state
5178                .tracks
5179                .get(name)
5180                .map(|t| t.lock().parent_track.clone())
5181                .unwrap_or(None)
5182        };
5183        if let Some(parent_name) = parent_name {
5184            let state = self.state.lock();
5185            if let Some(parent) = state.tracks.get(&parent_name).cloned() {
5186                let parent = parent.lock();
5187                parent.child_tracks.retain(|c| c.lock().name != *name);
5188            }
5189        }
5190        if let Some(removed_track) = self.state.lock().tracks.get(name).cloned() {
5191            for child in children {
5192                let removed = removed_track.lock();
5193                child.lock().disconnect_from_parent(removed);
5194                child.lock().parent_track = None;
5195            }
5196        }
5197        self.state.lock().tracks.remove(name);
5198        self.audio_recordings.remove(name);
5199        self.midi_recordings.remove(name);
5200        self.midi_hw_in_routes.retain(|r| r.to_track != *name);
5201        self.midi_hw_out_routes.retain(|r| r.from_track != *name);
5202        if self
5203            .pending_midi_learn
5204            .as_ref()
5205            .is_some_and(|(track_name, _, _)| track_name == name)
5206        {
5207            self.pending_midi_learn = None;
5208        }
5209    }
5210
5211    async fn handle_request_inner(&mut self, mut action_to_process: Action, record_history: bool) {
5212        let a = action_to_process.clone();
5213        let suppress_timing_history = self.playing
5214            && matches!(
5215                &action_to_process,
5216                Action::SetTempo(_) | Action::SetTimeSignature { .. }
5217            );
5218        let mut extra_inverse_actions: Vec<Action> = Vec::new();
5219        if record_history
5220            && !self.history_suspended
5221            && let Action::RemoveTrack(ref track_name) = action_to_process
5222        {
5223            for route in self
5224                .midi_hw_in_routes
5225                .iter()
5226                .filter(|route| &route.to_track == track_name)
5227            {
5228                extra_inverse_actions.push(Action::Connect {
5229                    from_track: format!("midi:hw:in:{}", route.device),
5230                    from_port: 0,
5231                    to_track: route.to_track.clone(),
5232                    to_port: route.to_port,
5233                    kind: Kind::MIDI,
5234                });
5235            }
5236            for route in self
5237                .midi_hw_out_routes
5238                .iter()
5239                .filter(|route| &route.from_track == track_name)
5240            {
5241                extra_inverse_actions.push(Action::Connect {
5242                    from_track: route.from_track.clone(),
5243                    from_port: route.from_port,
5244                    to_track: format!("midi:hw:out:{}", route.device),
5245                    to_port: 0,
5246                    kind: Kind::MIDI,
5247                });
5248            }
5249        }
5250        if record_history
5251            && !self.history_suspended
5252            && matches!(action_to_process, Action::ClearAllMidiLearnBindings)
5253        {
5254            if let Some(binding) = self.global_midi_learn_play_pause.clone() {
5255                extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
5256                    target: crate::message::GlobalMidiLearnTarget::PlayPause,
5257                    binding: Some(binding),
5258                });
5259            }
5260            if let Some(binding) = self.global_midi_learn_stop.clone() {
5261                extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
5262                    target: crate::message::GlobalMidiLearnTarget::Stop,
5263                    binding: Some(binding),
5264                });
5265            }
5266            if let Some(binding) = self.global_midi_learn_record_toggle.clone() {
5267                extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
5268                    target: crate::message::GlobalMidiLearnTarget::RecordToggle,
5269                    binding: Some(binding),
5270                });
5271            }
5272            for (key, binding) in self.session_midi_learn_slots.clone() {
5273                extra_inverse_actions.push(Action::SetSessionMidiLearnBinding {
5274                    target: crate::message::SessionMidiLearnTarget::Slot {
5275                        track_name: key.0,
5276                        scene_index: key.1,
5277                    },
5278                    binding: Some(binding),
5279                });
5280            }
5281            for (scene_index, binding) in self.session_midi_learn_scenes.clone() {
5282                extra_inverse_actions.push(Action::SetSessionMidiLearnBinding {
5283                    target: crate::message::SessionMidiLearnTarget::Scene(scene_index),
5284                    binding: Some(binding),
5285                });
5286            }
5287            for (track_name, binding) in self.session_midi_learn_stop_track.clone() {
5288                extra_inverse_actions.push(Action::SetSessionMidiLearnBinding {
5289                    target: crate::message::SessionMidiLearnTarget::StopTrack(track_name),
5290                    binding: Some(binding),
5291                });
5292            }
5293            if let Some(binding) = self.session_midi_learn_stop_all.clone() {
5294                extra_inverse_actions.push(Action::SetSessionMidiLearnBinding {
5295                    target: crate::message::SessionMidiLearnTarget::StopAll,
5296                    binding: Some(binding),
5297                });
5298            }
5299        }
5300        let mut inverse_actions = if record_history
5301            && !suppress_timing_history
5302            && should_record(&action_to_process)
5303            && !self.history_suspended
5304        {
5305            let state = self.state.lock();
5306            create_inverse_actions(&action_to_process, state).map(|mut actions| {
5307                actions.extend(extra_inverse_actions);
5308                actions
5309            })
5310        } else {
5311            None
5312        };
5313        if record_history && !suppress_timing_history && !self.history_suspended {
5314            match &action_to_process {
5315                Action::SetTempo(_) => {
5316                    inverse_actions = Some(vec![Action::SetTempo(self.tempo_bpm)]);
5317                }
5318                Action::SetLoopEnabled(_) => {
5319                    inverse_actions = Some(vec![Action::SetLoopEnabled(self.loop_enabled)]);
5320                }
5321                Action::SetLoopRange(_) => {
5322                    inverse_actions = Some(vec![
5323                        Action::SetLoopRange(self.loop_range_samples),
5324                        Action::SetLoopEnabled(self.loop_enabled),
5325                    ]);
5326                }
5327                Action::SetPunchEnabled(_) => {
5328                    inverse_actions = Some(vec![Action::SetPunchEnabled(self.punch_enabled)]);
5329                }
5330                Action::SetPunchRange(_) => {
5331                    inverse_actions = Some(vec![
5332                        Action::SetPunchRange(self.punch_range_samples),
5333                        Action::SetPunchEnabled(self.punch_enabled),
5334                    ]);
5335                }
5336                Action::SetMetronomeEnabled(_) => {
5337                    inverse_actions =
5338                        Some(vec![Action::SetMetronomeEnabled(self.metronome_enabled)]);
5339                }
5340                Action::SetTimeSignature { .. } => {
5341                    inverse_actions = Some(vec![Action::SetTimeSignature {
5342                        numerator: self.tsig_num,
5343                        denominator: self.tsig_denom,
5344                    }]);
5345                }
5346                Action::SetClipPlaybackEnabled(_) => {
5347                    inverse_actions = Some(vec![Action::SetClipPlaybackEnabled(
5348                        self.clip_playback_enabled,
5349                    )]);
5350                }
5351                Action::SetRecordEnabled(_) => {
5352                    inverse_actions = Some(vec![Action::SetRecordEnabled(self.record_enabled)]);
5353                }
5354                Action::SetGlobalMidiLearnBinding { target, .. } => {
5355                    let binding = match target {
5356                        crate::message::GlobalMidiLearnTarget::PlayPause => {
5357                            self.global_midi_learn_play_pause.clone()
5358                        }
5359                        crate::message::GlobalMidiLearnTarget::Stop => {
5360                            self.global_midi_learn_stop.clone()
5361                        }
5362                        crate::message::GlobalMidiLearnTarget::RecordToggle => {
5363                            self.global_midi_learn_record_toggle.clone()
5364                        }
5365                    };
5366                    inverse_actions = Some(vec![Action::SetGlobalMidiLearnBinding {
5367                        target: *target,
5368                        binding,
5369                    }]);
5370                }
5371                Action::SetSessionMidiLearnBinding { target, .. } => {
5372                    let binding = match target {
5373                        crate::message::SessionMidiLearnTarget::Slot {
5374                            track_name,
5375                            scene_index,
5376                        } => self
5377                            .session_midi_learn_slots
5378                            .get(&(track_name.clone(), *scene_index))
5379                            .cloned(),
5380                        crate::message::SessionMidiLearnTarget::Scene(scene_index) => {
5381                            self.session_midi_learn_scenes.get(scene_index).cloned()
5382                        }
5383                        crate::message::SessionMidiLearnTarget::StopTrack(track_name) => {
5384                            self.session_midi_learn_stop_track.get(track_name).cloned()
5385                        }
5386                        crate::message::SessionMidiLearnTarget::StopAll => {
5387                            self.session_midi_learn_stop_all.clone()
5388                        }
5389                    };
5390                    inverse_actions = Some(vec![Action::SetSessionMidiLearnBinding {
5391                        target: target.clone(),
5392                        binding,
5393                    }]);
5394                }
5395                _ => {}
5396            }
5397        }
5398
5399        match action_to_process {
5400            Action::Play => {
5401                tracing::debug!(
5402                    "Action::Play pressed, transport_sample={}",
5403                    self.transport_sample
5404                );
5405                self.playing = true;
5406                self.transport_restart_pending = true;
5407                self.notified_loop_wrap_sample = None;
5408                self.invalidate_track_cycle_state();
5409                if let Some(driver) = self.hw_driver.as_mut() {
5410                    driver.lock().set_playing(true);
5411                }
5412                #[cfg(unix)]
5413                if let Some(jack) = &self.jack_runtime
5414                    && let Err(e) = jack.lock().transport_start()
5415                {
5416                    self.notify_clients(Err(e)).await;
5417                }
5418                self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
5419                    .await;
5420                self.preload_track_clips().await;
5421                {
5422                    let echoes = self.apply_modulators(self.transport_sample);
5423                    for action in echoes {
5424                        self.notify_clients(Ok(action)).await;
5425                    }
5426                }
5427                let send_result = self.send_tasks().await;
5428                tracing::debug!("send_tasks after Play returned finished={}", send_result);
5429                if !self.awaiting_hwfinished
5430                    && !self.handling_hwfinished
5431                    && send_result
5432                    && self.hw_worker.is_some()
5433                {
5434                    self.transport_restart_pending = false;
5435                    self.request_hw_cycle().await;
5436                }
5437            }
5438            Action::Pause => {
5439                self.clip_playback_enabled = false;
5440                for track in self.state.lock().tracks.values() {
5441                    track.lock().set_clip_playback_enabled(false);
5442                }
5443                if !self.playing {
5444                    self.playing = true;
5445                    self.transport_restart_pending = true;
5446                    self.notified_loop_wrap_sample = None;
5447                    self.invalidate_track_cycle_state();
5448                    if let Some(driver) = self.hw_driver.as_mut() {
5449                        driver.lock().set_playing(true);
5450                    }
5451                    #[cfg(unix)]
5452                    if let Some(jack) = &self.jack_runtime
5453                        && let Err(e) = jack.lock().transport_start()
5454                    {
5455                        self.notify_clients(Err(e)).await;
5456                    }
5457                    self.preload_track_clips().await;
5458                    if !self.awaiting_hwfinished
5459                        && !self.handling_hwfinished
5460                        && self.send_tasks().await
5461                        && self.hw_worker.is_some()
5462                    {
5463                        self.transport_restart_pending = false;
5464                        self.request_hw_cycle().await;
5465                    }
5466                }
5467                self.notify_clients(Ok(Action::Pause)).await;
5468                self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
5469                    .await;
5470            }
5471            Action::Stop => {
5472                self.playing = false;
5473                self.transport_panic_flush_pending = false;
5474                self.transport_restart_pending = false;
5475                self.notified_loop_wrap_sample = None;
5476                self.invalidate_track_cycle_state();
5477                if let Some(driver) = self.hw_driver.as_mut() {
5478                    driver.lock().set_playing(false);
5479                }
5480                #[cfg(unix)]
5481                if let Some(jack) = &self.jack_runtime
5482                    && let Err(e) = jack.lock().transport_stop()
5483                {
5484                    self.notify_clients(Err(e)).await;
5485                }
5486                let panic_events = self.note_off_events_for_all_active_tracks();
5487                if let Some(worker) = &self.hw_worker {
5488                    if !panic_events.is_empty()
5489                        && let Err(e) = worker.tx.send(Message::HWMidiOutEvents(panic_events)).await
5490                    {
5491                        error!("Error sending stop MIDI panic events {e}");
5492                    }
5493                } else {
5494                    self.pending_hw_midi_out_events_by_device
5495                        .extend(panic_events);
5496                }
5497                self.reset_meters_after_stop();
5498                self.flush_recordings().await;
5499                self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
5500                    .await;
5501            }
5502            Action::JumpToEnd => {
5503                self.transport_sample = self.normalize_transport_sample(self.session_end_sample());
5504                self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
5505                    .await;
5506            }
5507            Action::Panic => {
5508                let panic_events = self.panic_events_for_all_hw_midi_outputs();
5509                if let Some(worker) = &self.hw_worker {
5510                    if !panic_events.is_empty() {
5511                        if let Err(e) = worker.tx.send(Message::ClearHWMidiOutEvents).await {
5512                            error!("Error clearing HW MIDI queue for panic {e}");
5513                        }
5514                        self.midi_hub
5515                            .lock()
5516                            .write_events_blocking(&panic_events, Duration::from_millis(250));
5517                    }
5518                } else if !panic_events.is_empty() {
5519                    self.pending_hw_midi_out_events_by_device
5520                        .extend(panic_events);
5521                }
5522            }
5523            Action::Session(ref session_action) => {
5524                self.handle_session_action(session_action.clone()).await;
5525            }
5526            Action::SessionRuntimeReport { .. } => {}
5527            Action::SessionMidiLearnTriggered { .. } => {}
5528            Action::SetClipPlaybackEnabled(enabled) => {
5529                self.clip_playback_enabled = enabled;
5530                for track in self.state.lock().tracks.values() {
5531                    track.lock().set_clip_playback_enabled(enabled);
5532                }
5533            }
5534            Action::TransportPosition(sample) => {
5535                self.transport_sample = self.normalize_transport_sample(sample);
5536                self.notified_loop_wrap_sample = None;
5537                {
5538                    let echoes = self.apply_modulators(self.transport_sample);
5539                    for action in echoes {
5540                        self.notify_clients(Ok(action)).await;
5541                    }
5542                }
5543                #[cfg(unix)]
5544                if let Some(jack) = &self.jack_runtime
5545                    && let Err(e) = jack.lock().transport_locate(self.transport_sample)
5546                {
5547                    self.notify_clients(Err(e)).await;
5548                }
5549                if self.playing {
5550                    self.transport_restart_pending = true;
5551                    self.invalidate_track_cycle_state();
5552                    self.transport_panic_flush_pending = self.hw_worker.is_some();
5553                    self.clear_hw_midi_output_state(true).await;
5554                    if !self.awaiting_hwfinished && !self.handling_hwfinished {
5555                        if self.hw_worker.is_some() {
5556                            self.request_hw_cycle().await;
5557                        } else if self.send_tasks().await {
5558                            self.transport_restart_pending = false;
5559                            self.request_hw_cycle().await;
5560                        }
5561                    }
5562                }
5563            }
5564            Action::SetLoopEnabled(enabled) => {
5565                self.loop_enabled = enabled && self.loop_range_samples.is_some();
5566                self.notified_loop_wrap_sample = None;
5567            }
5568            Action::SetLoopRange(range) => {
5569                self.loop_range_samples = range.and_then(|(start, end)| {
5570                    if end > start {
5571                        Some((start, end))
5572                    } else {
5573                        None
5574                    }
5575                });
5576                self.loop_enabled = self.loop_range_samples.is_some();
5577                self.notified_loop_wrap_sample = None;
5578                if self.loop_enabled
5579                    && let Some((loop_start, loop_end)) = self.loop_range_samples
5580                    && self.transport_sample >= loop_end
5581                {
5582                    self.transport_sample = loop_start;
5583                    self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
5584                        .await;
5585                }
5586            }
5587            Action::SetPunchEnabled(enabled) => {
5588                self.punch_enabled = enabled && self.punch_range_samples.is_some();
5589            }
5590            Action::SetPunchRange(range) => {
5591                self.punch_range_samples = range.and_then(|(start, end)| {
5592                    if end > start {
5593                        Some((start, end))
5594                    } else {
5595                        None
5596                    }
5597                });
5598                self.punch_enabled = self.punch_range_samples.is_some();
5599            }
5600            Action::SetMetronomeEnabled(enabled) => {
5601                self.metronome_enabled = enabled;
5602                if enabled {
5603                    self.ensure_metronome_track().await;
5604                }
5605                if let Some(track) = self.state.lock().tracks.get(Self::METRONOME_TRACK).cloned() {
5606                    track.lock().set_metronome_enabled(enabled);
5607                }
5608            }
5609            Action::SetTempo(bpm) => {
5610                self.tempo_bpm = bpm.max(1.0);
5611            }
5612            Action::SetTimeSignature {
5613                numerator,
5614                denominator,
5615            } => {
5616                self.tsig_num = numerator.max(1);
5617                self.tsig_denom = denominator.max(1);
5618            }
5619            Action::SetOscEnabled(enabled) => {
5620                if let Err(err) = self.set_osc_enabled_with(enabled, OscServer::start) {
5621                    self.notify_clients(Err(err)).await;
5622                }
5623            }
5624            Action::SetRecordEnabled(enabled) => {
5625                self.record_enabled = enabled;
5626                if !enabled {
5627                    if self.awaiting_hwfinished {
5628                        self.append_recorded_cycle();
5629                    }
5630                    self.flush_recordings().await;
5631                } else if self.session_dir.is_none() {
5632                    self.notify_clients(Err(
5633                        "Recording enabled but session path is not set".to_string()
5634                    ))
5635                    .await;
5636                }
5637            }
5638            Action::SetModulators(ref modulators) => {
5639                self.modulators = modulators.clone();
5640                let echoes = self.apply_modulators(self.transport_sample);
5641                for action in echoes {
5642                    self.notify_clients(Ok(action)).await;
5643                }
5644            }
5645            Action::SetStepRecording(enabled) => {
5646                self.step_recording_enabled = enabled;
5647            }
5648            Action::BeginHistoryGroup if self.history_group.is_none() => {
5649                self.history_group = Some(UndoEntry {
5650                    forward_actions: vec![],
5651                    inverse_actions: vec![],
5652                });
5653            }
5654            Action::EndHistoryGroup => {
5655                if let Some(mut group) = self.history_group.take()
5656                    && !group.forward_actions.is_empty()
5657                    && !group.inverse_actions.is_empty()
5658                {
5659                    let mut add_tracks = Vec::new();
5660                    let mut connections = Vec::new();
5661                    let mut rest = Vec::new();
5662                    for action in group.inverse_actions {
5663                        if matches!(action, Action::AddTrack { .. }) {
5664                            add_tracks.push(action);
5665                        } else if matches!(action, Action::Connect { .. }) {
5666                            connections.push(action);
5667                        } else {
5668                            rest.push(action);
5669                        }
5670                    }
5671                    group.inverse_actions = add_tracks;
5672                    group.inverse_actions.extend(rest);
5673                    group.inverse_actions.extend(connections);
5674                    self.history.record(group);
5675                }
5676            }
5677            Action::SetSessionPath(ref path) => {
5678                self.session_dir = Some(Path::new(path).to_path_buf());
5679                self.ensure_session_subdirs();
5680                #[cfg(all(unix, not(target_os = "macos")))]
5681                let _lv2_dir = self.session_plugins_dir();
5682                for track in self.state.lock().tracks.values() {
5683                    track.lock().set_session_base_dir(self.session_dir.clone());
5684                }
5685            }
5686            Action::MarkHistorySavePoint => {
5687                self.history.mark_save_point();
5688                self.notify_clients(Ok(Action::HistoryState {
5689                    dirty: self.history.is_dirty(),
5690                }))
5691                .await;
5692            }
5693            Action::ClearHistory => {
5694                self.history.clear();
5695                self.history.mark_save_point();
5696            }
5697            Action::BeginSessionRestore => {
5698                self.history_suspended = true;
5699                self.history.clear();
5700            }
5701            Action::EndSessionRestore => {
5702                self.history.clear();
5703                self.history_suspended = false;
5704                self.preload_track_clips_spawn();
5705            }
5706            Action::Quit => {
5707                self.flush_recordings().await;
5708                // Stop the HW worker before notifying the GUI so the
5709                // OSS audio channels are halted and closed from the
5710                // worker's own thread. The GUI calls exit(0) upon
5711                // receiving the Quit response, which skips Rust
5712                // destructors. Without this, the kernel's dsp_close
5713                // drains pending audio buffers for up to CHN_TIMEOUT
5714                // (5s) during process teardown.
5715                if let Some(worker) = self.hw_worker.take() {
5716                    if let Some(hw) = &self.hw_driver {
5717                        hw.lock().request_stop();
5718                    }
5719                    // Send MIDI panic (All Sound Off) for any active
5720                    // notes before stopping the worker.
5721                    let panic_events = self.panic_events_for_all_hw_midi_outputs();
5722                    if !panic_events.is_empty() {
5723                        let _ = worker.tx.send(Message::HWMidiOutEvents(panic_events)).await;
5724                    }
5725                    // Send Quit to the worker so it stops its audio
5726                    // cycle loop and releases the driver.
5727                    if let Err(e) = worker.tx.send(Message::Request(a.clone())).await {
5728                        error!("Error sending quit message to HW worker: {e}");
5729                    }
5730                    worker
5731                        .handle
5732                        .await
5733                        .unwrap_or_else(|e| error!("Error waiting for HW worker to quit: {e}"));
5734                }
5735                // Explicitly close audio and MIDI fds before sending
5736                // the Quit response. The GUI calls exit(0) upon
5737                // receiving it, which skips destructors — any
5738                // still-open device fd would trigger the kernel's
5739                // 5-second drain during process teardown.
5740                if let Some(hw) = &self.hw_driver {
5741                    hw.lock().close_fds();
5742                }
5743                self.midi_hub.lock().close_all();
5744                self.hw_driver = None;
5745                self.notify_clients(Ok(Action::Quit)).await;
5746                self.ready_workers.clear();
5747                while !self.workers.is_empty() {
5748                    let worker = self.workers.remove(0);
5749                    if let Err(e) = worker.tx.send(Message::Request(a.clone())).await {
5750                        error!("Error sending quit message to worker: {e}");
5751                    }
5752                    worker
5753                        .handle
5754                        .await
5755                        .unwrap_or_else(|e| error!("Error waiting for worker to quit: {e}"));
5756                }
5757                #[cfg(unix)]
5758                {
5759                    self.jack_runtime = None;
5760                }
5761                self.osc_server = None;
5762                return;
5763            }
5764            Action::AddTrack {
5765                ref name,
5766                audio_ins,
5767                midi_ins,
5768                audio_outs,
5769                midi_outs,
5770                folder,
5771            } => {
5772                let tracks = &mut self.state.lock().tracks;
5773                if tracks.contains_key(name) {
5774                    self.notify_clients(Err(format!("Track {} already exists", name)))
5775                        .await;
5776                    return;
5777                }
5778                let maybe_hw = if let Some(oss) = &self.hw_driver {
5779                    let hw = oss.lock();
5780                    Some((hw.cycle_samples(), hw.sample_rate() as f64))
5781                } else {
5782                    #[cfg(unix)]
5783                    if let Some(jack) = &self.jack_runtime {
5784                        let j = jack.lock();
5785                        Some((j.buffer_size, j.sample_rate as f64))
5786                    } else {
5787                        None
5788                    }
5789                    #[cfg(not(unix))]
5790                    None
5791                };
5792
5793                if let Some((chsamples, sample_rate)) = maybe_hw {
5794                    let track = if folder {
5795                        Track::new_folder(
5796                            name.clone(),
5797                            audio_ins,
5798                            audio_outs,
5799                            midi_ins,
5800                            midi_outs,
5801                            chsamples,
5802                            sample_rate,
5803                        )
5804                    } else {
5805                        Track::new(
5806                            name.clone(),
5807                            audio_ins,
5808                            audio_outs,
5809                            midi_ins,
5810                            midi_outs,
5811                            chsamples,
5812                            sample_rate,
5813                        )
5814                    };
5815                    tracks.insert(name.clone(), Arc::new(UnsafeMutex::new(Box::new(track))));
5816                    if let Some(track) = tracks.get(name) {
5817                        let t = track.lock();
5818                        t.set_clip_playback_enabled(self.clip_playback_enabled);
5819                        t.set_transport_timing(self.tempo_bpm, self.tsig_num, self.tsig_denom);
5820                        t.set_session_base_dir(self.session_dir.clone());
5821                    }
5822                } else {
5823                    self.notify_clients(Err(
5824                        "Engine needs to open audio device before adding audio track".to_string(),
5825                    ))
5826                    .await;
5827                }
5828            }
5829            Action::TrackAddAudioInput(ref name) => {
5830                let track = match self.track_handle_or_err(name) {
5831                    Ok(track) => track,
5832                    Err(e) => {
5833                        self.notify_clients(Err(e)).await;
5834                        return;
5835                    }
5836                };
5837                if let Err(e) = track.lock().add_audio_input() {
5838                    self.notify_clients(Err(e)).await;
5839                    return;
5840                }
5841            }
5842            Action::TrackAddAudioOutput(ref name) => {
5843                let track = match self.track_handle_or_err(name) {
5844                    Ok(track) => track,
5845                    Err(e) => {
5846                        self.notify_clients(Err(e)).await;
5847                        return;
5848                    }
5849                };
5850                if let Err(e) = track.lock().add_audio_output() {
5851                    self.notify_clients(Err(e)).await;
5852                    return;
5853                }
5854            }
5855            Action::TrackRemoveAudioInput(ref name) => {
5856                let track = match self.track_handle_or_err(name) {
5857                    Ok(track) => track,
5858                    Err(e) => {
5859                        self.notify_clients(Err(e)).await;
5860                        return;
5861                    }
5862                };
5863                if let Err(e) = track.lock().remove_audio_input() {
5864                    self.notify_clients(Err(e)).await;
5865                    return;
5866                }
5867            }
5868            Action::TrackRemoveAudioOutput(ref name) => {
5869                let track = match self.track_handle_or_err(name) {
5870                    Ok(track) => track,
5871                    Err(e) => {
5872                        self.notify_clients(Err(e)).await;
5873                        return;
5874                    }
5875                };
5876                let (hw_outputs, track_inputs) = {
5877                    let state = self.state.lock();
5878                    let hw_outputs = self.all_hw_output_audio_ports();
5879                    let track_inputs = state
5880                        .tracks
5881                        .iter()
5882                        .filter(|(track_name, _)| *track_name != name)
5883                        .flat_map(|(_, handle)| handle.lock().audio.ins.clone())
5884                        .collect::<Vec<_>>();
5885                    (hw_outputs, track_inputs)
5886                };
5887                if let Err(e) = track.lock().remove_audio_output(&hw_outputs, &track_inputs) {
5888                    self.notify_clients(Err(e)).await;
5889                    return;
5890                }
5891            }
5892            Action::RenameTrack {
5893                ref old_name,
5894                ref new_name,
5895            } => {
5896                if self.state.lock().tracks.contains_key(new_name) {
5897                    self.notify_clients(Err(format!("Track '{}' already exists", new_name)))
5898                        .await;
5899                    return;
5900                }
5901
5902                let Some(track) = self.state.lock().tracks.remove(old_name) else {
5903                    self.notify_clients(Err(format!("Track '{}' not found", old_name)))
5904                        .await;
5905                    return;
5906                };
5907
5908                track.lock().name = new_name.clone();
5909                self.state.lock().tracks.insert(new_name.clone(), track);
5910                for other in self.state.lock().tracks.values() {
5911                    let other = other.lock();
5912                    if other.parent_track.as_deref() == Some(old_name.as_str()) {
5913                        other.parent_track = Some(new_name.clone());
5914                    }
5915                }
5916
5917                if let Some(recording) = self.audio_recordings.remove(old_name) {
5918                    self.audio_recordings.insert(new_name.clone(), recording);
5919                }
5920                if let Some(recording) = self.midi_recordings.remove(old_name) {
5921                    self.midi_recordings.insert(new_name.clone(), recording);
5922                }
5923
5924                for route in &mut self.midi_hw_in_routes {
5925                    if route.to_track == *old_name {
5926                        route.to_track = new_name.clone();
5927                    }
5928                }
5929                for route in &mut self.midi_hw_out_routes {
5930                    if route.from_track == *old_name {
5931                        route.from_track = new_name.clone();
5932                    }
5933                }
5934                if let Some((armed_track, target, device)) = self.pending_midi_learn.clone()
5935                    && armed_track == *old_name
5936                {
5937                    self.pending_midi_learn = Some((new_name.clone(), target, device));
5938                }
5939
5940                self.notify_clients(Ok(Action::RenameTrack {
5941                    old_name: old_name.clone(),
5942                    new_name: new_name.clone(),
5943                }))
5944                .await;
5945            }
5946            Action::RemoveTrack(ref name) => {
5947                let mut descendant_names = Vec::new();
5948                self.collect_descendant_track_names(name, &mut descendant_names);
5949                let names_to_remove: Vec<String> = descendant_names
5950                    .iter()
5951                    .cloned()
5952                    .chain(std::iter::once(name.clone()))
5953                    .collect();
5954
5955                let combined_inverse = if record_history && !self.history_suspended {
5956                    let state = self.state.lock();
5957                    let mut inv = Vec::new();
5958                    for n in &names_to_remove {
5959                        if let Some(mut actions) =
5960                            create_inverse_actions(&Action::RemoveTrack(n.clone()), state)
5961                        {
5962                            inv.append(&mut actions);
5963                        }
5964                        for route in self.midi_hw_in_routes.iter().filter(|r| &r.to_track == n) {
5965                            inv.push(Action::Connect {
5966                                from_track: format!("midi:hw:in:{}", route.device),
5967                                from_port: 0,
5968                                to_track: route.to_track.clone(),
5969                                to_port: route.to_port,
5970                                kind: Kind::MIDI,
5971                            });
5972                        }
5973                        for route in self
5974                            .midi_hw_out_routes
5975                            .iter()
5976                            .filter(|r| &r.from_track == n)
5977                        {
5978                            inv.push(Action::Connect {
5979                                from_track: route.from_track.clone(),
5980                                from_port: route.from_port,
5981                                to_track: format!("midi:hw:out:{}", route.device),
5982                                to_port: 0,
5983                                kind: Kind::MIDI,
5984                            });
5985                        }
5986                    }
5987
5988                    // Reorder so all AddTrack actions come first, then everything else, then
5989                    // explicit Connect actions. This mirrors EndHistoryGroup and guarantees that
5990                    // tracks are recreated before they are re-parented or reconnected.
5991                    let mut add_tracks = Vec::new();
5992                    let mut connections = Vec::new();
5993                    let mut rest = Vec::new();
5994                    for action in inv {
5995                        match action {
5996                            Action::AddTrack { .. } => add_tracks.push(action),
5997                            Action::Connect { .. } => connections.push(action),
5998                            _ => rest.push(action),
5999                        }
6000                    }
6001                    let mut ordered = add_tracks;
6002                    ordered.extend(rest);
6003                    ordered.extend(connections);
6004                    ordered
6005                } else {
6006                    Vec::new()
6007                };
6008
6009                for n in &descendant_names {
6010                    self.remove_single_track(n).await;
6011                    self.notify_clients(Ok(Action::RemoveTrack(n.clone())))
6012                        .await;
6013                }
6014                self.remove_single_track(name).await;
6015
6016                if record_history && !self.history_suspended && !combined_inverse.is_empty() {
6017                    self.history.record(UndoEntry {
6018                        forward_actions: vec![Action::RemoveTrack(name.clone())],
6019                        inverse_actions: combined_inverse,
6020                    });
6021                }
6022
6023                // The outer code already computed a per-action inverse for the original
6024                // RemoveTrack. We have recorded a combined inverse for the whole subtree, so
6025                // suppress that default recording.
6026                inverse_actions = None;
6027            }
6028            Action::TrackLevel(ref name, level) => {
6029                if name == "hw:out" {
6030                    self.hw_out_level_db = level;
6031                } else if let Some(track) = self.state.lock().tracks.get(name) {
6032                    track.lock().set_level(level);
6033                }
6034            }
6035            Action::TrackBalance(ref name, balance) => {
6036                if name == "hw:out" {
6037                    self.hw_out_balance = balance.clamp(-1.0, 1.0);
6038                } else if let Some(track) = self.state.lock().tracks.get(name) {
6039                    track.lock().set_balance(balance);
6040                }
6041            }
6042            Action::TrackAutomationLevel(ref name, level) => {
6043                tracing::debug!(%name, level, "engine received TrackAutomationLevel");
6044                if name == "hw:out" {
6045                    self.hw_out_level_db = level;
6046                } else if let Some(track) = self.state.lock().tracks.get(name) {
6047                    track.lock().set_level(level);
6048                }
6049            }
6050            Action::TrackAutomationBalance(ref name, balance) => {
6051                if name == "hw:out" {
6052                    self.hw_out_balance = balance.clamp(-1.0, 1.0);
6053                } else if let Some(track) = self.state.lock().tracks.get(name) {
6054                    track.lock().set_balance(balance);
6055                }
6056            }
6057            Action::TrackMidiCc {
6058                ref track_name,
6059                channel,
6060                cc,
6061                value,
6062            } => {
6063                if let Some(track) = self.state.lock().tracks.get(track_name) {
6064                    track
6065                        .lock()
6066                        .pending_automation_midi_events
6067                        .push(MidiEvent::new(
6068                            0,
6069                            vec![0xB0 | channel.min(15), cc.min(127), value.min(127)],
6070                        ));
6071                }
6072            }
6073            Action::RequestMeterSnapshot => {
6074                self.notify_clients(Ok(Action::MeterSnapshot {
6075                    hw_out_db: self.latest_hw_out_meter_db.clone(),
6076                    track_meters: self.latest_track_meter_snapshot.clone(),
6077                }))
6078                .await;
6079                return;
6080            }
6081            Action::TrackMeters { .. } => {}
6082            Action::MeterSnapshot { .. } => {}
6083            Action::TrackToggleArm(ref name) => {
6084                if self.reject_if_track_frozen(name, "arming/disarming").await {
6085                    return;
6086                }
6087                if let Some(track) = self.state.lock().tracks.get(name).cloned() {
6088                    track.lock().arm();
6089                    let armed = track.lock().armed;
6090                    if !armed && self.audio_recordings.contains_key(name) {
6091                        self.flush_track_recording(name).await;
6092                    }
6093                } else {
6094                    tracing::warn!(
6095                        "TrackToggleArm for '{}' but track not found in engine",
6096                        name
6097                    );
6098                }
6099            }
6100            Action::TrackToggleMute(ref name) => {
6101                if name == "hw:out" {
6102                    self.hw_out_muted = !self.hw_out_muted;
6103                } else if let Some(track) = self.state.lock().tracks.get(name) {
6104                    track.lock().mute();
6105                }
6106            }
6107            Action::TrackTogglePhase(ref name) => {
6108                if let Some(track) = self.state.lock().tracks.get(name) {
6109                    track.lock().invert_phase();
6110                }
6111            }
6112            Action::TrackToggleSolo(ref name) => {
6113                if name == "hw:out" {
6114                    return;
6115                }
6116                if let Some(track) = self.state.lock().tracks.get(name) {
6117                    track.lock().solo();
6118                }
6119            }
6120            Action::TrackToggleMaster(ref name) => {
6121                if let Some(track) = self.state.lock().tracks.get(name) {
6122                    track.lock().toggle_master();
6123                }
6124            }
6125            Action::TrackToggleInputMonitor {
6126                ref track_name,
6127                lane,
6128            } => {
6129                if let Some(track) = self.state.lock().tracks.get(track_name) {
6130                    track.lock().toggle_input_monitor(lane);
6131                }
6132            }
6133            Action::TrackToggleDiskMonitor {
6134                ref track_name,
6135                lane,
6136            } => {
6137                if let Some(track) = self.state.lock().tracks.get(track_name) {
6138                    track.lock().toggle_disk_monitor(lane);
6139                }
6140            }
6141            Action::TrackToggleMidiInputMonitor {
6142                ref track_name,
6143                lane,
6144            } => {
6145                if let Some(track) = self.state.lock().tracks.get(track_name) {
6146                    track.lock().toggle_midi_input_monitor(lane);
6147                }
6148            }
6149            Action::TrackToggleMidiDiskMonitor {
6150                ref track_name,
6151                lane,
6152            } => {
6153                if let Some(track) = self.state.lock().tracks.get(track_name) {
6154                    track.lock().toggle_midi_disk_monitor(lane);
6155                }
6156            }
6157            Action::TrackSetColor {
6158                ref track_name,
6159                color,
6160            } => {
6161                if let Some(track) = self.state.lock().tracks.get(track_name) {
6162                    track.lock().color = color;
6163                }
6164            }
6165            Action::TrackArmMidiLearn {
6166                ref track_name,
6167                target,
6168            } => {
6169                if let Err(e) = self.track_handle_or_err(track_name) {
6170                    self.notify_clients(Err(e)).await;
6171                    return;
6172                }
6173                self.pending_midi_learn = Some((track_name.clone(), target, None));
6174            }
6175            Action::GlobalArmMidiLearn { target } => {
6176                self.pending_global_midi_learn = Some(target);
6177            }
6178            Action::SessionArmMidiLearn { ref target } => {
6179                self.pending_session_midi_learn = Some(target.clone());
6180            }
6181            Action::TrackSetMidiLearnBinding {
6182                ref track_name,
6183                target,
6184                ref binding,
6185            } => {
6186                if let Some(binding) = binding.as_ref() {
6187                    let conflicts = self.midi_learn_slot_conflicts(
6188                        binding,
6189                        Some(MidiLearnSlot::Track(track_name.clone(), target)),
6190                    );
6191                    if !conflicts.is_empty() {
6192                        self.notify_clients(Err(format!(
6193                            "MIDI learn conflict for '{}' {:?}: {}",
6194                            track_name,
6195                            target,
6196                            conflicts.join(", ")
6197                        )))
6198                        .await;
6199                        return;
6200                    }
6201                }
6202                let track = match self.track_handle_or_err(track_name) {
6203                    Ok(track) => track,
6204                    Err(e) => {
6205                        self.notify_clients(Err(e)).await;
6206                        return;
6207                    }
6208                };
6209                match target {
6210                    crate::message::TrackMidiLearnTarget::Volume => {
6211                        track.lock().midi_learn_volume = binding.clone();
6212                    }
6213                    crate::message::TrackMidiLearnTarget::Balance => {
6214                        track.lock().midi_learn_balance = binding.clone();
6215                    }
6216                    crate::message::TrackMidiLearnTarget::Mute => {
6217                        track.lock().midi_learn_mute = binding.clone();
6218                    }
6219                    crate::message::TrackMidiLearnTarget::Solo => {
6220                        track.lock().midi_learn_solo = binding.clone();
6221                    }
6222                    crate::message::TrackMidiLearnTarget::Arm => {
6223                        track.lock().midi_learn_arm = binding.clone();
6224                    }
6225                    crate::message::TrackMidiLearnTarget::InputMonitor => {
6226                        track.lock().midi_learn_input_monitor = binding.clone();
6227                    }
6228                    crate::message::TrackMidiLearnTarget::DiskMonitor => {
6229                        track.lock().midi_learn_disk_monitor = binding.clone();
6230                    }
6231                }
6232            }
6233            Action::SetGlobalMidiLearnBinding {
6234                target,
6235                ref binding,
6236            } => {
6237                if let Some(binding) = binding.as_ref() {
6238                    let conflicts = self
6239                        .midi_learn_slot_conflicts(binding, Some(MidiLearnSlot::Global(target)));
6240                    if !conflicts.is_empty() {
6241                        self.notify_clients(Err(format!(
6242                            "Global MIDI learn conflict for {:?}: {}",
6243                            target,
6244                            conflicts.join(", ")
6245                        )))
6246                        .await;
6247                        return;
6248                    }
6249                }
6250                match target {
6251                    crate::message::GlobalMidiLearnTarget::PlayPause => {
6252                        self.global_midi_learn_play_pause = binding.clone();
6253                    }
6254                    crate::message::GlobalMidiLearnTarget::Stop => {
6255                        self.global_midi_learn_stop = binding.clone();
6256                    }
6257                    crate::message::GlobalMidiLearnTarget::RecordToggle => {
6258                        self.global_midi_learn_record_toggle = binding.clone();
6259                    }
6260                }
6261            }
6262            Action::SetSessionMidiLearnBinding {
6263                ref target,
6264                ref binding,
6265            } => {
6266                if let Some(binding) = binding.as_ref() {
6267                    let conflicts = self.midi_learn_slot_conflicts(
6268                        binding,
6269                        Some(MidiLearnSlot::Session(target.clone())),
6270                    );
6271                    if !conflicts.is_empty() {
6272                        self.notify_clients(Err(format!(
6273                            "Session MIDI learn conflict for {:?}: {}",
6274                            target,
6275                            conflicts.join(", ")
6276                        )))
6277                        .await;
6278                        return;
6279                    }
6280                }
6281                match target {
6282                    crate::message::SessionMidiLearnTarget::Slot {
6283                        track_name,
6284                        scene_index,
6285                    } => {
6286                        if binding.is_some() {
6287                            self.session_midi_learn_slots.insert(
6288                                (track_name.clone(), *scene_index),
6289                                binding.clone().unwrap(),
6290                            );
6291                        } else {
6292                            self.session_midi_learn_slots
6293                                .remove(&(track_name.clone(), *scene_index));
6294                        }
6295                    }
6296                    crate::message::SessionMidiLearnTarget::Scene(scene_index) => {
6297                        if binding.is_some() {
6298                            self.session_midi_learn_scenes
6299                                .insert(*scene_index, binding.clone().unwrap());
6300                        } else {
6301                            self.session_midi_learn_scenes.remove(scene_index);
6302                        }
6303                    }
6304                    crate::message::SessionMidiLearnTarget::StopTrack(track_name) => {
6305                        if binding.is_some() {
6306                            self.session_midi_learn_stop_track
6307                                .insert(track_name.clone(), binding.clone().unwrap());
6308                        } else {
6309                            self.session_midi_learn_stop_track.remove(track_name);
6310                        }
6311                    }
6312                    crate::message::SessionMidiLearnTarget::StopAll => {
6313                        self.session_midi_learn_stop_all = binding.clone();
6314                    }
6315                }
6316            }
6317            Action::TrackSetFolder {
6318                ref track_name,
6319                is_folder,
6320            } => {
6321                let track = match self.track_handle_or_err(track_name) {
6322                    Ok(track) => track,
6323                    Err(e) => {
6324                        self.notify_clients(Err(e)).await;
6325                        return;
6326                    }
6327                };
6328                if is_folder {
6329                    let is_master = track.lock().is_master;
6330                    if is_master {
6331                        self.notify_clients(Err(format!(
6332                            "Track '{}' is the master track and cannot be made a folder",
6333                            track_name
6334                        )))
6335                        .await;
6336                        return;
6337                    }
6338                }
6339                {
6340                    let track = track.lock();
6341                    track.is_folder = is_folder;
6342                    track.ensure_default_audio_passthrough();
6343                    track.ensure_default_midi_passthrough();
6344                }
6345                self.notify_clients(Ok(Action::TrackSetFolder {
6346                    track_name: track_name.clone(),
6347                    is_folder,
6348                }))
6349                .await;
6350            }
6351            Action::TrackSetParent {
6352                ref track_name,
6353                ref parent_name,
6354            } => {
6355                let track = match self.track_handle_or_err(track_name) {
6356                    Ok(track) => track,
6357                    Err(e) => {
6358                        self.notify_clients(Err(e)).await;
6359                        return;
6360                    }
6361                };
6362                if parent_name.as_deref() == Some(track_name.as_str()) {
6363                    self.notify_clients(Err("Track cannot be its own parent".to_string()))
6364                        .await;
6365                    return;
6366                }
6367
6368                // Validate the new parent is a folder (if any).
6369                if let Some(parent_name) = parent_name {
6370                    let state = self.state.lock();
6371                    let parent = state.tracks.get(parent_name);
6372                    if parent.is_none() {
6373                        self.notify_clients(Err(format!(
6374                            "Parent track '{}' does not exist",
6375                            parent_name
6376                        )))
6377                        .await;
6378                        return;
6379                    }
6380                    if !parent.unwrap().lock().is_folder {
6381                        self.notify_clients(Err(format!(
6382                            "Track '{}' is not a folder",
6383                            parent_name
6384                        )))
6385                        .await;
6386                        return;
6387                    }
6388                }
6389
6390                // Disconnect from the old parent and update its child list.
6391                {
6392                    let old_parent_name = track.lock().parent_track.clone();
6393                    if let Some(old_parent_name) = old_parent_name {
6394                        let state = self.state.lock();
6395                        if let (Some(parent_arc), Some(child_arc)) = (
6396                            state.tracks.get(&old_parent_name).cloned(),
6397                            state.tracks.get(track_name).cloned(),
6398                        ) {
6399                            {
6400                                let parent = parent_arc.lock();
6401                                parent.child_tracks.retain(|c| c.lock().name != *track_name);
6402                            }
6403                            {
6404                                let child = child_arc.lock();
6405                                let parent = parent_arc.lock();
6406                                child.disconnect_from_parent(parent);
6407                            }
6408                        }
6409                    }
6410                }
6411
6412                let mut disconnect_actions = Vec::new();
6413
6414                // Remove all existing audio and MIDI connections involving this track.
6415                {
6416                    let state = self.state.lock();
6417                    let hw_inputs = self.all_hw_input_audio_ports();
6418                    let hw_outputs = self.all_hw_output_audio_ports();
6419                    if let Some(child_arc) = state.tracks.get(track_name).cloned() {
6420                        let child = child_arc.lock();
6421                        for (port_idx, inp) in child.audio.ins.iter().enumerate() {
6422                            let sources = inp.connections.lock().clone();
6423                            for src in sources {
6424                                let _ = AudioIO::disconnect(&src, inp);
6425                                if let Some((src_name, src_port)) =
6426                                    self.find_audio_io_owner(state, &src)
6427                                {
6428                                    disconnect_actions.push(Action::Disconnect {
6429                                        from_track: src_name,
6430                                        from_port: src_port,
6431                                        to_track: track_name.clone(),
6432                                        to_port: port_idx,
6433                                        kind: Kind::Audio,
6434                                    });
6435                                } else if let Some(src_port) = hw_inputs
6436                                    .iter()
6437                                    .position(|hw_in| std::sync::Arc::ptr_eq(hw_in, &src))
6438                                {
6439                                    disconnect_actions.push(Action::Disconnect {
6440                                        from_track: "hw:in".to_string(),
6441                                        from_port: src_port,
6442                                        to_track: track_name.clone(),
6443                                        to_port: port_idx,
6444                                        kind: Kind::Audio,
6445                                    });
6446                                }
6447                            }
6448                        }
6449                        for (port_idx, out) in child.audio.outs.iter().enumerate() {
6450                            let targets = out.connections.lock().clone();
6451                            for tgt in targets {
6452                                let _ = AudioIO::disconnect(out, &tgt);
6453                                if let Some((tgt_name, tgt_port)) =
6454                                    self.find_audio_io_owner(state, &tgt)
6455                                {
6456                                    disconnect_actions.push(Action::Disconnect {
6457                                        from_track: track_name.clone(),
6458                                        from_port: port_idx,
6459                                        to_track: tgt_name,
6460                                        to_port: tgt_port,
6461                                        kind: Kind::Audio,
6462                                    });
6463                                } else if let Some(tgt_port) = hw_outputs
6464                                    .iter()
6465                                    .position(|hw_out| std::sync::Arc::ptr_eq(hw_out, &tgt))
6466                                {
6467                                    disconnect_actions.push(Action::Disconnect {
6468                                        from_track: track_name.clone(),
6469                                        from_port: port_idx,
6470                                        to_track: "hw:out".to_string(),
6471                                        to_port: tgt_port,
6472                                        kind: Kind::Audio,
6473                                    });
6474                                }
6475                            }
6476                        }
6477
6478                        // Remove MIDI hardware routes.
6479                        for route in self
6480                            .midi_hw_in_routes
6481                            .iter()
6482                            .filter(|r| r.to_track == *track_name)
6483                        {
6484                            disconnect_actions.push(Action::Disconnect {
6485                                from_track: format!("midi:hw:in:{}", route.device),
6486                                from_port: 0,
6487                                to_track: track_name.clone(),
6488                                to_port: route.to_port,
6489                                kind: Kind::MIDI,
6490                            });
6491                        }
6492                        self.midi_hw_in_routes.retain(|r| r.to_track != *track_name);
6493
6494                        for route in self
6495                            .midi_hw_out_routes
6496                            .iter()
6497                            .filter(|r| r.from_track == *track_name)
6498                        {
6499                            disconnect_actions.push(Action::Disconnect {
6500                                from_track: track_name.clone(),
6501                                from_port: route.from_port,
6502                                to_track: format!("midi:hw:out:{}", route.device),
6503                                to_port: 0,
6504                                kind: Kind::MIDI,
6505                            });
6506                        }
6507                        self.midi_hw_out_routes
6508                            .retain(|r| r.from_track != *track_name);
6509
6510                        // Remove track-to-track MIDI connections where this track is the source.
6511                        for (port_idx, out) in child.midi.outs.iter().enumerate() {
6512                            let targets = out.lock().connections.clone();
6513                            for tgt in targets {
6514                                if let Some((tgt_name, tgt_port, _)) =
6515                                    self.find_midi_io_owner(state, &tgt)
6516                                {
6517                                    let _ = MIDIIO::disconnect(out, &tgt);
6518                                    disconnect_actions.push(Action::Disconnect {
6519                                        from_track: track_name.clone(),
6520                                        from_port: port_idx,
6521                                        to_track: tgt_name,
6522                                        to_port: tgt_port,
6523                                        kind: Kind::MIDI,
6524                                    });
6525                                }
6526                            }
6527                        }
6528                    }
6529
6530                    // Remove track-to-track MIDI connections where this track is the target.
6531                    let child_input_arcs: Vec<_> =
6532                        if let Some(child_arc) = state.tracks.get(track_name).cloned() {
6533                            let child = child_arc.lock();
6534                            child.midi.ins.clone()
6535                        } else {
6536                            Vec::new()
6537                        };
6538                    for (other_name, other_track) in &state.tracks {
6539                        if other_name == track_name {
6540                            continue;
6541                        }
6542                        let other = other_track.lock();
6543                        for (out_port, out) in other.midi.outs.iter().enumerate() {
6544                            let targets = out.lock().connections.clone();
6545                            for tgt in targets {
6546                                if let Some(to_port) = child_input_arcs
6547                                    .iter()
6548                                    .position(|inp| std::sync::Arc::ptr_eq(inp, &tgt))
6549                                {
6550                                    let _ = MIDIIO::disconnect(out, &tgt);
6551                                    disconnect_actions.push(Action::Disconnect {
6552                                        from_track: other_name.clone(),
6553                                        from_port: out_port,
6554                                        to_track: track_name.clone(),
6555                                        to_port,
6556                                        kind: Kind::MIDI,
6557                                    });
6558                                }
6559                            }
6560                        }
6561                    }
6562                }
6563
6564                // Apply the parent change.
6565                {
6566                    track.lock().parent_track = parent_name.clone();
6567                }
6568
6569                // Connect to the new parent and add to its child list.
6570                if let Some(parent_name) = parent_name {
6571                    let state = self.state.lock();
6572                    if let (Some(parent_arc), Some(child_arc)) = (
6573                        state.tracks.get(parent_name).cloned(),
6574                        state.tracks.get(track_name).cloned(),
6575                    ) {
6576                        {
6577                            let parent = parent_arc.lock();
6578                            parent.child_tracks.push(child_arc.clone());
6579                        }
6580                        {
6581                            let child = child_arc.lock();
6582                            let parent = parent_arc.lock();
6583                            // Folder input -> child input (one-to-one when counts match).
6584                            if parent.audio.ins.len() == child.audio.ins.len() {
6585                                for (parent_in, child_in) in
6586                                    parent.audio.ins.iter().zip(child.audio.ins.iter())
6587                                {
6588                                    Track::connect_directed_audio(parent_in, child_in);
6589                                }
6590                            }
6591                            // Child output -> folder output (one-to-one when counts match).
6592                            if parent.audio.outs.len() == child.audio.outs.len() {
6593                                for (child_out, parent_out) in
6594                                    child.audio.outs.iter().zip(parent.audio.outs.iter())
6595                                {
6596                                    AudioIO::connect(child_out, parent_out);
6597                                }
6598                            }
6599                            // Folder MIDI input -> child MIDI input (one-to-one when counts match).
6600                            if parent.midi.ins.len() == child.midi.ins.len() {
6601                                for (parent_in, child_in) in
6602                                    parent.midi.ins.iter().zip(child.midi.ins.iter())
6603                                {
6604                                    let child_in_lock = child_in.lock();
6605                                    if !child_in_lock
6606                                        .connections
6607                                        .iter()
6608                                        .any(|c| Arc::ptr_eq(c, parent_in))
6609                                    {
6610                                        child_in_lock.connections.push(parent_in.clone());
6611                                    }
6612                                }
6613                            }
6614                            // Child MIDI output -> folder MIDI output (one-to-one when counts match).
6615                            if parent.midi.outs.len() == child.midi.outs.len() {
6616                                for (child_out, parent_out) in
6617                                    child.midi.outs.iter().zip(parent.midi.outs.iter())
6618                                {
6619                                    let child_out_lock = child_out.lock();
6620                                    if !child_out_lock
6621                                        .connections
6622                                        .iter()
6623                                        .any(|c| Arc::ptr_eq(c, parent_out))
6624                                    {
6625                                        child_out_lock.connections.push(parent_out.clone());
6626                                    }
6627                                }
6628                            }
6629                            child.invalidate_audio_route_cache();
6630                            parent.invalidate_audio_route_cache();
6631                            child.invalidate_midi_route_cache();
6632                            parent.invalidate_midi_route_cache();
6633                        }
6634                    }
6635                }
6636
6637                // Restore default input->output passthrough so audio/MIDI can flow
6638                // through the track whether it is a root track or a folder child.
6639                {
6640                    let state = self.state.lock();
6641                    if let Some(child_arc) = state.tracks.get(track_name).cloned() {
6642                        let child = child_arc.lock();
6643                        child.ensure_default_audio_passthrough();
6644                        child.ensure_default_midi_passthrough();
6645                    }
6646                }
6647
6648                for action in disconnect_actions {
6649                    self.notify_clients(Ok(action)).await;
6650                }
6651
6652                self.notify_clients(Ok(Action::TrackSetParent {
6653                    track_name: track_name.clone(),
6654                    parent_name: parent_name.clone(),
6655                }))
6656                .await;
6657            }
6658            Action::TrackToggleFolder { ref track_name } => {
6659                let track = match self.track_handle_or_err(track_name) {
6660                    Ok(track) => track,
6661                    Err(e) => {
6662                        self.notify_clients(Err(e)).await;
6663                        return;
6664                    }
6665                };
6666                {
6667                    let t = track.lock();
6668                    t.folder_open = !t.folder_open;
6669                }
6670                self.notify_clients(Ok(Action::TrackToggleFolder {
6671                    track_name: track_name.clone(),
6672                }))
6673                .await;
6674
6675                self.notify_clients(Ok(Action::TrackSetFolder {
6676                    track_name: track_name.clone(),
6677                    is_folder: track.lock().is_folder,
6678                }))
6679                .await;
6680            }
6681            Action::TrackSetMidiLaneChannel {
6682                ref track_name,
6683                lane,
6684                channel,
6685            } => {
6686                let track = match self.track_handle_or_err(track_name) {
6687                    Ok(track) => track,
6688                    Err(e) => {
6689                        self.notify_clients(Err(e)).await;
6690                        return;
6691                    }
6692                };
6693                track.lock().set_midi_lane_channel(lane, channel);
6694            }
6695            Action::TrackSetFrozen {
6696                ref track_name,
6697                frozen,
6698            } => {
6699                let track = match self.track_handle_or_err(track_name) {
6700                    Ok(track) => track,
6701                    Err(e) => {
6702                        self.notify_clients(Err(e)).await;
6703                        return;
6704                    }
6705                };
6706                track.lock().set_frozen(frozen);
6707            }
6708            Action::TrackSetSessionSlot {
6709                ref track_name,
6710                scene_index,
6711                ref clip_id,
6712            } => {
6713                let track = match self.track_handle_or_err(track_name) {
6714                    Ok(track) => track,
6715                    Err(e) => {
6716                        self.notify_clients(Err(e)).await;
6717                        return;
6718                    }
6719                };
6720                let track = track.lock();
6721                match clip_id {
6722                    Some(id) => {
6723                        track.session_slots.insert(scene_index, id.clone());
6724                    }
6725                    None => {
6726                        track.session_slots.remove(&scene_index);
6727                    }
6728                }
6729            }
6730            Action::TrackOfflineBounce {
6731                track_name,
6732                output_path,
6733                start_sample,
6734                length_samples,
6735                automation_lanes,
6736                apply_fader,
6737            } => {
6738                if self.offline_bounce_jobs.contains_key(&track_name) {
6739                    self.notify_clients(Err(format!(
6740                        "Offline bounce for track '{}' is already in progress",
6741                        track_name
6742                    )))
6743                    .await;
6744                    return;
6745                }
6746                if let Err(e) = self.track_handle_or_err(&track_name) {
6747                    self.notify_clients(Err(e)).await;
6748                    return;
6749                }
6750                if length_samples == 0 {
6751                    self.notify_clients(Err(format!(
6752                        "Track '{}' has no renderable content for offline bounce",
6753                        track_name
6754                    )))
6755                    .await;
6756                    return;
6757                }
6758                let Some(worker_index) = self.take_ready_worker_index() else {
6759                    self.pending_requests
6760                        .push_front(Action::TrackOfflineBounce {
6761                            track_name,
6762                            output_path,
6763                            start_sample,
6764                            length_samples,
6765                            automation_lanes,
6766                            apply_fader,
6767                        });
6768                    return;
6769                };
6770                let cancel = Arc::new(AtomicBool::new(false));
6771                self.offline_bounce_jobs.insert(
6772                    track_name.clone(),
6773                    OfflineBounceJob {
6774                        cancel: cancel.clone(),
6775                    },
6776                );
6777                let track_name_clone = track_name.clone();
6778                let worker = &self.workers[worker_index];
6779                let job = crate::message::OfflineBounceWork {
6780                    state: self.state.clone(),
6781                    track_name,
6782                    output_path,
6783                    start_sample,
6784                    length_samples,
6785                    tempo_bpm: self.tempo_bpm,
6786                    tsig_num: self.tsig_num,
6787                    tsig_denom: self.tsig_denom,
6788                    automation_lanes,
6789                    cancel,
6790                    apply_fader,
6791                };
6792                if let Err(e) = worker.tx.send(Message::ProcessOfflineBounce(job)).await {
6793                    self.offline_bounce_jobs.remove(&track_name_clone);
6794                    self.notify_clients(Err(format!("Failed to schedule offline bounce: {e}")))
6795                        .await;
6796                }
6797                return;
6798            }
6799            Action::TrackOfflineBounceCancel { .. } => {}
6800            Action::TrackOfflineBounceCancelAll => {}
6801            Action::TrackOfflineBounceCanceled { .. } => {}
6802            Action::TrackOfflineBounceProgress { .. } => {}
6803            Action::PianoKey {
6804                ref track_name,
6805                note,
6806                velocity,
6807                on,
6808            } => {
6809                if let Some(track) = self.state.lock().tracks.get(track_name) {
6810                    let status = if on { 0x90 } else { 0x80 };
6811                    let event = MidiEvent::new(0, vec![status, note.min(127), velocity.min(127)]);
6812                    track.lock().push_hw_midi_events(&[event]);
6813                }
6814            }
6815            Action::ModifyMidiNotes { .. }
6816            | Action::ModifyMidiControllers { .. }
6817            | Action::DeleteMidiControllers { .. }
6818            | Action::InsertMidiControllers { .. }
6819            | Action::DeleteMidiNotes { .. }
6820            | Action::InsertMidiNotes { .. } => {
6821                if let Err(e) = self.apply_midi_edit_action(&action_to_process) {
6822                    self.notify_clients(Err(e)).await;
6823                    return;
6824                }
6825            }
6826            Action::SetMidiSysExEvents { .. } => {
6827                if let Err(e) = self.apply_midi_edit_action(&action_to_process) {
6828                    self.notify_clients(Err(e)).await;
6829                    return;
6830                }
6831            }
6832            Action::TrackClearDefaultPassthrough { ref track_name } => {
6833                if self
6834                    .reject_if_track_frozen(track_name, "plugin graph editing")
6835                    .await
6836                {
6837                    return;
6838                }
6839                let track = match self.track_handle_or_err(track_name) {
6840                    Ok(track) => track,
6841                    Err(e) => {
6842                        self.notify_clients(Err(e)).await;
6843                        return;
6844                    }
6845                };
6846                track.lock().clear_default_passthrough();
6847            }
6848            Action::TrackClearPlugins { ref track_name } => {
6849                if self
6850                    .reject_if_track_frozen(track_name, "plugin graph editing")
6851                    .await
6852                {
6853                    return;
6854                }
6855                let track = match self.track_handle_or_err(track_name) {
6856                    Ok(track) => track,
6857                    Err(e) => {
6858                        self.notify_clients(Err(e)).await;
6859                        return;
6860                    }
6861                };
6862                track.lock().clear_plugins();
6863                self.notify_clients(Ok(Action::Log {
6864                    source: "engine".to_string(),
6865                    message: format!("Cleared plugins from track '{track_name}'"),
6866                }))
6867                .await;
6868            }
6869            #[cfg(all(unix, not(target_os = "macos")))]
6870            Action::ClipSetLv2PluginState { ref track_name, .. } => {
6871                self.notify_clients(Err(format!(
6872                    "Track '{}': clip LV2 plugin state changes are not supported",
6873                    track_name
6874                )))
6875                .await;
6876            }
6877            Action::TrackGetClapNoteNames { ref track_name } => {
6878                let track = match self.track_handle_or_err(track_name) {
6879                    Ok(track) => track,
6880                    Err(e) => {
6881                        self.notify_clients(Err(e)).await;
6882                        return;
6883                    }
6884                };
6885                let note_names = track.lock().get_clap_note_names();
6886                self.notify_clients(Ok(Action::TrackClapNoteNames {
6887                    track_name: track_name.clone(),
6888                    note_names,
6889                }))
6890                .await;
6891            }
6892            Action::TrackGetPluginGraph { ref track_name } => {
6893                let track = match self.track_handle_or_err(track_name) {
6894                    Ok(track) => track,
6895                    Err(e) => {
6896                        self.notify_clients(Err(e)).await;
6897                        return;
6898                    }
6899                };
6900                let (plugins, connections, connectable_connections) = {
6901                    let track = track.lock();
6902                    (
6903                        track.plugin_graph_plugins(),
6904                        track.plugin_graph_connections(),
6905                        track.connectable_connections(),
6906                    )
6907                };
6908                self.notify_clients(Ok(Action::TrackPluginGraph {
6909                    track_name: track_name.clone(),
6910                    plugins,
6911                    connections,
6912                    connectable_connections,
6913                }))
6914                .await;
6915                return;
6916            }
6917            Action::TrackPluginGraph { .. } => {}
6918            Action::TrackConnectPluginAudio {
6919                ref track_name,
6920                ref from_node,
6921                from_port,
6922                ref to_node,
6923                to_port,
6924            } => {
6925                if self
6926                    .reject_if_track_frozen(track_name, "plugin routing changes")
6927                    .await
6928                {
6929                    return;
6930                }
6931                let track = match self.track_handle_or_err(track_name) {
6932                    Ok(track) => track,
6933                    Err(e) => {
6934                        self.notify_clients(Err(e)).await;
6935                        return;
6936                    }
6937                };
6938                if let Err(e) = track.lock().connect_plugin_audio(
6939                    from_node.clone(),
6940                    from_port,
6941                    to_node.clone(),
6942                    to_port,
6943                ) {
6944                    self.notify_clients(Err(e)).await;
6945                    return;
6946                }
6947            }
6948            Action::TrackConnectPluginMidi {
6949                ref track_name,
6950                ref from_node,
6951                from_port,
6952                ref to_node,
6953                to_port,
6954            } => {
6955                if self
6956                    .reject_if_track_frozen(track_name, "plugin routing changes")
6957                    .await
6958                {
6959                    return;
6960                }
6961                let track = match self.track_handle_or_err(track_name) {
6962                    Ok(track) => track,
6963                    Err(e) => {
6964                        self.notify_clients(Err(e)).await;
6965                        return;
6966                    }
6967                };
6968                if let Err(e) = track.lock().connect_plugin_midi(
6969                    from_node.clone(),
6970                    from_port,
6971                    to_node.clone(),
6972                    to_port,
6973                ) {
6974                    self.notify_clients(Err(e)).await;
6975                    return;
6976                }
6977            }
6978            Action::TrackDisconnectPluginAudio {
6979                ref track_name,
6980                ref from_node,
6981                from_port,
6982                ref to_node,
6983                to_port,
6984            } => {
6985                if self
6986                    .reject_if_track_frozen(track_name, "plugin routing changes")
6987                    .await
6988                {
6989                    return;
6990                }
6991                let track = match self.track_handle_or_err(track_name) {
6992                    Ok(track) => track,
6993                    Err(e) => {
6994                        self.notify_clients(Err(e)).await;
6995                        return;
6996                    }
6997                };
6998                if let Err(e) = track.lock().disconnect_plugin_audio(
6999                    from_node.clone(),
7000                    from_port,
7001                    to_node.clone(),
7002                    to_port,
7003                ) {
7004                    self.notify_clients(Err(e)).await;
7005                    return;
7006                }
7007            }
7008            Action::TrackDisconnectPluginMidi {
7009                ref track_name,
7010                ref from_node,
7011                from_port,
7012                ref to_node,
7013                to_port,
7014            } => {
7015                if self
7016                    .reject_if_track_frozen(track_name, "plugin routing changes")
7017                    .await
7018                {
7019                    return;
7020                }
7021                let track = match self.track_handle_or_err(track_name) {
7022                    Ok(track) => track,
7023                    Err(e) => {
7024                        self.notify_clients(Err(e)).await;
7025                        return;
7026                    }
7027                };
7028                if let Err(e) = track.lock().disconnect_plugin_midi(
7029                    from_node.clone(),
7030                    from_port,
7031                    to_node.clone(),
7032                    to_port,
7033                ) {
7034                    self.notify_clients(Err(e)).await;
7035                    return;
7036                }
7037            }
7038            Action::TrackConnectAudio {
7039                ref track_name,
7040                ref from,
7041                from_port,
7042                ref to,
7043                to_port,
7044            } => {
7045                if self
7046                    .reject_if_track_frozen(track_name, "routing changes")
7047                    .await
7048                {
7049                    return;
7050                }
7051                let track = match self.track_handle_or_err(track_name) {
7052                    Ok(track) => track,
7053                    Err(e) => {
7054                        self.notify_clients(Err(e)).await;
7055                        return;
7056                    }
7057                };
7058                if let Err(e) = track.lock().connect_audio_connectable(
7059                    from.clone(),
7060                    from_port,
7061                    to.clone(),
7062                    to_port,
7063                ) {
7064                    self.notify_clients(Err(e)).await;
7065                    return;
7066                }
7067            }
7068            Action::TrackDisconnectAudio {
7069                ref track_name,
7070                ref from,
7071                from_port,
7072                ref to,
7073                to_port,
7074            } => {
7075                if self
7076                    .reject_if_track_frozen(track_name, "routing changes")
7077                    .await
7078                {
7079                    return;
7080                }
7081                let track = match self.track_handle_or_err(track_name) {
7082                    Ok(track) => track,
7083                    Err(e) => {
7084                        self.notify_clients(Err(e)).await;
7085                        return;
7086                    }
7087                };
7088                if let Err(e) = track.lock().disconnect_audio_connectable(
7089                    from.clone(),
7090                    from_port,
7091                    to.clone(),
7092                    to_port,
7093                ) {
7094                    self.notify_clients(Err(e)).await;
7095                    return;
7096                }
7097            }
7098            Action::TrackConnectMidi {
7099                ref track_name,
7100                ref from,
7101                from_port,
7102                ref to,
7103                to_port,
7104            } => {
7105                if self
7106                    .reject_if_track_frozen(track_name, "routing changes")
7107                    .await
7108                {
7109                    return;
7110                }
7111                let track = match self.track_handle_or_err(track_name) {
7112                    Ok(track) => track,
7113                    Err(e) => {
7114                        self.notify_clients(Err(e)).await;
7115                        return;
7116                    }
7117                };
7118                if let Err(e) = track.lock().connect_midi_connectable(
7119                    from.clone(),
7120                    from_port,
7121                    to.clone(),
7122                    to_port,
7123                ) {
7124                    self.notify_clients(Err(e)).await;
7125                    return;
7126                }
7127            }
7128            Action::TrackDisconnectMidi {
7129                ref track_name,
7130                ref from,
7131                from_port,
7132                ref to,
7133                to_port,
7134            } => {
7135                if self
7136                    .reject_if_track_frozen(track_name, "routing changes")
7137                    .await
7138                {
7139                    return;
7140                }
7141                let track = match self.track_handle_or_err(track_name) {
7142                    Ok(track) => track,
7143                    Err(e) => {
7144                        self.notify_clients(Err(e)).await;
7145                        return;
7146                    }
7147                };
7148                if let Err(e) = track.lock().disconnect_midi_connectable(
7149                    from.clone(),
7150                    from_port,
7151                    to.clone(),
7152                    to_port,
7153                ) {
7154                    self.notify_clients(Err(e)).await;
7155                    return;
7156                }
7157            }
7158            #[cfg(all(unix, not(target_os = "macos")))]
7159            Action::ListLv2Plugins => {
7160                match crate::plugins::scan_plugins::<crate::plugins::types::Lv2PluginInfo>("lv2") {
7161                    Ok(plugins) => {
7162                        self.notify_clients(Ok(Action::Lv2Plugins(plugins))).await;
7163                    }
7164                    Err(e) => {
7165                        tracing::error!("LV2 plugin scan failed: {e}");
7166                        self.notify_clients(Ok(Action::Lv2PluginsUnavailable { error: e }))
7167                            .await;
7168                    }
7169                }
7170                return;
7171            }
7172            #[cfg(all(unix, not(target_os = "macos")))]
7173            Action::Lv2Plugins(_) => {}
7174            #[cfg(all(unix, not(target_os = "macos")))]
7175            Action::Lv2PluginsUnavailable { .. } => {}
7176            Action::ListVst3Plugins => {
7177                match crate::plugins::scan_plugins::<crate::plugins::types::Vst3PluginInfo>("vst3")
7178                {
7179                    Ok(plugins) => {
7180                        self.notify_clients(Ok(Action::Vst3Plugins(plugins))).await;
7181                    }
7182                    Err(e) => {
7183                        tracing::error!("VST3 plugin scan failed: {e}");
7184                        self.notify_clients(Ok(Action::Vst3PluginsUnavailable { error: e }))
7185                            .await;
7186                    }
7187                }
7188                return;
7189            }
7190            Action::Vst3Plugins(_) => {}
7191            Action::Vst3PluginsUnavailable { .. } => {}
7192            Action::ListClapPlugins => {
7193                match crate::plugins::scan_plugins::<crate::plugins::types::ClapPluginInfo>("clap")
7194                {
7195                    Ok(plugins) => {
7196                        self.notify_clients(Ok(Action::ClapPlugins(plugins))).await;
7197                    }
7198                    Err(e) => {
7199                        tracing::error!("CLAP plugin scan failed: {e}");
7200                        self.notify_clients(Ok(Action::ClapPluginsUnavailable { error: e }))
7201                            .await;
7202                    }
7203                }
7204                return;
7205            }
7206            Action::ListClapPluginsWithCapabilities => {
7207                match crate::plugins::scan_plugins::<crate::plugins::types::ClapPluginInfo>("clap")
7208                {
7209                    Ok(plugins) => {
7210                        self.notify_clients(Ok(Action::ClapPlugins(plugins))).await;
7211                    }
7212                    Err(e) => {
7213                        tracing::error!("CLAP plugin scan failed: {e}");
7214                        self.notify_clients(Ok(Action::ClapPluginsUnavailable { error: e }))
7215                            .await;
7216                    }
7217                }
7218                return;
7219            }
7220            Action::ClapPlugins(_) => {}
7221            Action::ClapPluginsUnavailable { .. } => {}
7222            Action::TrackLoadClapPlugin {
7223                ref track_name,
7224                ref plugin_path,
7225                instance_id,
7226            } => {
7227                if self
7228                    .reject_if_track_frozen(track_name, "CLAP plugin loading")
7229                    .await
7230                {
7231                    return;
7232                }
7233                let track = match self.track_handle_or_err(track_name) {
7234                    Ok(track) => track,
7235                    Err(e) => {
7236                        self.notify_clients(Err(e)).await;
7237                        return;
7238                    }
7239                };
7240                let track = track.lock();
7241                if track.audio.processing {
7242                    self.notify_clients(Err(format!(
7243                        "Track '{}' is currently processing audio; stop playback before loading CLAP plugins",
7244                        track_name
7245                    )))
7246                    .await;
7247                    return;
7248                }
7249                if let Err(e) = track.load_clap_plugin(plugin_path, instance_id) {
7250                    self.notify_clients(Err(e)).await;
7251                    return;
7252                }
7253                self.notify_clients(Ok(Action::Log {
7254                    source: "engine".to_string(),
7255                    message: format!("CLAP plugin loaded on track '{track_name}': {plugin_path}"),
7256                }))
7257                .await;
7258                if let Some(instance) = track.clap_plugins.last()
7259                    && let Some(stderr) = instance.processor.lock().take_stderr()
7260                {
7261                    let source = format!("clap:{plugin_path}");
7262                    self.spawn_plugin_host_stderr_reader(stderr, source);
7263                    self.notify_clients(Ok(Action::Log {
7264                        source: "engine".to_string(),
7265                        message: format!(
7266                            "Attached stderr reader for CLAP plugin on track '{track_name}'"
7267                        ),
7268                    }))
7269                    .await;
7270                }
7271            }
7272            Action::TrackUnloadClapPlugin {
7273                ref track_name,
7274                ref plugin_path,
7275            } => {
7276                if self
7277                    .reject_if_track_frozen(track_name, "CLAP plugin unloading")
7278                    .await
7279                {
7280                    return;
7281                }
7282                let track = match self.track_handle_or_err(track_name) {
7283                    Ok(track) => track,
7284                    Err(e) => {
7285                        self.notify_clients(Err(e)).await;
7286                        return;
7287                    }
7288                };
7289                let track = track.lock();
7290                if track.audio.processing {
7291                    self.notify_clients(Err(format!(
7292                        "Track '{}' is currently processing audio; stop playback before unloading CLAP plugins",
7293                        track_name
7294                    )))
7295                    .await;
7296                    return;
7297                }
7298                if let Err(e) = track.unload_clap_plugin(plugin_path) {
7299                    self.notify_clients(Err(e)).await;
7300                    return;
7301                }
7302            }
7303            Action::TrackUnloadClapPluginInstance {
7304                ref track_name,
7305                instance_id,
7306            } => {
7307                if self
7308                    .reject_if_track_frozen(track_name, "CLAP plugin unloading")
7309                    .await
7310                {
7311                    return;
7312                }
7313                let track = match self.track_handle_or_err(track_name) {
7314                    Ok(track) => track,
7315                    Err(e) => {
7316                        self.notify_clients(Err(e)).await;
7317                        return;
7318                    }
7319                };
7320                let track = track.lock();
7321                if track.audio.processing {
7322                    self.notify_clients(Err(format!(
7323                        "Track '{}' is currently processing audio; stop playback before unloading CLAP plugins",
7324                        track_name
7325                    )))
7326                    .await;
7327                    return;
7328                }
7329                if let Err(e) = track.unload_clap_plugin_instance(instance_id) {
7330                    self.notify_clients(Err(e)).await;
7331                    return;
7332                }
7333            }
7334            Action::TrackShowClapGui {
7335                ref track_name,
7336                instance_id,
7337            } => {
7338                let track = match self.track_handle_or_err(track_name) {
7339                    Ok(track) => track,
7340                    Err(e) => {
7341                        self.notify_clients(Err(e)).await;
7342                        return;
7343                    }
7344                };
7345                if let Err(e) = track.lock().show_clap_gui(instance_id) {
7346                    self.notify_clients(Err(e)).await;
7347                    return;
7348                }
7349            }
7350            Action::TrackLoadVst3Plugin {
7351                ref track_name,
7352                ref plugin_path,
7353                instance_id,
7354            } => {
7355                if self
7356                    .reject_if_track_frozen(track_name, "VST3 plugin loading")
7357                    .await
7358                {
7359                    return;
7360                }
7361                let track = match self.track_handle_or_err(track_name) {
7362                    Ok(track) => track,
7363                    Err(e) => {
7364                        self.notify_clients(Err(e)).await;
7365                        return;
7366                    }
7367                };
7368                let track = track.lock();
7369                if track.audio.processing {
7370                    self.notify_clients(Err(format!(
7371                        "Track '{}' is currently processing audio; stop playback before loading VST3 plugins",
7372                        track_name
7373                    )))
7374                    .await;
7375                    return;
7376                }
7377                if let Err(e) = track.load_vst3_plugin(plugin_path, instance_id) {
7378                    self.notify_clients(Err(e)).await;
7379                    return;
7380                }
7381                if let Some(instance) = track.vst3_plugins.last()
7382                    && let Some(stderr) = instance.processor.lock().take_stderr()
7383                {
7384                    let source = format!("vst3:{plugin_path}");
7385                    self.spawn_plugin_host_stderr_reader(stderr, source);
7386                }
7387            }
7388            Action::TrackUnloadVst3Plugin {
7389                ref track_name,
7390                ref plugin_path,
7391            } => {
7392                if self
7393                    .reject_if_track_frozen(track_name, "VST3 plugin unloading")
7394                    .await
7395                {
7396                    return;
7397                }
7398                let track = match self.track_handle_or_err(track_name) {
7399                    Ok(track) => track,
7400                    Err(e) => {
7401                        self.notify_clients(Err(e)).await;
7402                        return;
7403                    }
7404                };
7405                let track = track.lock();
7406                if track.audio.processing {
7407                    self.notify_clients(Err(format!(
7408                        "Track '{}' is currently processing audio; stop playback before unloading VST3 plugins",
7409                        track_name
7410                    )))
7411                    .await;
7412                    return;
7413                }
7414                if let Err(e) = track.unload_vst3_plugin(plugin_path) {
7415                    self.notify_clients(Err(e)).await;
7416                    return;
7417                }
7418            }
7419            Action::TrackUnloadVst3PluginInstance {
7420                ref track_name,
7421                instance_id,
7422            } => {
7423                if self
7424                    .reject_if_track_frozen(track_name, "VST3 plugin unloading")
7425                    .await
7426                {
7427                    return;
7428                }
7429                let track = match self.track_handle_or_err(track_name) {
7430                    Ok(track) => track,
7431                    Err(e) => {
7432                        self.notify_clients(Err(e)).await;
7433                        return;
7434                    }
7435                };
7436                let track = track.lock();
7437                if track.audio.processing {
7438                    self.notify_clients(Err(format!(
7439                        "Track '{}' is currently processing audio; stop playback before unloading VST3 plugins",
7440                        track_name
7441                    )))
7442                    .await;
7443                    return;
7444                }
7445                if let Err(e) = track.unload_vst3_plugin_instance(instance_id) {
7446                    self.notify_clients(Err(e)).await;
7447                    return;
7448                }
7449            }
7450            Action::TrackShowVst3Gui {
7451                ref track_name,
7452                instance_id,
7453            } => {
7454                let track = match self.track_handle_or_err(track_name) {
7455                    Ok(track) => track,
7456                    Err(e) => {
7457                        self.notify_clients(Err(e)).await;
7458                        return;
7459                    }
7460                };
7461                if let Err(e) = track.lock().show_vst3_gui(instance_id) {
7462                    self.notify_clients(Err(e)).await;
7463                    return;
7464                }
7465            }
7466            #[cfg(all(unix, not(target_os = "macos")))]
7467            Action::TrackLoadLv2Plugin {
7468                ref track_name,
7469                ref plugin_uri,
7470                instance_id,
7471            } => {
7472                if self
7473                    .reject_if_track_frozen(track_name, "LV2 plugin loading")
7474                    .await
7475                {
7476                    return;
7477                }
7478                let track = match self.track_handle_or_err(track_name) {
7479                    Ok(track) => track,
7480                    Err(e) => {
7481                        self.notify_clients(Err(e)).await;
7482                        return;
7483                    }
7484                };
7485                let track = track.lock();
7486                if track.audio.processing {
7487                    self.notify_clients(Err(format!(
7488                        "Track '{}' is currently processing audio; stop playback before loading LV2 plugins",
7489                        track_name
7490                    )))
7491                    .await;
7492                    return;
7493                }
7494                if let Err(e) = track.load_lv2_plugin(plugin_uri, instance_id) {
7495                    self.notify_clients(Err(e)).await;
7496                    return;
7497                }
7498                if let Some(instance) = track.lv2_plugins.last()
7499                    && let Some(stderr) = instance.processor.lock().take_stderr()
7500                {
7501                    let source = format!("lv2:{plugin_uri}");
7502                    self.spawn_plugin_host_stderr_reader(stderr, source);
7503                }
7504            }
7505            #[cfg(all(unix, not(target_os = "macos")))]
7506            Action::TrackUnloadLv2Plugin {
7507                ref track_name,
7508                ref plugin_uri,
7509            } => {
7510                if self
7511                    .reject_if_track_frozen(track_name, "LV2 plugin unloading")
7512                    .await
7513                {
7514                    return;
7515                }
7516                let track = match self.track_handle_or_err(track_name) {
7517                    Ok(track) => track,
7518                    Err(e) => {
7519                        self.notify_clients(Err(e)).await;
7520                        return;
7521                    }
7522                };
7523                let track = track.lock();
7524                if track.audio.processing {
7525                    self.notify_clients(Err(format!(
7526                        "Track '{}' is currently processing audio; stop playback before unloading LV2 plugins",
7527                        track_name
7528                    )))
7529                    .await;
7530                    return;
7531                }
7532                if let Err(e) = track.unload_lv2_plugin(plugin_uri) {
7533                    self.notify_clients(Err(e)).await;
7534                    return;
7535                }
7536            }
7537            #[cfg(all(unix, not(target_os = "macos")))]
7538            Action::TrackUnloadLv2PluginInstance {
7539                ref track_name,
7540                instance_id,
7541            } => {
7542                if self
7543                    .reject_if_track_frozen(track_name, "LV2 plugin unloading")
7544                    .await
7545                {
7546                    return;
7547                }
7548                let track = match self.track_handle_or_err(track_name) {
7549                    Ok(track) => track,
7550                    Err(e) => {
7551                        self.notify_clients(Err(e)).await;
7552                        return;
7553                    }
7554                };
7555                let track = track.lock();
7556                if track.audio.processing {
7557                    self.notify_clients(Err(format!(
7558                        "Track '{}' is currently processing audio; stop playback before unloading LV2 plugins",
7559                        track_name
7560                    )))
7561                    .await;
7562                    return;
7563                }
7564                if let Err(e) = track.unload_lv2_plugin_instance(instance_id) {
7565                    self.notify_clients(Err(e)).await;
7566                    return;
7567                }
7568            }
7569            #[cfg(all(unix, not(target_os = "macos")))]
7570            Action::TrackShowLv2Gui {
7571                ref track_name,
7572                instance_id,
7573            } => {
7574                let track = match self.track_handle_or_err(track_name) {
7575                    Ok(track) => track,
7576                    Err(e) => {
7577                        self.notify_clients(Err(e)).await;
7578                        return;
7579                    }
7580                };
7581                if let Err(e) = track.lock().show_lv2_gui(instance_id) {
7582                    self.notify_clients(Err(e)).await;
7583                    return;
7584                }
7585            }
7586            Action::TrackSetPluginResourceDir {
7587                ref track_name,
7588                instance_id,
7589                ref format,
7590                ref directory,
7591            } => {
7592                let track = match self.track_handle_or_err(track_name) {
7593                    Ok(track) => track,
7594                    Err(e) => {
7595                        self.notify_clients(Err(e)).await;
7596                        return;
7597                    }
7598                };
7599                let dir = std::path::Path::new(directory);
7600                let result = if format.eq_ignore_ascii_case("CLAP") {
7601                    track.lock().set_clap_plugin_resource_dir(instance_id, dir)
7602                } else if format.eq_ignore_ascii_case("LV2") {
7603                    #[cfg(all(unix, not(target_os = "macos")))]
7604                    {
7605                        track.lock().set_lv2_plugin_resource_dir(instance_id, dir)
7606                    }
7607                    #[cfg(not(all(unix, not(target_os = "macos"))))]
7608                    Err("LV2 is not supported on this platform".to_string())
7609                } else {
7610                    Err(format!(
7611                        "Unsupported plugin format for resource dir: {format}"
7612                    ))
7613                };
7614                if let Err(e) = result {
7615                    self.notify_clients(Err(e)).await;
7616                    return;
7617                }
7618            }
7619            Action::TrackClapFileReferences {
7620                ref track_name,
7621                instance_id,
7622                refs: _,
7623            } => match self.track_handle_or_err(track_name) {
7624                Ok(track) => {
7625                    let refs = track.lock().clap_file_references(instance_id).unwrap_or_else(|e| {
7626                        tracing::warn!(track_name = %track_name, instance_id, error = %e, "Failed to enumerate CLAP file references");
7627                        Vec::new()
7628                    });
7629                    self.notify_clients(Ok(Action::TrackClapFileReferences {
7630                        track_name: track_name.clone(),
7631                        instance_id,
7632                        refs,
7633                    }))
7634                    .await;
7635                }
7636                Err(e) => {
7637                    self.notify_clients(Err(e)).await;
7638                }
7639            },
7640            Action::TrackUpdateClapFileReference {
7641                ref track_name,
7642                instance_id,
7643                index,
7644                ref path,
7645            } => {
7646                let track = match self.track_handle_or_err(track_name) {
7647                    Ok(track) => track,
7648                    Err(e) => {
7649                        self.notify_clients(Err(e)).await;
7650                        return;
7651                    }
7652                };
7653                if let Err(e) = track
7654                    .lock()
7655                    .update_clap_file_reference(instance_id, index, path)
7656                {
7657                    self.notify_clients(Err(e)).await;
7658                    return;
7659                }
7660            }
7661            Action::ClipSetPluginResourceDir {
7662                ref track_name,
7663                clip_idx,
7664                instance_id,
7665                ref format,
7666                ref directory,
7667            } => {
7668                let track = match self.track_handle_or_err(track_name) {
7669                    Ok(track) => track,
7670                    Err(e) => {
7671                        self.notify_clients(Err(e)).await;
7672                        return;
7673                    }
7674                };
7675                let dir = std::path::Path::new(directory);
7676                let track = track.lock();
7677                let result = if format.eq_ignore_ascii_case("CLAP") {
7678                    track.clip_set_clap_plugin_resource_dir(clip_idx, instance_id, dir)
7679                } else if format.eq_ignore_ascii_case("LV2") {
7680                    #[cfg(all(unix, not(target_os = "macos")))]
7681                    {
7682                        track.clip_set_lv2_plugin_resource_dir(clip_idx, instance_id, dir)
7683                    }
7684                    #[cfg(not(all(unix, not(target_os = "macos"))))]
7685                    Err("LV2 is not supported on this platform".to_string())
7686                } else {
7687                    Err(format!(
7688                        "Unsupported plugin format for resource dir: {format}"
7689                    ))
7690                };
7691                if let Err(e) = result {
7692                    self.notify_clients(Err(e)).await;
7693                    return;
7694                }
7695            }
7696            Action::ClipClapFileReferences {
7697                ref track_name,
7698                clip_idx,
7699                instance_id,
7700                refs: _,
7701            } => match self.track_handle_or_err(track_name) {
7702                Ok(track) => {
7703                    let track = track.lock();
7704                    let refs = track
7705                        .clip_clap_file_references(clip_idx, instance_id)
7706                        .unwrap_or_else(|e| {
7707                            tracing::warn!(
7708                                track_name = %track_name,
7709                                clip_idx,
7710                                instance_id,
7711                                error = %e,
7712                                "Failed to enumerate clip CLAP file references"
7713                            );
7714                            Vec::new()
7715                        });
7716                    self.notify_clients(Ok(Action::ClipClapFileReferences {
7717                        track_name: track_name.clone(),
7718                        clip_idx,
7719                        instance_id,
7720                        refs,
7721                    }))
7722                    .await;
7723                }
7724                Err(e) => {
7725                    self.notify_clients(Err(e)).await;
7726                }
7727            },
7728            Action::ClipUpdateClapFileReference {
7729                ref track_name,
7730                clip_idx,
7731                instance_id,
7732                index,
7733                ref path,
7734            } => {
7735                let track = match self.track_handle_or_err(track_name) {
7736                    Ok(track) => track,
7737                    Err(e) => {
7738                        self.notify_clients(Err(e)).await;
7739                        return;
7740                    }
7741                };
7742                if let Err(e) =
7743                    track
7744                        .lock()
7745                        .clip_update_clap_file_reference(clip_idx, instance_id, index, path)
7746                {
7747                    self.notify_clients(Err(e)).await;
7748                    return;
7749                }
7750            }
7751            Action::TrackSetClapParameter {
7752                ref track_name,
7753                instance_id,
7754                param_id,
7755                value,
7756            } => {
7757                if self
7758                    .reject_if_track_frozen(track_name, "CLAP parameter changes")
7759                    .await
7760                {
7761                    return;
7762                }
7763                match self.track_handle_or_err(track_name) {
7764                    Ok(track) => {
7765                        if let Err(e) =
7766                            track
7767                                .lock()
7768                                .set_clap_parameter(instance_id, param_id, value)
7769                        {
7770                            self.notify_clients(Err(e)).await;
7771                            return;
7772                        }
7773                        self.notify_clients(Ok(a.clone())).await;
7774                    }
7775                    Err(e) => {
7776                        self.notify_clients(Err(e)).await;
7777                    }
7778                }
7779            }
7780            Action::ClipSetClapParameter {
7781                ref track_name,
7782                clip_idx,
7783                instance_id,
7784                param_id,
7785                value,
7786            } => {
7787                if self
7788                    .reject_if_track_frozen(track_name, "CLAP parameter changes")
7789                    .await
7790                {
7791                    return;
7792                }
7793                match self.track_handle_or_err(track_name) {
7794                    Ok(track) => {
7795                        if let Err(e) = track.lock().clip_set_clap_parameter(
7796                            clip_idx,
7797                            instance_id,
7798                            param_id,
7799                            value,
7800                        ) {
7801                            self.notify_clients(Err(e)).await;
7802                            return;
7803                        }
7804                        self.notify_clients(Ok(a.clone())).await;
7805                    }
7806                    Err(e) => {
7807                        self.notify_clients(Err(e)).await;
7808                    }
7809                }
7810            }
7811            Action::TrackSetClapParameterAt {
7812                ref track_name,
7813                instance_id,
7814                param_id,
7815                value,
7816                frame,
7817            } => {
7818                if self
7819                    .reject_if_track_frozen(track_name, "CLAP parameter changes")
7820                    .await
7821                {
7822                    return;
7823                }
7824                match self.track_handle_or_err(track_name) {
7825                    Ok(track) => {
7826                        if let Err(e) =
7827                            track
7828                                .lock()
7829                                .set_clap_parameter_at(instance_id, param_id, value, frame)
7830                        {
7831                            self.notify_clients(Err(e)).await;
7832                            return;
7833                        }
7834                        self.notify_clients(Ok(a.clone())).await;
7835                    }
7836                    Err(e) => {
7837                        self.notify_clients(Err(e)).await;
7838                    }
7839                }
7840            }
7841            Action::TrackBeginClapParameterEdit {
7842                ref track_name,
7843                instance_id,
7844                param_id,
7845                frame,
7846            } => {
7847                if self
7848                    .reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
7849                    .await
7850                {
7851                    return;
7852                }
7853                match self.track_handle_or_err(track_name) {
7854                    Ok(track) => {
7855                        if let Err(e) =
7856                            track
7857                                .lock()
7858                                .begin_clap_parameter_edit(instance_id, param_id, frame)
7859                        {
7860                            self.notify_clients(Err(e)).await;
7861                            return;
7862                        }
7863                        self.notify_clients(Ok(a.clone())).await;
7864                    }
7865                    Err(e) => {
7866                        self.notify_clients(Err(e)).await;
7867                    }
7868                }
7869            }
7870            Action::TrackEndClapParameterEdit {
7871                ref track_name,
7872                instance_id,
7873                param_id,
7874                frame,
7875            } => {
7876                if self
7877                    .reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
7878                    .await
7879                {
7880                    return;
7881                }
7882                match self.track_handle_or_err(track_name) {
7883                    Ok(track) => {
7884                        if let Err(e) =
7885                            track
7886                                .lock()
7887                                .end_clap_parameter_edit(instance_id, param_id, frame)
7888                        {
7889                            self.notify_clients(Err(e)).await;
7890                            return;
7891                        }
7892                        self.notify_clients(Ok(a.clone())).await;
7893                    }
7894                    Err(e) => {
7895                        self.notify_clients(Err(e)).await;
7896                    }
7897                }
7898            }
7899            Action::TrackGetClapParameters {
7900                ref track_name,
7901                instance_id,
7902            } => match self.track_handle_or_err(track_name) {
7903                Ok(track) => match track.lock().get_clap_parameters(instance_id) {
7904                    Ok(parameters) => {
7905                        self.notify_clients(Ok(Action::TrackClapParameters {
7906                            track_name: track_name.clone(),
7907                            instance_id,
7908                            parameters,
7909                        }))
7910                        .await;
7911                    }
7912                    Err(e) => {
7913                        self.notify_clients(Err(e)).await;
7914                    }
7915                },
7916                Err(e) => {
7917                    self.notify_clients(Err(e)).await;
7918                }
7919            },
7920            Action::TrackClapParameters { .. } => {}
7921            Action::TrackClapSnapshotState {
7922                ref track_name,
7923                instance_id,
7924            } => match self.track_handle_or_err(track_name) {
7925                Ok(track) => {
7926                    let plugin_path = track
7927                        .lock()
7928                        .clap_plugins
7929                        .iter()
7930                        .find(|instance| instance.id == instance_id)
7931                        .map(|instance| instance.processor.lock().path().to_string())
7932                        .unwrap_or_default();
7933                    match track.lock().clap_snapshot_state(instance_id) {
7934                        Ok(state) => {
7935                            self.notify_clients(Ok(Action::TrackClapStateSnapshot {
7936                                track_name: track_name.clone(),
7937                                instance_id,
7938                                plugin_path,
7939                                state,
7940                            }))
7941                            .await;
7942                        }
7943                        Err(e) => {
7944                            self.notify_clients(Err(e)).await;
7945                        }
7946                    }
7947                }
7948                Err(e) => {
7949                    self.notify_clients(Err(e)).await;
7950                }
7951            },
7952            Action::ClipClapSnapshotState {
7953                ref track_name,
7954                clip_idx,
7955                instance_id,
7956            } => match self.track_handle_or_err(track_name) {
7957                Ok(track) => match track.lock().clip_clap_snapshot_state(clip_idx, instance_id) {
7958                    Ok((plugin_path, state)) => {
7959                        self.notify_clients(Ok(Action::ClipClapStateSnapshot {
7960                            track_name: track_name.clone(),
7961                            clip_idx,
7962                            instance_id,
7963                            plugin_path,
7964                            state,
7965                        }))
7966                        .await;
7967                    }
7968                    Err(e) => {
7969                        self.notify_clients(Err(e)).await;
7970                    }
7971                },
7972                Err(e) => {
7973                    self.notify_clients(Err(e)).await;
7974                }
7975            },
7976            Action::TrackClapStateSnapshot { .. } => {}
7977            Action::ClipClapStateSnapshot { .. } => {}
7978            Action::TrackClapStateDirty { .. } => {}
7979            Action::ClipClapStateDirty { .. } => {}
7980            Action::TrackClapRestoreState {
7981                ref track_name,
7982                instance_id,
7983                ref state,
7984            } => {
7985                if self
7986                    .reject_if_track_frozen(track_name, "CLAP state restore")
7987                    .await
7988                {
7989                    return;
7990                }
7991                let track = match self.track_handle_or_err(track_name) {
7992                    Ok(track) => track,
7993                    Err(e) => {
7994                        self.notify_clients(Err(e)).await;
7995                        return;
7996                    }
7997                };
7998                let track = track.lock();
7999                if track.audio.processing {
8000                    self.notify_clients(Err(format!(
8001                        "Track '{}' is currently processing audio; stop playback before restoring CLAP state",
8002                        track_name
8003                    )))
8004                    .await;
8005                    return;
8006                }
8007                if let Err(e) = track.clap_restore_state(instance_id, state) {
8008                    self.notify_clients(Err(e)).await;
8009                    return;
8010                }
8011            }
8012            Action::ClipClapRestoreState {
8013                ref track_name,
8014                clip_idx,
8015                instance_id,
8016                ref state,
8017            } => {
8018                if self
8019                    .reject_if_track_frozen(track_name, "CLAP state restore")
8020                    .await
8021                {
8022                    return;
8023                }
8024                let track = match self.track_handle_or_err(track_name) {
8025                    Ok(track) => track,
8026                    Err(e) => {
8027                        self.notify_clients(Err(e)).await;
8028                        return;
8029                    }
8030                };
8031                let track = track.lock();
8032                if track.audio.processing {
8033                    self.notify_clients(Err(format!(
8034                        "Track '{}' is currently processing audio; stop playback before restoring CLAP state",
8035                        track_name
8036                    )))
8037                    .await;
8038                    return;
8039                }
8040                if let Err(e) = track.clip_clap_restore_state(clip_idx, instance_id, state) {
8041                    self.notify_clients(Err(e)).await;
8042                    return;
8043                }
8044            }
8045            Action::TrackSnapshotAllClapStates { ref track_name } => {
8046                let track = match self.track_handle_or_err(track_name) {
8047                    Ok(track) => track,
8048                    Err(e) => {
8049                        self.notify_clients(Err(e)).await;
8050                        return;
8051                    }
8052                };
8053                let instances: Vec<_> = {
8054                    let locked = track.lock();
8055                    locked
8056                        .clap_plugins
8057                        .iter()
8058                        .map(|i| (i.id, i.processor.lock().path().to_string()))
8059                        .collect()
8060                };
8061                for (instance_id, plugin_path) in instances {
8062                    match track.lock().clap_snapshot_state(instance_id) {
8063                        Ok(state) => {
8064                            self.notify_clients(Ok(Action::TrackClapStateSnapshot {
8065                                track_name: track_name.clone(),
8066                                instance_id,
8067                                plugin_path,
8068                                state,
8069                            }))
8070                            .await;
8071                        }
8072                        Err(_e) => {}
8073                    }
8074                }
8075                self.notify_clients(Ok(Action::TrackSnapshotAllClapStatesDone {
8076                    track_name: track_name.clone(),
8077                }))
8078                .await;
8079            }
8080            Action::TrackSnapshotAllClapStatesDone { .. } => {}
8081            Action::TrackGetVst3Graph { ref track_name } => {
8082                match self.track_handle_or_err(track_name) {
8083                    Ok(track) => {
8084                        let t = track.lock();
8085                        let plugins = t.vst3_graph_plugins();
8086                        let connections = t.vst3_graph_connections();
8087                        self.notify_clients(Ok(Action::TrackVst3Graph {
8088                            track_name: track_name.clone(),
8089                            plugins,
8090                            connections,
8091                        }))
8092                        .await;
8093                    }
8094                    Err(e) => {
8095                        self.notify_clients(Err(e)).await;
8096                    }
8097                }
8098            }
8099            Action::TrackVst3Graph { .. } => {}
8100            Action::TrackSetVst3Parameter {
8101                ref track_name,
8102                instance_id,
8103                param_id,
8104                value,
8105            } => {
8106                if self
8107                    .reject_if_track_frozen(track_name, "VST3 parameter changes")
8108                    .await
8109                {
8110                    return;
8111                }
8112                match self.track_handle_or_err(track_name) {
8113                    Ok(track) => {
8114                        if let Err(e) =
8115                            track
8116                                .lock()
8117                                .set_vst3_parameter(instance_id, param_id, value)
8118                        {
8119                            self.notify_clients(Err(e)).await;
8120                            return;
8121                        }
8122                        self.notify_clients(Ok(a.clone())).await;
8123                    }
8124                    Err(e) => {
8125                        self.notify_clients(Err(e)).await;
8126                    }
8127                }
8128            }
8129            Action::TrackSetPluginBypassed {
8130                ref track_name,
8131                instance_id,
8132                ref format,
8133                bypassed,
8134            } => match self.track_handle_or_err(track_name) {
8135                Ok(track) => {
8136                    let result = match format.as_str() {
8137                        "CLAP" => track.lock().set_clap_plugin_bypassed(instance_id, bypassed),
8138                        "VST3" => track.lock().set_vst3_plugin_bypassed(instance_id, bypassed),
8139                        #[cfg(all(unix, not(target_os = "macos")))]
8140                        "LV2" => track.lock().set_lv2_plugin_bypassed(instance_id, bypassed),
8141                        _ => Err(format!("Unknown plugin format for bypass: {format}")),
8142                    };
8143                    if let Err(e) = result {
8144                        self.notify_clients(Err(e)).await;
8145                        return;
8146                    }
8147                    self.notify_clients(Ok(a.clone())).await;
8148                }
8149                Err(e) => {
8150                    self.notify_clients(Err(e)).await;
8151                }
8152            },
8153            Action::TrackGetVst3Parameters {
8154                ref track_name,
8155                instance_id,
8156            } => match self.track_handle_or_err(track_name) {
8157                Ok(track) => match track.lock().get_vst3_parameters(instance_id) {
8158                    Ok(parameters) => {
8159                        self.notify_clients(Ok(Action::TrackVst3Parameters {
8160                            track_name: track_name.clone(),
8161                            instance_id,
8162                            parameters,
8163                        }))
8164                        .await;
8165                    }
8166                    Err(e) => {
8167                        self.notify_clients(Err(e)).await;
8168                    }
8169                },
8170                Err(e) => {
8171                    self.notify_clients(Err(e)).await;
8172                }
8173            },
8174            Action::TrackVst3Parameters { .. } => {}
8175            Action::TrackVst3SnapshotState {
8176                ref track_name,
8177                instance_id,
8178            } => match self.track_handle_or_err(track_name) {
8179                Ok(track) => match track.lock().vst3_snapshot_state(instance_id) {
8180                    Ok(state) => {
8181                        self.notify_clients(Ok(Action::TrackVst3StateSnapshot {
8182                            track_name: track_name.clone(),
8183                            instance_id,
8184                            state,
8185                        }))
8186                        .await;
8187                    }
8188                    Err(e) => {
8189                        self.notify_clients(Err(e)).await;
8190                    }
8191                },
8192                Err(e) => {
8193                    self.notify_clients(Err(e)).await;
8194                }
8195            },
8196            Action::ClipVst3SnapshotState {
8197                ref track_name,
8198                clip_idx,
8199                instance_id,
8200            } => match self.track_handle_or_err(track_name) {
8201                Ok(track) => match track.lock().clip_vst3_snapshot_state(clip_idx, instance_id) {
8202                    Ok(state) => {
8203                        self.notify_clients(Ok(Action::ClipVst3StateSnapshot {
8204                            track_name: track_name.clone(),
8205                            clip_idx,
8206                            instance_id,
8207                            state,
8208                        }))
8209                        .await;
8210                    }
8211                    Err(e) => {
8212                        self.notify_clients(Err(e)).await;
8213                    }
8214                },
8215                Err(e) => {
8216                    self.notify_clients(Err(e)).await;
8217                }
8218            },
8219            Action::TrackVst3StateSnapshot { .. } => {}
8220            Action::ClipVst3StateSnapshot { .. } => {}
8221            Action::TrackVst3RestoreState {
8222                ref track_name,
8223                instance_id,
8224                ref state,
8225            } => match self.track_handle_or_err(track_name) {
8226                Ok(track) => {
8227                    if let Err(e) = track.lock().vst3_restore_state(instance_id, state) {
8228                        self.notify_clients(Err(e)).await;
8229                        return;
8230                    }
8231                    self.notify_clients(Ok(a.clone())).await;
8232                }
8233                Err(e) => {
8234                    self.notify_clients(Err(e)).await;
8235                }
8236            },
8237            Action::TrackConnectVst3Audio {
8238                ref track_name,
8239                ref from_node,
8240                from_port,
8241                ref to_node,
8242                to_port,
8243            } => {
8244                if self
8245                    .reject_if_track_frozen(track_name, "VST3 routing changes")
8246                    .await
8247                {
8248                    return;
8249                }
8250                match self.track_handle_or_err(track_name) {
8251                    Ok(track) => {
8252                        if let Err(e) = track
8253                            .lock()
8254                            .connect_vst3_audio(from_node, from_port, to_node, to_port)
8255                        {
8256                            self.notify_clients(Err(e)).await;
8257                            return;
8258                        }
8259                        self.notify_clients(Ok(a.clone())).await;
8260                    }
8261                    Err(e) => {
8262                        self.notify_clients(Err(e)).await;
8263                    }
8264                }
8265            }
8266            Action::TrackDisconnectVst3Audio {
8267                ref track_name,
8268                ref from_node,
8269                from_port,
8270                ref to_node,
8271                to_port,
8272            } => {
8273                if self
8274                    .reject_if_track_frozen(track_name, "VST3 routing changes")
8275                    .await
8276                {
8277                    return;
8278                }
8279                match self.track_handle_or_err(track_name) {
8280                    Ok(track) => {
8281                        if let Err(e) = track
8282                            .lock()
8283                            .disconnect_vst3_audio(from_node, from_port, to_node, to_port)
8284                        {
8285                            self.notify_clients(Err(e)).await;
8286                            return;
8287                        }
8288                        self.notify_clients(Ok(a.clone())).await;
8289                    }
8290                    Err(e) => {
8291                        self.notify_clients(Err(e)).await;
8292                    }
8293                }
8294            }
8295            Action::ClipMove {
8296                ref kind,
8297                ref from,
8298                ref to,
8299                copy,
8300            } => {
8301                if let Some(from_track_handle) = self.state.lock().tracks.get(&from.track_name)
8302                    && let Some(to_track_handle) = self.state.lock().tracks.get(&to.track_name)
8303                {
8304                    let from_track = from_track_handle.lock();
8305                    let to_track = to_track_handle.lock();
8306                    match kind {
8307                        Kind::Audio => {
8308                            if from.clip_index >= from_track.audio.clips.len() {
8309                                self.notify_clients(Err(format!(
8310                                    "Clip index {} is too high, as track {} has only {} clips!",
8311                                    from.clip_index,
8312                                    from_track.name.clone(),
8313                                    from_track.audio.clips.len(),
8314                                )))
8315                                .await;
8316                                return;
8317                            }
8318                            if from_track.audio.ins.len() != to_track.audio.ins.len() {
8319                                self.notify_clients(Err(format!(
8320                                    "Cannot move/copy audio clip from '{}' ({} inputs) to '{}' ({} inputs)",
8321                                    from_track.name,
8322                                    from_track.audio.ins.len(),
8323                                    to_track.name,
8324                                    to_track.audio.ins.len()
8325                                )))
8326                                .await;
8327                                return;
8328                            }
8329                            let clip_copy = from_track.audio.clips[from.clip_index].clone();
8330                            if !copy {
8331                                from_track.audio.clips.remove(from.clip_index);
8332                            }
8333                            let mut clip_copy = clip_copy;
8334                            clip_copy.start = to.sample_offset;
8335                            let max_lane = to_track.audio.ins.len().saturating_sub(1);
8336                            clip_copy.input_channel = to.input_channel.min(max_lane);
8337                            to_track.audio.clips.push(clip_copy);
8338                        }
8339                        Kind::MIDI => {
8340                            if from.clip_index >= from_track.midi.clips.len() {
8341                                self.notify_clients(Err(format!(
8342                                    "Clip index {} is too high, as track {} has only {} clips!",
8343                                    from.clip_index,
8344                                    from_track.name.clone(),
8345                                    from_track.midi.clips.len(),
8346                                )))
8347                                .await;
8348                                return;
8349                            }
8350                            let clip_copy = from_track.midi.clips[from.clip_index].clone();
8351                            if !copy {
8352                                from_track.midi.clips.remove(from.clip_index);
8353                            }
8354                            let mut clip_copy = clip_copy;
8355                            clip_copy.start = to.sample_offset;
8356                            let max_lane = to_track.midi.ins.len().saturating_sub(1);
8357                            clip_copy.input_channel = to.input_channel.min(max_lane);
8358                            to_track.midi.clips.push(clip_copy);
8359                        }
8360                    }
8361                }
8362            }
8363            Action::AddClip {
8364                ref clip_id,
8365                ref name,
8366                ref track_name,
8367                start,
8368                length,
8369                offset,
8370                input_channel,
8371                muted,
8372                ref peaks_file,
8373                kind,
8374                fade_enabled,
8375                fade_in_samples,
8376                fade_out_samples,
8377                ref source_name,
8378                source_offset,
8379                source_length,
8380                ref preview_name,
8381                ref pitch_correction_points,
8382                pitch_correction_frame_likeness,
8383                pitch_correction_inertia_ms,
8384                pitch_correction_formant_compensation,
8385                ref plugin_graph_json,
8386            } => {
8387                self.add_clip_to_track(ClipAddRequest {
8388                    clip_id,
8389                    name,
8390                    track_name,
8391                    start,
8392                    length,
8393                    offset,
8394                    input_channel,
8395                    muted,
8396                    peaks_file: peaks_file.clone(),
8397                    kind,
8398                    fade_enabled,
8399                    fade_in_samples,
8400                    fade_out_samples,
8401                    source_name: source_name.clone(),
8402                    source_offset,
8403                    source_length,
8404                    preview_name: preview_name.clone(),
8405                    pitch_correction_points: pitch_correction_points.clone(),
8406                    pitch_correction_frame_likeness,
8407                    pitch_correction_inertia_ms,
8408                    pitch_correction_formant_compensation,
8409                    plugin_graph_json: plugin_graph_json.clone(),
8410                });
8411                if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
8412                    let track_name = track_name.clone();
8413                    tokio::task::spawn_blocking(move || {
8414                        track.lock().preload_clips();
8415                        tracing::debug!("Preloaded clips for track '{}' after AddClip", track_name);
8416                    });
8417                }
8418            }
8419            Action::AddGroupedClip {
8420                ref track_name,
8421                kind,
8422                ref audio_clip,
8423                ref midi_clip,
8424            } => {
8425                self.add_grouped_clip_to_track(
8426                    track_name,
8427                    kind,
8428                    audio_clip.clone(),
8429                    midi_clip.clone(),
8430                );
8431                if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
8432                    let track_name = track_name.clone();
8433                    tokio::task::spawn_blocking(move || {
8434                        track.lock().preload_clips();
8435                        tracing::debug!(
8436                            "Preloaded clips for track '{}' after AddGroupedClip",
8437                            track_name
8438                        );
8439                    });
8440                }
8441            }
8442            Action::RemoveClip {
8443                ref track_name,
8444                kind,
8445                ref clip_indices,
8446            } => {
8447                self.remove_clips_from_track(track_name, kind, clip_indices);
8448            }
8449            Action::RenameClip {
8450                ref track_name,
8451                kind,
8452                clip_index,
8453                ref new_name,
8454            } => {
8455                self.rename_clip_references(track_name, kind, clip_index, new_name);
8456            }
8457            Action::SetClipSourceName {
8458                ref track_name,
8459                kind,
8460                clip_index,
8461                ref name,
8462            } => {
8463                self.set_clip_source_name(track_name, clip_index, kind, name.clone());
8464            }
8465            Action::SetClipFade {
8466                ref track_name,
8467                clip_index,
8468                kind,
8469                fade_enabled,
8470                fade_in_samples,
8471                fade_out_samples,
8472            } => {
8473                self.set_clip_fade(
8474                    track_name,
8475                    clip_index,
8476                    kind,
8477                    fade_enabled,
8478                    fade_in_samples,
8479                    fade_out_samples,
8480                );
8481            }
8482            Action::SetClipBounds {
8483                ref track_name,
8484                clip_index,
8485                kind,
8486                start,
8487                length,
8488                offset,
8489            } => {
8490                self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
8491            }
8492            Action::SyncClipBounds {
8493                ref track_name,
8494                clip_index,
8495                kind,
8496                start,
8497                length,
8498                offset,
8499            } => {
8500                self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
8501            }
8502            Action::SetClipMuted {
8503                ref track_name,
8504                clip_index,
8505                kind,
8506                muted,
8507            } => {
8508                self.set_clip_muted(track_name, clip_index, kind, muted);
8509            }
8510            Action::SetClipPluginGraphJson {
8511                ref track_name,
8512                clip_index,
8513                ref plugin_graph_json,
8514            } => {
8515                self.set_clip_plugin_graph_json(track_name, clip_index, plugin_graph_json.clone());
8516            }
8517            Action::SetClipPitchCorrection {
8518                ref track_name,
8519                clip_index,
8520                ref preview_name,
8521                ref source_name,
8522                source_offset,
8523                source_length,
8524                ref pitch_correction_points,
8525                pitch_correction_frame_likeness,
8526                pitch_correction_inertia_ms,
8527                pitch_correction_formant_compensation,
8528            } => {
8529                self.set_clip_pitch_correction(
8530                    track_name,
8531                    clip_index,
8532                    preview_name.clone(),
8533                    source_name.clone(),
8534                    source_offset,
8535                    source_length,
8536                    pitch_correction_points.clone(),
8537                    pitch_correction_frame_likeness,
8538                    pitch_correction_inertia_ms,
8539                    pitch_correction_formant_compensation,
8540                );
8541            }
8542            Action::Connect {
8543                ref from_track,
8544                from_port,
8545                ref to_track,
8546                to_port,
8547                kind,
8548            } => {
8549                match kind {
8550                    Kind::Audio => {
8551                        let (from_audio_io, to_audio_io) = self
8552                            .resolve_audio_route_ports(from_track, from_port, to_track, to_port);
8553                        match (from_audio_io, to_audio_io) {
8554                            (Some(source), Some(target)) => {
8555                                if from_track != "hw:in"
8556                                    && to_track != "hw:out"
8557                                    && self.check_if_leads_to_kind(
8558                                        Kind::Audio,
8559                                        to_track,
8560                                        from_track,
8561                                    )
8562                                {
8563                                    self.notify_clients(Err(
8564                                        "Circular routing is not allowed!".into()
8565                                    ))
8566                                    .await;
8567                                    return;
8568                                }
8569                                crate::audio::io::AudioIO::connect(&source, &target);
8570                            }
8571                            (None, _) => {
8572                                self.notify_clients(Err(format!(
8573                                    "Source track '{}' not found",
8574                                    from_track
8575                                )))
8576                                .await;
8577                                return;
8578                            }
8579                            (_, None) => {
8580                                self.notify_clients(Err(format!(
8581                                    "Destination track '{}' not found",
8582                                    to_track
8583                                )))
8584                                .await;
8585                                return;
8586                            }
8587                        }
8588                    }
8589                    Kind::MIDI => {
8590                        let from_hw_in_device = Self::midi_hw_in_device(from_track);
8591                        let to_hw_out_device = Self::midi_hw_out_device(to_track);
8592                        let from_is_invalid_hw = Self::midi_hw_out_device(from_track).is_some();
8593                        let to_is_invalid_hw = Self::midi_hw_in_device(to_track).is_some();
8594
8595                        if from_is_invalid_hw || to_is_invalid_hw {
8596                            self.notify_clients(Err(
8597                                "Invalid MIDI hardware connection direction".to_string()
8598                            ))
8599                            .await;
8600                            return;
8601                        }
8602
8603                        if from_hw_in_device.is_none()
8604                            && to_hw_out_device.is_none()
8605                            && self.check_if_leads_to_kind(Kind::MIDI, to_track, from_track)
8606                        {
8607                            self.notify_clients(Err("Circular routing is not allowed!".into()))
8608                                .await;
8609                            return;
8610                        }
8611
8612                        let state = self.state.lock();
8613                        let from_track_handle = state.tracks.get(from_track);
8614                        let to_track_handle = state.tracks.get(to_track);
8615
8616                        if let (Some(from_device), Some(to_device)) =
8617                            (from_hw_in_device, to_hw_out_device)
8618                        {
8619                            let route = MidiHwThruRoute {
8620                                from_device: from_device.to_string(),
8621                                to_device: to_device.to_string(),
8622                            };
8623                            if !self.midi_hw_thru_routes.iter().any(|r| r == &route) {
8624                                self.midi_hw_thru_routes.push(route);
8625                            }
8626                        } else if let Some(device) = from_hw_in_device {
8627                            if let Some(t_t) = to_track_handle {
8628                                if t_t.lock().midi.ins.get(to_port).is_none() {
8629                                    self.notify_clients(Err(format!(
8630                                        "MIDI input port {} not found on track '{}'",
8631                                        to_port, to_track
8632                                    )))
8633                                    .await;
8634                                    return;
8635                                }
8636                                let route = MidiHwInRoute {
8637                                    device: device.to_string(),
8638                                    to_track: to_track.to_string(),
8639                                    to_port,
8640                                };
8641                                if !self.midi_hw_in_routes.iter().any(|r| r == &route) {
8642                                    self.midi_hw_in_routes.push(route);
8643                                }
8644                            } else {
8645                                self.notify_clients(Err(format!(
8646                                    "MIDI destination track not found: {}",
8647                                    to_track
8648                                )))
8649                                .await;
8650                                return;
8651                            }
8652                        } else if let Some(device) = to_hw_out_device {
8653                            if let Some(f_t) = from_track_handle {
8654                                if f_t.lock().midi.outs.get(from_port).is_none() {
8655                                    self.notify_clients(Err(format!(
8656                                        "MIDI output port {} not found on track '{}'",
8657                                        from_port, from_track
8658                                    )))
8659                                    .await;
8660                                    return;
8661                                }
8662                                let route = MidiHwOutRoute {
8663                                    from_track: from_track.to_string(),
8664                                    from_port,
8665                                    device: device.to_string(),
8666                                };
8667                                if !self.midi_hw_out_routes.iter().any(|r| r == &route) {
8668                                    self.midi_hw_out_routes.push(route);
8669                                }
8670                            } else {
8671                                self.notify_clients(Err(format!(
8672                                    "MIDI source track not found: {}",
8673                                    from_track
8674                                )))
8675                                .await;
8676                                return;
8677                            }
8678                        } else {
8679                            match (from_track_handle, to_track_handle) {
8680                                (Some(f_t), Some(t_t)) => {
8681                                    let to_in_res = t_t.lock().midi.ins.get(to_port).cloned();
8682                                    if let Some(to_in) = to_in_res {
8683                                        let from_track = f_t.lock();
8684                                        if let Err(e) =
8685                                            from_track.midi.connect_out(from_port, to_in)
8686                                        {
8687                                            self.notify_clients(Err(e)).await;
8688                                            return;
8689                                        }
8690                                        from_track.invalidate_midi_route_cache();
8691                                    } else {
8692                                        self.notify_clients(Err(format!(
8693                                            "MIDI input port {} not found on track '{}'",
8694                                            to_port, to_track
8695                                        )))
8696                                        .await;
8697                                        return;
8698                                    }
8699                                }
8700                                _ => {
8701                                    self.notify_clients(Err(format!(
8702                                        "MIDI tracks not found: {} or {}",
8703                                        from_track, to_track
8704                                    )))
8705                                    .await;
8706                                    return;
8707                                }
8708                            }
8709                        }
8710                    }
8711                };
8712            }
8713            Action::Disconnect {
8714                ref from_track,
8715                from_port,
8716                ref to_track,
8717                to_port,
8718                kind,
8719            } => {
8720                if kind == Kind::Audio {
8721                    if let Err(e) = self.disconnect_audio_route_and_notify(a.clone()).await {
8722                        self.notify_clients(Err(e)).await;
8723                    }
8724                } else if kind == Kind::MIDI {
8725                    let from_hw_in_device = Self::midi_hw_in_device(from_track);
8726                    let to_hw_out_device = Self::midi_hw_out_device(to_track);
8727
8728                    if let (Some(from_device), Some(to_device)) =
8729                        (from_hw_in_device, to_hw_out_device)
8730                    {
8731                        let before = self.midi_hw_thru_routes.len();
8732                        self.midi_hw_thru_routes.retain(|r| {
8733                            !(r.from_device == from_device && r.to_device == to_device)
8734                        });
8735                        if self.midi_hw_thru_routes.len() < before {
8736                            self.notify_clients(Ok(a.clone())).await;
8737                        } else {
8738                            self.notify_clients(Err(format!(
8739                                "Disconnect failed: MIDI route not found ({} -> {})",
8740                                from_track, to_track
8741                            )))
8742                            .await;
8743                        }
8744                        return;
8745                    }
8746
8747                    if let Some(device) = from_hw_in_device {
8748                        let before = self.midi_hw_in_routes.len();
8749                        self.midi_hw_in_routes.retain(|r| {
8750                            !(r.device == device && r.to_track == *to_track && r.to_port == to_port)
8751                        });
8752                        if self.midi_hw_in_routes.len() < before {
8753                            self.notify_clients(Ok(a.clone())).await;
8754                        } else {
8755                            self.notify_clients(Err(format!(
8756                                "Disconnect failed: MIDI route not found ({} -> {})",
8757                                from_track, to_track
8758                            )))
8759                            .await;
8760                        }
8761                        return;
8762                    }
8763
8764                    if let Some(device) = to_hw_out_device {
8765                        let before = self.midi_hw_out_routes.len();
8766                        self.midi_hw_out_routes.retain(|r| {
8767                            !(r.from_track == *from_track
8768                                && r.from_port == from_port
8769                                && r.device == device)
8770                        });
8771                        if self.midi_hw_out_routes.len() < before {
8772                            self.notify_clients(Ok(a.clone())).await;
8773                        } else {
8774                            self.notify_clients(Err(format!(
8775                                "Disconnect failed: MIDI route not found ({} -> {})",
8776                                from_track, to_track
8777                            )))
8778                            .await;
8779                        }
8780                        return;
8781                    }
8782
8783                    let state = self.state.lock();
8784                    if let (Some(f_t), Some(t_t)) =
8785                        (state.tracks.get(from_track), state.tracks.get(to_track))
8786                        && let Some(to_in) = t_t.lock().midi.ins.get(to_port).cloned()
8787                    {
8788                        let from_track = f_t.lock();
8789                        if let Err(e) = from_track.midi.disconnect_out(from_port, &to_in) {
8790                            self.notify_clients(Err(e)).await;
8791                        } else {
8792                            from_track.invalidate_midi_route_cache();
8793                            self.notify_clients(Ok(a.clone())).await;
8794                        }
8795                    } else {
8796                        self.notify_clients(Err(format!(
8797                            "Disconnect failed: MIDI ports not found ({} -> {})",
8798                            from_track, to_track
8799                        )))
8800                        .await;
8801                    }
8802                }
8803            }
8804
8805            Action::OpenAudioDevice {
8806                ref device,
8807                ref input_device,
8808                sample_rate_hz,
8809                bits,
8810                exclusive,
8811                period_frames,
8812                nperiods,
8813                sync_mode,
8814                ..
8815            } => {
8816                #[cfg(unix)]
8817                {
8818                    let request = AudioOpenRequest {
8819                        device,
8820                        input_device: input_device.as_deref(),
8821                        sample_rate_hz,
8822                        bits,
8823                        exclusive,
8824                        period_frames,
8825                        nperiods,
8826                        sync_mode,
8827                    };
8828                    if self.maybe_open_jack_runtime(request).await.is_some() {
8829                        return;
8830                    }
8831                }
8832                let hw_opts = Self::build_hw_options(exclusive, period_frames, nperiods, sync_mode);
8833                let open_result = self
8834                    .open_non_jack_audio_device(
8835                        device,
8836                        input_device.as_deref(),
8837                        sample_rate_hz,
8838                        bits,
8839                        hw_opts,
8840                    )
8841                    .await;
8842                match open_result {
8843                    Ok(()) => {}
8844                    Err(e) => {
8845                        error!("Failed to open audio device: {e}");
8846                        self.notify_clients(Err(e)).await;
8847                        return;
8848                    }
8849                }
8850                self.finalize_open_audio_device().await;
8851                if let Some(hw) = &self.hw_driver {
8852                    let effective_action = {
8853                        let hw = hw.lock();
8854                        Action::OpenAudioDevice {
8855                            device: device.clone(),
8856                            input_device: input_device.clone(),
8857                            sample_rate_hz: hw.sample_rate(),
8858                            bits: hw.sample_bits(),
8859                            exclusive,
8860                            period_frames,
8861                            nperiods,
8862                            sync_mode,
8863                            actual_period_frames: hw.cycle_samples(),
8864                            input_channels: hw.input_channels(),
8865                            output_channels: hw.output_channels(),
8866                            bytes_per_frame: hw.frame_size_bytes(),
8867                        }
8868                    };
8869                    action_to_process = effective_action;
8870                }
8871            }
8872            Action::JackAddAudioInputPort => {
8873                #[cfg(unix)]
8874                {
8875                    if let Some(jack) = self.jack_runtime.clone() {
8876                        let (input_channels, output_channels, rate) = {
8877                            let jack = jack.lock();
8878                            if let Err(e) = jack.add_audio_input_port() {
8879                                self.notify_clients(Err(e)).await;
8880                                return;
8881                            }
8882                            (
8883                                jack.input_channels(),
8884                                jack.output_channels(),
8885                                jack.sample_rate,
8886                            )
8887                        };
8888                        self.publish_hw_infos(input_channels, output_channels, rate)
8889                            .await;
8890                        self.notify_clients(Ok(a.clone())).await;
8891                    } else {
8892                        self.notify_clients(Err(
8893                            "JACK runtime is not active; open the JACK backend first".to_string(),
8894                        ))
8895                        .await;
8896                    }
8897                }
8898                #[cfg(not(unix))]
8899                {
8900                    self.notify_clients(Err(
8901                        "JACK backend is not available on this platform build".to_string(),
8902                    ))
8903                    .await;
8904                }
8905            }
8906            Action::JackRemoveAudioInputPort(_removed_port) => {
8907                #[cfg(unix)]
8908                {
8909                    let removed_port = _removed_port;
8910                    if let Some(jack) = self.jack_runtime.clone() {
8911                        let (removed_port, removed_io) = {
8912                            let jack = jack.lock();
8913                            let removed_port = Some(removed_port);
8914                            let removed_io =
8915                                removed_port.and_then(|port| jack.input_audio_port(port));
8916                            match (removed_port, removed_io) {
8917                                (Some(port), Some(io)) => (port, io),
8918                                _ => {
8919                                    self.notify_clients(Err(
8920                                        "JACK audio input port index is out of range".to_string(),
8921                                    ))
8922                                    .await;
8923                                    return;
8924                                }
8925                            }
8926                        };
8927                        let reindex_notifications =
8928                            self.reindex_notifications_for_removed_hw_input(removed_port);
8929                        for disconnect in
8930                            self.disconnect_actions_for_removed_hw_input(removed_port, &removed_io)
8931                        {
8932                            if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await
8933                            {
8934                                self.notify_clients(Err(e)).await;
8935                                return;
8936                            }
8937                        }
8938                        let (input_channels, output_channels, rate) = {
8939                            let jack = jack.lock();
8940                            if let Err(e) = jack.remove_audio_input_port(removed_port) {
8941                                self.notify_clients(Err(e)).await;
8942                                return;
8943                            }
8944                            (
8945                                jack.input_channels(),
8946                                jack.output_channels(),
8947                                jack.sample_rate,
8948                            )
8949                        };
8950                        for action in reindex_notifications {
8951                            self.notify_clients(Ok(action)).await;
8952                        }
8953                        self.publish_hw_infos(input_channels, output_channels, rate)
8954                            .await;
8955                        self.notify_clients(Ok(a.clone())).await;
8956                    } else {
8957                        self.notify_clients(Err(
8958                            "JACK runtime is not active; open the JACK backend first".to_string(),
8959                        ))
8960                        .await;
8961                    }
8962                }
8963                #[cfg(not(unix))]
8964                {
8965                    self.notify_clients(Err(
8966                        "JACK backend is not available on this platform build".to_string(),
8967                    ))
8968                    .await;
8969                }
8970            }
8971            Action::JackAddAudioOutputPort => {
8972                #[cfg(unix)]
8973                {
8974                    if let Some(jack) = self.jack_runtime.clone() {
8975                        let (input_channels, output_channels, rate) = {
8976                            let jack = jack.lock();
8977                            if let Err(e) = jack.add_audio_output_port() {
8978                                self.notify_clients(Err(e)).await;
8979                                return;
8980                            }
8981                            (
8982                                jack.input_channels(),
8983                                jack.output_channels(),
8984                                jack.sample_rate,
8985                            )
8986                        };
8987                        self.publish_hw_infos(input_channels, output_channels, rate)
8988                            .await;
8989                        self.notify_clients(Ok(a.clone())).await;
8990                    } else {
8991                        self.notify_clients(Err(
8992                            "JACK runtime is not active; open the JACK backend first".to_string(),
8993                        ))
8994                        .await;
8995                    }
8996                }
8997                #[cfg(not(unix))]
8998                {
8999                    self.notify_clients(Err(
9000                        "JACK backend is not available on this platform build".to_string(),
9001                    ))
9002                    .await;
9003                }
9004            }
9005            Action::JackRemoveAudioOutputPort(_removed_port) => {
9006                #[cfg(unix)]
9007                {
9008                    let removed_port = _removed_port;
9009                    if let Some(jack) = self.jack_runtime.clone() {
9010                        let (removed_port, removed_io) = {
9011                            let jack = jack.lock();
9012                            let removed_port = Some(removed_port);
9013                            let removed_io =
9014                                removed_port.and_then(|port| jack.output_audio_port(port));
9015                            match (removed_port, removed_io) {
9016                                (Some(port), Some(io)) => (port, io),
9017                                _ => {
9018                                    self.notify_clients(Err(
9019                                        "JACK audio output port index is out of range".to_string(),
9020                                    ))
9021                                    .await;
9022                                    return;
9023                                }
9024                            }
9025                        };
9026                        let reindex_notifications =
9027                            self.reindex_notifications_for_removed_hw_output(removed_port);
9028                        for disconnect in
9029                            self.disconnect_actions_for_removed_hw_output(removed_port, &removed_io)
9030                        {
9031                            if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await
9032                            {
9033                                self.notify_clients(Err(e)).await;
9034                                return;
9035                            }
9036                        }
9037                        let (input_channels, output_channels, rate) = {
9038                            let jack = jack.lock();
9039                            if let Err(e) = jack.remove_audio_output_port(removed_port) {
9040                                self.notify_clients(Err(e)).await;
9041                                return;
9042                            }
9043                            (
9044                                jack.input_channels(),
9045                                jack.output_channels(),
9046                                jack.sample_rate,
9047                            )
9048                        };
9049                        for action in reindex_notifications {
9050                            self.notify_clients(Ok(action)).await;
9051                        }
9052                        self.publish_hw_infos(input_channels, output_channels, rate)
9053                            .await;
9054                        self.notify_clients(Ok(a.clone())).await;
9055                    } else {
9056                        self.notify_clients(Err(
9057                            "JACK runtime is not active; open the JACK backend first".to_string(),
9058                        ))
9059                        .await;
9060                    }
9061                }
9062                #[cfg(not(unix))]
9063                {
9064                    self.notify_clients(Err(
9065                        "JACK backend is not available on this platform build".to_string(),
9066                    ))
9067                    .await;
9068                }
9069            }
9070            Action::OpenMidiInputDevice(ref device) => {
9071                let midi_hub = self.midi_hub.lock();
9072                if let Err(e) = midi_hub.open_input(device) {
9073                    self.notify_clients(Err(e)).await;
9074                    return;
9075                }
9076            }
9077            Action::OpenMidiOutputDevice(ref device) => {
9078                let midi_hub = self.midi_hub.lock();
9079                if let Err(e) = midi_hub.open_output(device) {
9080                    self.notify_clients(Err(e)).await;
9081                    return;
9082                }
9083            }
9084            Action::RequestSessionDiagnostics => {
9085                let (
9086                    track_count,
9087                    frozen_track_count,
9088                    audio_clip_count,
9089                    midi_clip_count,
9090                    lv2_instance_count,
9091                    vst3_instance_count,
9092                    clap_instance_count,
9093                ) = {
9094                    let tracks = &self.state.lock().tracks;
9095                    let mut track_count = 0usize;
9096                    let mut frozen_track_count = 0usize;
9097                    let mut audio_clip_count = 0usize;
9098                    let mut midi_clip_count = 0usize;
9099                    #[cfg(all(unix, not(target_os = "macos")))]
9100                    let mut lv2_instance_count = 0usize;
9101                    #[cfg(not(all(unix, not(target_os = "macos"))))]
9102                    let lv2_instance_count = 0usize;
9103                    let mut vst3_instance_count = 0usize;
9104                    let mut clap_instance_count = 0usize;
9105                    for track in tracks.values() {
9106                        let t = track.lock();
9107                        track_count += 1;
9108                        if t.frozen {
9109                            frozen_track_count += 1;
9110                        }
9111                        audio_clip_count += t.audio.clips.len();
9112                        midi_clip_count += t.midi.clips.len();
9113                        #[cfg(all(unix, not(target_os = "macos")))]
9114                        {
9115                            lv2_instance_count += t.lv2_plugins.len();
9116                        }
9117                        vst3_instance_count += t.vst3_plugins.len();
9118                        clap_instance_count += t.clap_plugins.len();
9119                    }
9120                    (
9121                        track_count,
9122                        frozen_track_count,
9123                        audio_clip_count,
9124                        midi_clip_count,
9125                        lv2_instance_count,
9126                        vst3_instance_count,
9127                        clap_instance_count,
9128                    )
9129                };
9130                #[cfg(not(all(unix, not(target_os = "macos"))))]
9131                let _lv2_instance_count = lv2_instance_count;
9132                let pending_hw_midi_events = self.pending_hw_midi_events.len()
9133                    + self
9134                        .pending_hw_midi_events_by_device
9135                        .values()
9136                        .map(std::vec::Vec::len)
9137                        .sum::<usize>();
9138                let sample_rate_hz = if let Some(hw) = &self.hw_driver {
9139                    hw.lock().sample_rate() as usize
9140                } else {
9141                    #[cfg(unix)]
9142                    {
9143                        self.jack_runtime
9144                            .as_ref()
9145                            .map(|j| j.lock().sample_rate)
9146                            .unwrap_or(0)
9147                    }
9148                    #[cfg(not(unix))]
9149                    0
9150                };
9151                let cycle_samples = self.current_cycle_samples();
9152                self.notify_clients(Ok(Action::SessionDiagnosticsReport {
9153                    track_count,
9154                    frozen_track_count,
9155                    audio_clip_count,
9156                    midi_clip_count,
9157                    #[cfg(all(unix, not(target_os = "macos")))]
9158                    lv2_instance_count,
9159                    vst3_instance_count,
9160                    clap_instance_count,
9161                    pending_requests: self.pending_requests.len(),
9162                    workers_total: self.workers.len(),
9163                    workers_ready: self.ready_workers.len(),
9164                    pending_hw_midi_events,
9165                    playing: self.playing,
9166                    transport_sample: self.transport_sample,
9167                    tempo_bpm: self.tempo_bpm,
9168                    sample_rate_hz,
9169                    cycle_samples,
9170                }))
9171                .await;
9172            }
9173            Action::RequestMidiLearnMappingsReport => {
9174                let mut lines = Vec::<String>::new();
9175                let fmt_binding = |b: &crate::message::MidiLearnBinding| {
9176                    let device = b.device.as_deref().unwrap_or("*");
9177                    format!("{device} CH{} CC{}", b.channel + 1, b.cc)
9178                };
9179                if let Some(b) = self.global_midi_learn_play_pause.as_ref() {
9180                    lines.push(format!("Global PlayPause: {}", fmt_binding(b)));
9181                }
9182                if let Some(b) = self.global_midi_learn_stop.as_ref() {
9183                    lines.push(format!("Global Stop: {}", fmt_binding(b)));
9184                }
9185                if let Some(b) = self.global_midi_learn_record_toggle.as_ref() {
9186                    lines.push(format!("Global RecordToggle: {}", fmt_binding(b)));
9187                }
9188                for (track_name, track) in self.state.lock().tracks.iter() {
9189                    let t = track.lock();
9190                    if let Some(b) = t.midi_learn_volume.as_ref() {
9191                        lines.push(format!("{} Volume: {}", track_name, fmt_binding(b)));
9192                    }
9193                    if let Some(b) = t.midi_learn_balance.as_ref() {
9194                        lines.push(format!("{} Balance: {}", track_name, fmt_binding(b)));
9195                    }
9196                    if let Some(b) = t.midi_learn_mute.as_ref() {
9197                        lines.push(format!("{} Mute: {}", track_name, fmt_binding(b)));
9198                    }
9199                    if let Some(b) = t.midi_learn_solo.as_ref() {
9200                        lines.push(format!("{} Solo: {}", track_name, fmt_binding(b)));
9201                    }
9202                    if let Some(b) = t.midi_learn_arm.as_ref() {
9203                        lines.push(format!("{} Arm: {}", track_name, fmt_binding(b)));
9204                    }
9205                    if let Some(b) = t.midi_learn_input_monitor.as_ref() {
9206                        lines.push(format!("{} InputMonitor: {}", track_name, fmt_binding(b)));
9207                    }
9208                    if let Some(b) = t.midi_learn_disk_monitor.as_ref() {
9209                        lines.push(format!("{} DiskMonitor: {}", track_name, fmt_binding(b)));
9210                    }
9211                }
9212                for ((track_name, scene_index), binding) in &self.session_midi_learn_slots {
9213                    lines.push(format!(
9214                        "{} Slot {}: {}",
9215                        track_name,
9216                        scene_index + 1,
9217                        fmt_binding(binding)
9218                    ));
9219                }
9220                for (scene_index, binding) in &self.session_midi_learn_scenes {
9221                    lines.push(format!(
9222                        "Scene {}: {}",
9223                        scene_index + 1,
9224                        fmt_binding(binding)
9225                    ));
9226                }
9227                for (track_name, binding) in &self.session_midi_learn_stop_track {
9228                    lines.push(format!("{} Stop: {}", track_name, fmt_binding(binding)));
9229                }
9230                if let Some(binding) = self.session_midi_learn_stop_all.as_ref() {
9231                    lines.push(format!("Stop All Clips: {}", fmt_binding(binding)));
9232                }
9233                if lines.is_empty() {
9234                    lines.push("No MIDI learn mappings configured".to_string());
9235                }
9236                self.notify_clients(Ok(Action::MidiLearnMappingsReport { lines }))
9237                    .await;
9238            }
9239            Action::ClearAllMidiLearnBindings => {
9240                self.pending_midi_learn = None;
9241                self.pending_global_midi_learn = None;
9242                self.pending_session_midi_learn = None;
9243                self.global_midi_learn_play_pause = None;
9244                self.global_midi_learn_stop = None;
9245                self.global_midi_learn_record_toggle = None;
9246                self.session_midi_learn_slots.clear();
9247                self.session_midi_learn_scenes.clear();
9248                self.session_midi_learn_stop_track.clear();
9249                self.session_midi_learn_stop_all = None;
9250                self.midi_cc_gate.clear();
9251                for track in self.state.lock().tracks.values() {
9252                    let t = track.lock();
9253                    t.midi_learn_volume = None;
9254                    t.midi_learn_balance = None;
9255                    t.midi_learn_mute = None;
9256                    t.midi_learn_solo = None;
9257                    t.midi_learn_arm = None;
9258                    t.midi_learn_input_monitor = None;
9259                    t.midi_learn_disk_monitor = None;
9260                }
9261            }
9262            #[cfg(all(unix, not(target_os = "macos")))]
9263            Action::TrackLv2PluginControls { .. } => {}
9264            #[cfg(all(unix, not(target_os = "macos")))]
9265            Action::ClipLv2PluginControls { .. } => {}
9266            #[cfg(all(unix, not(target_os = "macos")))]
9267            Action::TrackLv2Midnam { .. } => {}
9268            Action::TrackClapNoteNames { .. } => {}
9269            Action::SessionDiagnosticsReport { .. } => {}
9270            Action::MidiLearnMappingsReport { .. } => {}
9271            Action::HWInfo { .. } => {}
9272            Action::HistoryState { .. } => {}
9273            Action::Undo => {}
9274            Action::Redo => {}
9275            Action::ApplyGroupedActions(_) => {}
9276            _ => {}
9277        }
9278
9279        if let Some(inverse) = inverse_actions {
9280            if let Some(group) = self.history_group.as_mut() {
9281                group.forward_actions.push(action_to_process.clone());
9282                group.inverse_actions.splice(0..0, inverse);
9283            } else {
9284                self.history.record(UndoEntry {
9285                    forward_actions: vec![action_to_process.clone()],
9286                    inverse_actions: inverse,
9287                });
9288            }
9289        }
9290
9291        self.notify_clients(Ok(action_to_process)).await;
9292    }
9293
9294    pub async fn work(&mut self) {
9295        while let Some(message) = self.rx.recv().await {
9296            match message {
9297                Message::Ready(id) => self.push_ready_worker(id),
9298                Message::Finished {
9299                    worker_id,
9300                    task,
9301                    output_linear,
9302                    process_epoch,
9303                    parameter_updates,
9304                } => {
9305                    tracing::debug!(
9306                        "engine received Finished from worker {} for task {:?} (epoch {} vs {})",
9307                        worker_id,
9308                        task,
9309                        process_epoch,
9310                        self.track_process_epoch
9311                    );
9312                    self.push_ready_worker(worker_id);
9313                    let task_key = Self::task_key(&task);
9314                    self.task_processing_started_at.remove(&task_key);
9315                    if process_epoch != self.track_process_epoch {
9316                        if let Some(track) = self
9317                            .state
9318                            .lock()
9319                            .tracks
9320                            .get(&Self::task_track_name(&task))
9321                            .cloned()
9322                        {
9323                            let t = track.lock();
9324                            t.audio.finished = false;
9325                            t.audio.processing = false;
9326                        }
9327                        continue;
9328                    }
9329                    self.cycle_tasks_running
9330                        .retain(|t| Self::task_key(t) != task_key);
9331                    self.cycle_tasks_finished.push(task.clone());
9332                    let track_name = Self::task_track_name(&task);
9333                    let peak = output_linear.iter().copied().fold(0.0_f32, |a, b| a.max(b));
9334                    tracing::info!(
9335                        "Finished task for '{}' epoch={} output_peak={}",
9336                        track_name,
9337                        process_epoch,
9338                        peak
9339                    );
9340                    self.track_meter_linear_by_track
9341                        .insert(track_name.clone(), output_linear);
9342                    for action in parameter_updates {
9343                        self.notify_clients(Ok(action)).await;
9344                    }
9345                    self.force_stalled_task_completions();
9346                    let all_finished = self.send_tasks().await;
9347                    tracing::debug!(
9348                        "engine after Finished for {}: all_finished={}",
9349                        track_name,
9350                        all_finished
9351                    );
9352                    if all_finished {
9353                        self.on_all_tracks_finished().await;
9354                    }
9355                }
9356                Message::Channel(s) => {
9357                    self.clients.push(s);
9358                }
9359
9360                Message::Request(a) => {
9361                    match a {
9362                        Action::TrackOfflineBounceCancel { track_name } => {
9363                            if let Some(job) = self.offline_bounce_jobs.get(&track_name) {
9364                                job.cancel.store(true, Ordering::Relaxed);
9365                            }
9366                        }
9367                        Action::TrackOfflineBounceCancelAll => {
9368                            for job in self.offline_bounce_jobs.values() {
9369                                job.cancel.store(true, Ordering::Relaxed);
9370                            }
9371                        }
9372                        _ if !self.offline_bounce_jobs.is_empty() => {
9373                            self.pending_requests.push_back(a);
9374                        }
9375                        Action::OpenAudioDevice { .. }
9376                        | Action::OpenMidiInputDevice(_)
9377                        | Action::OpenMidiOutputDevice(_)
9378                        | Action::RequestMeterSnapshot
9379                        | Action::Quit
9380                        | Action::Log { .. }
9381                        | Action::Play
9382                        | Action::Pause
9383                        | Action::Stop
9384                        | Action::TransportPosition(_)
9385                        | Action::JumpToEnd
9386                        | Action::SetLoopEnabled(_)
9387                        | Action::SetLoopRange(_)
9388                        | Action::SetPunchEnabled(_)
9389                        | Action::SetPunchRange(_)
9390                        | Action::SetMetronomeEnabled(_)
9391                        | Action::SetTempo(_)
9392                        | Action::SetTimeSignature { .. }
9393                        | Action::SetOscEnabled(_)
9394                        | Action::SetClipPlaybackEnabled(_)
9395                        | Action::SetRecordEnabled(_)
9396                        | Action::SetStepRecording(_)
9397                        | Action::StepRecordMidiNote { .. }
9398                        | Action::SetSessionPath(_)
9399                        | Action::ClearHistory
9400                        | Action::BeginSessionRestore
9401                        | Action::PianoKey { .. }
9402                        | Action::ModifyMidiNotes { .. }
9403                        | Action::ModifyMidiControllers { .. }
9404                        | Action::DeleteMidiControllers { .. }
9405                        | Action::InsertMidiControllers { .. }
9406                        | Action::DeleteMidiNotes { .. }
9407                        | Action::InsertMidiNotes { .. }
9408                        | Action::SetMidiSysExEvents { .. }
9409                        | Action::Session(_) => {
9410                            self.handle_request(a).await;
9411                        }
9412                        #[cfg(all(unix, not(target_os = "macos")))]
9413                        Action::ListLv2Plugins => {
9414                            self.handle_request(a).await;
9415                        }
9416                        Action::ListVst3Plugins => {
9417                            self.handle_request(a).await;
9418                        }
9419                        Action::ListClapPlugins => {
9420                            self.handle_request(a).await;
9421                        }
9422                        Action::ListClapPluginsWithCapabilities => {
9423                            self.handle_request(a).await;
9424                        }
9425                        _ => {
9426                            self.pending_requests.push_back(a);
9427                            if self.can_schedule_hw_cycle() {
9428                                self.request_hw_cycle().await;
9429                            } else {
9430                                while let Some(next) = self.pending_requests.pop_front() {
9431                                    self.handle_request(next).await;
9432                                }
9433                            }
9434                        }
9435                    };
9436                    self.publish_clap_state_dirty().await;
9437                }
9438                Message::OfflineBounceFinished { result } => {
9439                    if let Ok(Action::TrackOfflineBounce { track_name, .. }) = &result {
9440                        self.offline_bounce_jobs.remove(track_name);
9441                    }
9442                    self.notify_clients(result).await;
9443                    if self.offline_bounce_jobs.is_empty() {
9444                        while let Some(next) = self.pending_requests.pop_front() {
9445                            self.handle_request(next).await;
9446                        }
9447                    }
9448                }
9449                Message::HWFinished => {
9450                    if !self.awaiting_hwfinished {
9451                        tracing::debug!("HWFinished ignored (not awaiting)");
9452                        continue;
9453                    }
9454                    tracing::debug!("HWFinished handling; playing={}", self.playing);
9455                    self.handling_hwfinished = true;
9456                    self.awaiting_hwfinished = false;
9457                    #[cfg(unix)]
9458                    {
9459                        if let Some(jack) = &self.jack_runtime {
9460                            if !self.pending_hw_midi_out_events.is_empty() {
9461                                let out_events =
9462                                    std::mem::take(&mut self.pending_hw_midi_out_events);
9463                                jack.lock().write_events(&out_events);
9464                            }
9465                            let mut in_events = vec![];
9466                            jack.lock().read_events_into(&mut in_events);
9467                            if !in_events.is_empty() {
9468                                self.pending_hw_midi_events.extend(in_events);
9469                            }
9470                        }
9471                    }
9472                    #[cfg(unix)]
9473                    if self.jack_runtime.is_some() {
9474                        self.sync_from_jack_transport().await;
9475                    }
9476                    while let Some(a) = self.pending_requests.pop_front() {
9477                        self.handle_request(a).await;
9478                    }
9479                    self.apply_mute_solo_policy();
9480                    self.append_recorded_cycle();
9481                    self.flush_completed_recordings().await;
9482                    let hw_in_routes = self.midi_hw_in_routes.clone();
9483                    let pending_hw_in_by_device = self.pending_hw_midi_events_by_device.clone();
9484                    let mut reconfigured_tracks = Vec::new();
9485                    for (track_name, track) in self.state.lock().tracks.iter() {
9486                        let track_lock = track.lock();
9487                        if self.jack_runtime_is_some() {
9488                            if !self.pending_hw_midi_events.is_empty() {
9489                                track_lock.push_hw_midi_events(&self.pending_hw_midi_events);
9490                            }
9491                        } else {
9492                            for route in hw_in_routes.iter().filter(|r| &r.to_track == track_name) {
9493                                if let Some(events) = pending_hw_in_by_device.get(&route.device) {
9494                                    track_lock.push_hw_midi_events_to_port(route.to_port, events);
9495                                }
9496                            }
9497                        }
9498                        if track_lock.setup() {
9499                            reconfigured_tracks.push(track_name.clone());
9500                        }
9501                    }
9502                    self.publish_track_meters().await;
9503                    self.publish_session_runtime_reports().await;
9504                    self.publish_clap_state_dirty().await;
9505                    for track_name in reconfigured_tracks {
9506                        let track = self.state.lock().tracks.get(&track_name).cloned();
9507                        if let Some(track) = track {
9508                            let (plugins, connections, connectable_connections) = {
9509                                let track_lock = track.lock();
9510                                (
9511                                    track_lock.plugin_graph_plugins(),
9512                                    track_lock.plugin_graph_connections(),
9513                                    track_lock.connectable_connections(),
9514                                )
9515                            };
9516                            self.notify_clients(Ok(Action::TrackPluginGraph {
9517                                track_name: track_name.clone(),
9518                                plugins,
9519                                connections,
9520                                connectable_connections,
9521                            }))
9522                            .await;
9523                        }
9524                    }
9525                    self.pending_hw_midi_events.clear();
9526                    self.pending_hw_midi_events_by_device.clear();
9527                    if self.playing {
9528                        if self.transport_panic_flush_pending {
9529                            self.transport_panic_flush_pending = false;
9530                        } else if self.transport_restart_pending {
9531                            self.transport_restart_pending = false;
9532                        } else {
9533                            let next = self
9534                                .transport_sample
9535                                .saturating_add(self.current_cycle_samples());
9536                            let normalized = self.normalize_transport_sample(next);
9537                            let wrapped = normalized != next;
9538                            self.transport_sample = normalized;
9539                            if wrapped {
9540                                if self.notified_loop_wrap_sample == Some(self.transport_sample) {
9541                                    self.notified_loop_wrap_sample = None;
9542                                } else {
9543                                    self.notify_clients(Ok(Action::TransportPosition(
9544                                        self.transport_sample,
9545                                    )))
9546                                    .await;
9547                                }
9548                            }
9549                        }
9550                    }
9551                    {
9552                        let echoes = self.apply_modulators(self.transport_sample);
9553                        for action in echoes {
9554                            self.notify_clients(Ok(action)).await;
9555                        }
9556                    }
9557                    self.invalidate_track_cycle_state();
9558                    let all_finished = self.send_tasks().await;
9559                    tracing::debug!(
9560                        "HWFinished send_tasks finished={} hw_worker={}",
9561                        all_finished,
9562                        self.hw_worker.is_some()
9563                    );
9564                    if all_finished && self.hw_worker.is_some() {
9565                        self.request_hw_cycle().await;
9566                    }
9567                    #[cfg(unix)]
9568                    {
9569                        if self.jack_runtime.is_some() {
9570                            self.awaiting_hwfinished = true;
9571                        }
9572                    }
9573                    self.handling_hwfinished = false;
9574                }
9575                Message::HWMidiEvents(events) => {
9576                    for hw_event in events {
9577                        let thru_targets: Vec<String> = self
9578                            .midi_hw_thru_routes
9579                            .iter()
9580                            .filter(|route| route.from_device == hw_event.device)
9581                            .map(|route| route.to_device.clone())
9582                            .collect();
9583                        for device in thru_targets {
9584                            self.pending_hw_midi_out_events_by_device.push(HwMidiEvent {
9585                                device,
9586                                event: hw_event.event.clone(),
9587                            });
9588                        }
9589                        if hw_event.event.data.len() >= 3 {
9590                            let status = hw_event.event.data[0];
9591                            if status & 0xF0 == 0xB0 {
9592                                let channel = status & 0x0F;
9593                                let cc = hw_event.event.data[1];
9594                                let value = hw_event.event.data[2];
9595                                self.handle_incoming_hw_cc(&hw_event.device, channel, cc, value)
9596                                    .await;
9597                            }
9598                            if self.step_recording_enabled && status & 0xF0 == 0x90 {
9599                                let channel = status & 0x0F;
9600                                let pitch = hw_event.event.data[1];
9601                                let velocity = hw_event.event.data[2];
9602                                if velocity > 0 {
9603                                    self.notify_clients(Ok(Action::StepRecordMidiNote {
9604                                        device: hw_event.device.clone(),
9605                                        channel,
9606                                        pitch,
9607                                        velocity,
9608                                    }))
9609                                    .await;
9610                                }
9611                            }
9612                        }
9613                        self.pending_hw_midi_events_by_device
9614                            .entry(hw_event.device)
9615                            .or_default()
9616                            .push(hw_event.event);
9617                    }
9618                }
9619                _ => {}
9620            }
9621        }
9622    }
9623
9624    fn collect_hw_midi_output_events(&self) -> Vec<MidiEvent> {
9625        let mut events = vec![];
9626        for track in self.state.lock().tracks.values() {
9627            events.extend(
9628                track
9629                    .lock()
9630                    .take_hw_midi_out_events()
9631                    .into_iter()
9632                    .map(|evt| evt.event),
9633            );
9634        }
9635        events.sort_by_key(|a| a.frame);
9636        events
9637    }
9638
9639    fn collect_hw_midi_output_events_by_device(&mut self) -> Vec<HwMidiEvent> {
9640        let mut events = Vec::<HwMidiEvent>::new();
9641        let routes = self.midi_hw_out_routes.clone();
9642        let mut events_by_track = HashMap::<String, Vec<crate::track::HwMidiOutEvent>>::new();
9643        {
9644            let state = self.state.lock();
9645            for route in &routes {
9646                if events_by_track.contains_key(&route.from_track) {
9647                    continue;
9648                }
9649                let Some(track) = state.tracks.get(&route.from_track) else {
9650                    continue;
9651                };
9652                events_by_track.insert(
9653                    route.from_track.clone(),
9654                    track.lock().take_hw_midi_out_events(),
9655                );
9656            }
9657        }
9658
9659        for route in routes {
9660            let Some(track_events) = events_by_track.get(&route.from_track) else {
9661                continue;
9662            };
9663            for hw_event in track_events
9664                .iter()
9665                .filter(|evt| evt.port == route.from_port)
9666            {
9667                self.update_active_hw_notes_for_track(
9668                    &route.from_track,
9669                    &route.device,
9670                    &hw_event.event.data,
9671                );
9672                events.push(HwMidiEvent {
9673                    device: route.device.clone(),
9674                    event: hw_event.event.clone(),
9675                });
9676            }
9677        }
9678        events.sort_by(|a, b| {
9679            a.event
9680                .frame
9681                .cmp(&b.event.frame)
9682                .then_with(|| a.device.cmp(&b.device))
9683        });
9684        events
9685    }
9686}
9687
9688#[cfg(test)]
9689mod tests {
9690    use super::*;
9691    use crate::mutex::UnsafeMutex;
9692    use tokio::sync::mpsc::channel;
9693    use tokio::time::{Duration as TokioDuration, timeout};
9694
9695    #[test]
9696    #[cfg(unix)]
9697    fn jack_transport_sync_decision_starts_and_syncs_position_on_external_play() {
9698        let decision = Engine::jack_transport_sync_decision(false, 128, true, 256, 64);
9699
9700        assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Start));
9701        assert_eq!(decision.position_sync, Some(256));
9702    }
9703
9704    #[test]
9705    #[cfg(unix)]
9706    fn jack_transport_sync_decision_stops_and_syncs_position_on_external_stop() {
9707        let decision = Engine::jack_transport_sync_decision(true, 512, false, 96, 64);
9708
9709        assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Stop));
9710        assert_eq!(decision.position_sync, Some(96));
9711    }
9712
9713    #[test]
9714    #[cfg(unix)]
9715    fn jack_transport_sync_decision_ignores_small_rolling_drift() {
9716        let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1040, 64);
9717
9718        assert_eq!(decision.play_sync, None);
9719        assert_eq!(decision.position_sync, None);
9720    }
9721
9722    #[test]
9723    #[cfg(unix)]
9724    fn jack_transport_sync_decision_syncs_large_rolling_jump() {
9725        let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1200, 64);
9726
9727        assert_eq!(decision.play_sync, None);
9728        assert_eq!(decision.position_sync, Some(1200));
9729    }
9730
9731    #[test]
9732    #[cfg(unix)]
9733    fn jack_transport_sync_decision_syncs_locate_while_stopped() {
9734        let decision = Engine::jack_transport_sync_decision(false, 400, false, 900, 64);
9735
9736        assert_eq!(decision.play_sync, None);
9737        assert_eq!(decision.position_sync, Some(900));
9738    }
9739
9740    fn make_engine_with_client() -> (Engine, tokio::sync::mpsc::Receiver<Message>) {
9741        let (engine_tx, engine_rx) = channel(16);
9742        let mut engine = Engine::new(engine_rx, engine_tx);
9743        let (client_tx, client_rx) = channel(16);
9744        engine.clients.push(client_tx);
9745        (engine, client_rx)
9746    }
9747
9748    fn insert_track(engine: &mut Engine, track: Track) {
9749        engine.state.lock().tracks.insert(
9750            track.name.clone(),
9751            Arc::new(UnsafeMutex::new(Box::new(track))),
9752        );
9753    }
9754
9755    fn osc_packet(address: &str) -> Vec<u8> {
9756        fn push_padded_osc_string(packet: &mut Vec<u8>, value: &str) {
9757            packet.extend_from_slice(value.as_bytes());
9758            packet.push(0);
9759            while !packet.len().is_multiple_of(4) {
9760                packet.push(0);
9761            }
9762        }
9763
9764        let mut packet = Vec::new();
9765        push_padded_osc_string(&mut packet, address);
9766        push_padded_osc_string(&mut packet, ",");
9767        packet
9768    }
9769
9770    #[tokio::test]
9771    async fn set_osc_enabled_starts_and_stops_server() {
9772        let (mut engine, _client_rx) = make_engine_with_client();
9773
9774        engine
9775            .set_osc_enabled_with(true, |tx| OscServer::start_on_addr(tx, "127.0.0.1:0"))
9776            .expect("start osc server on ephemeral port");
9777        assert!(engine.osc_server.is_some());
9778
9779        engine
9780            .set_osc_enabled_with(false, OscServer::start)
9781            .expect("stop osc server");
9782        assert!(engine.osc_server.is_none());
9783    }
9784
9785    #[tokio::test]
9786    async fn osc_server_forwards_transport_packets_to_engine_channel() {
9787        let (tx, mut rx) = channel(4);
9788        let mut server =
9789            OscServer::start_on_addr(tx, "127.0.0.1:0").expect("start osc test server");
9790        let socket = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind sender socket");
9791        let packet = osc_packet("/transport/play");
9792        socket
9793            .send_to(&packet, server.listen_addr())
9794            .expect("send osc packet");
9795
9796        let message = timeout(TokioDuration::from_secs(1), rx.recv())
9797            .await
9798            .expect("packet delivery timeout")
9799            .expect("osc message");
9800        match message {
9801            Message::Request(Action::Play) => {}
9802            other => panic!("unexpected osc message: {other:?}"),
9803        }
9804
9805        server.stop();
9806    }
9807
9808    #[tokio::test]
9809    async fn track_offline_bounce_rejects_zero_length_requests() {
9810        let (mut engine, mut client_rx) = make_engine_with_client();
9811        insert_track(
9812            &mut engine,
9813            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
9814        );
9815
9816        engine
9817            .handle_request(Action::TrackOfflineBounce {
9818                track_name: "track".to_string(),
9819                output_path: "/tmp/out.wav".to_string(),
9820                start_sample: 0,
9821                length_samples: 0,
9822                automation_lanes: vec![],
9823                apply_fader: false,
9824            })
9825            .await;
9826
9827        match client_rx.recv().await.expect("response") {
9828            Message::Response(Err(err)) => {
9829                assert!(err.contains("has no renderable content for offline bounce"));
9830            }
9831            other => panic!("unexpected message: {other:?}"),
9832        }
9833    }
9834
9835    #[tokio::test]
9836    async fn track_offline_bounce_rejects_when_same_track_is_active() {
9837        let (mut engine, mut client_rx) = make_engine_with_client();
9838        engine.offline_bounce_jobs.insert(
9839            "other".to_string(),
9840            OfflineBounceJob {
9841                cancel: Arc::new(AtomicBool::new(false)),
9842            },
9843        );
9844
9845        engine
9846            .handle_request(Action::TrackOfflineBounce {
9847                track_name: "other".to_string(),
9848                output_path: "/tmp/out.wav".to_string(),
9849                start_sample: 0,
9850                length_samples: 128,
9851                automation_lanes: vec![],
9852                apply_fader: false,
9853            })
9854            .await;
9855
9856        match client_rx.recv().await.expect("response") {
9857            Message::Response(Err(err)) => {
9858                assert!(err.contains("already in progress"));
9859            }
9860            other => panic!("unexpected message: {other:?}"),
9861        }
9862    }
9863
9864    #[tokio::test]
9865    async fn track_offline_bounce_allows_different_track_concurrently() {
9866        let (mut engine, _client_rx) = make_engine_with_client();
9867        insert_track(
9868            &mut engine,
9869            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
9870        );
9871        engine.offline_bounce_jobs.insert(
9872            "other".to_string(),
9873            OfflineBounceJob {
9874                cancel: Arc::new(AtomicBool::new(false)),
9875            },
9876        );
9877
9878        engine
9879            .handle_request(Action::TrackOfflineBounce {
9880                track_name: "track".to_string(),
9881                output_path: "/tmp/out.wav".to_string(),
9882                start_sample: 0,
9883                length_samples: 128,
9884                automation_lanes: vec![],
9885                apply_fader: false,
9886            })
9887            .await;
9888
9889        assert!(engine.offline_bounce_jobs.contains_key("other"));
9890        assert_eq!(engine.pending_requests.len(), 1);
9891    }
9892
9893    #[tokio::test]
9894    async fn reject_if_track_frozen_sends_error_and_blocks_operation() {
9895        let (mut engine, mut client_rx) = make_engine_with_client();
9896        let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
9897        track.set_frozen(true);
9898        insert_track(&mut engine, track);
9899
9900        let rejected = engine
9901            .reject_if_track_frozen("track", "arming/disarming")
9902            .await;
9903
9904        assert!(rejected);
9905        match client_rx.recv().await.expect("response") {
9906            Message::Response(Err(err)) => {
9907                assert_eq!(err, "Track 'track' is frozen; arming/disarming is blocked");
9908            }
9909            other => panic!("unexpected message: {other:?}"),
9910        }
9911    }
9912
9913    #[tokio::test]
9914    async fn undo_restores_original_clip_bounds_after_stretch_style_group() {
9915        let (mut engine, _client_rx) = make_engine_with_client();
9916        let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
9917        let mut clip = AudioClip::new("audio/original.wav".to_string(), 100, 220);
9918        clip.offset = 12;
9919        clip.fade_in_samples = 20;
9920        clip.fade_out_samples = 30;
9921        track.audio.clips.push(clip);
9922        insert_track(&mut engine, track);
9923
9924        engine.handle_request(Action::BeginHistoryGroup).await;
9925        engine
9926            .handle_request(Action::SetClipBounds {
9927                track_name: "track".to_string(),
9928                clip_index: 0,
9929                kind: Kind::Audio,
9930                start: 120,
9931                length: 180,
9932                offset: 0,
9933            })
9934            .await;
9935        engine
9936            .handle_request(Action::SetClipSourceName {
9937                track_name: "track".to_string(),
9938                clip_index: 0,
9939                kind: Kind::Audio,
9940                name: "audio/stretched.wav".to_string(),
9941            })
9942            .await;
9943        engine
9944            .handle_request(Action::SetClipFade {
9945                track_name: "track".to_string(),
9946                clip_index: 0,
9947                kind: Kind::Audio,
9948                fade_enabled: true,
9949                fade_in_samples: 12,
9950                fade_out_samples: 12,
9951            })
9952            .await;
9953        engine.handle_request(Action::EndHistoryGroup).await;
9954
9955        engine.handle_request(Action::Undo).await;
9956
9957        let state = engine.state.lock();
9958        let track = state.tracks.get("track").expect("track exists").lock();
9959        let clip = track.audio.clips.first().expect("clip exists");
9960        assert_eq!(clip.name, "audio/original.wav");
9961        assert_eq!(clip.start, 100);
9962        assert_eq!(clip.end, 220);
9963        assert_eq!(clip.end.saturating_sub(clip.start), 120);
9964        assert_eq!(clip.offset, 12);
9965    }
9966
9967    #[tokio::test]
9968    async fn track_offline_bounce_queues_when_no_worker_is_ready() {
9969        let (mut engine, _client_rx) = make_engine_with_client();
9970        insert_track(
9971            &mut engine,
9972            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
9973        );
9974
9975        engine
9976            .handle_request(Action::TrackOfflineBounce {
9977                track_name: "track".to_string(),
9978                output_path: "/tmp/out.wav".to_string(),
9979                start_sample: 0,
9980                length_samples: 128,
9981                automation_lanes: vec![],
9982                apply_fader: false,
9983            })
9984            .await;
9985
9986        assert!(engine.offline_bounce_jobs.is_empty());
9987        assert_eq!(engine.pending_requests.len(), 1);
9988        assert!(matches!(
9989            engine.pending_requests.front(),
9990            Some(Action::TrackOfflineBounce { track_name, length_samples, .. })
9991                if track_name == "track" && *length_samples == 128
9992        ));
9993    }
9994
9995    #[tokio::test]
9996    async fn track_offline_bounce_returns_missing_track_error() {
9997        let (mut engine, mut client_rx) = make_engine_with_client();
9998
9999        engine
10000            .handle_request(Action::TrackOfflineBounce {
10001                track_name: "missing".to_string(),
10002                output_path: "/tmp/out.wav".to_string(),
10003                start_sample: 0,
10004                length_samples: 128,
10005                automation_lanes: vec![],
10006                apply_fader: false,
10007            })
10008            .await;
10009
10010        match client_rx.recv().await.expect("response") {
10011            Message::Response(Err(err)) => {
10012                assert_eq!(err, "Track not found: missing");
10013            }
10014            other => panic!("unexpected message: {other:?}"),
10015        }
10016    }
10017
10018    #[tokio::test]
10019    async fn track_offline_bounce_clears_job_when_worker_send_fails() {
10020        let (mut engine, mut client_rx) = make_engine_with_client();
10021        insert_track(
10022            &mut engine,
10023            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
10024        );
10025        let (worker_tx, worker_rx) = channel(1);
10026        drop(worker_rx);
10027        engine
10028            .workers
10029            .push(WorkerData::new(worker_tx, tokio::spawn(async {})));
10030        engine.ready_workers.push(0);
10031
10032        engine
10033            .handle_request(Action::TrackOfflineBounce {
10034                track_name: "track".to_string(),
10035                output_path: "/tmp/out.wav".to_string(),
10036                start_sample: 0,
10037                length_samples: 128,
10038                automation_lanes: vec![],
10039                apply_fader: false,
10040            })
10041            .await;
10042
10043        assert!(engine.offline_bounce_jobs.is_empty());
10044        match client_rx.recv().await.expect("response") {
10045            Message::Response(Err(err)) => {
10046                assert!(err.contains("Failed to schedule offline bounce"));
10047            }
10048            other => panic!("unexpected message: {other:?}"),
10049        }
10050    }
10051
10052    #[tokio::test]
10053    async fn play_stop_play_keeps_clip_output_audible() {
10054        use crate::audio::clip::AudioClip;
10055        use crate::audio_codec::write_wav_f32;
10056
10057        let (engine_tx, engine_rx) = channel(16);
10058        let mut engine = Engine::new(engine_rx, engine_tx);
10059        let state = engine.state();
10060        let (client_tx, mut client_rx) = channel(16);
10061        engine.clients.push(client_tx);
10062        engine.init().await;
10063
10064        let tmp_dir = std::env::temp_dir().join("maolan_play_stop_play_test");
10065        let _ = std::fs::create_dir_all(&tmp_dir);
10066        let wav_path = tmp_dir.join("tone.wav");
10067        let sample_rate = 48_000u32;
10068        let clip_samples = sample_rate as usize;
10069        let mut samples = Vec::with_capacity(clip_samples);
10070        for i in 0..clip_samples {
10071            let phase = i as f32 / sample_rate as f32 * 2.0 * std::f32::consts::PI * 440.0;
10072            samples.push(phase.sin() * 0.5);
10073        }
10074        write_wav_f32(&wav_path, &samples, 1, sample_rate).expect("write wav");
10075
10076        let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 1024, sample_rate as f64);
10077        let mut clip = AudioClip::new(wav_path.to_string_lossy().to_string(), 0, clip_samples);
10078        clip.fade_enabled = false;
10079        track.audio.clips.push(clip);
10080        track.session_base_dir = Some(tmp_dir.clone());
10081        insert_track(&mut engine, track);
10082
10083        let tx = engine.tx.clone();
10084        let work_handle = tokio::spawn(async move {
10085            engine.work().await;
10086        });
10087
10088        // Wait for worker tasks to start up and send Ready messages.
10089        tokio::time::sleep(TokioDuration::from_millis(100)).await;
10090
10091        async fn drain_responses(
10092            client_rx: &mut tokio::sync::mpsc::Receiver<Message>,
10093            count: usize,
10094        ) {
10095            for _ in 0..count {
10096                let _ = tokio::time::timeout(TokioDuration::from_secs(2), client_rx.recv()).await;
10097            }
10098        }
10099
10100        async fn wait_for_track_processed(
10101            client_rx: &mut tokio::sync::mpsc::Receiver<Message>,
10102            state: &Arc<UnsafeMutex<State>>,
10103        ) -> bool {
10104            let deadline = Instant::now() + Duration::from_secs(5);
10105            while Instant::now() < deadline {
10106                let msg =
10107                    tokio::time::timeout(TokioDuration::from_millis(100), client_rx.recv()).await;
10108                if let Ok(Some(Message::Response(Ok(Action::TransportPosition(_)))))
10109                | Ok(Some(Message::Response(Ok(Action::Play)))) = msg
10110                {
10111                    let track_deadline = Instant::now() + Duration::from_secs(5);
10112                    while Instant::now() < track_deadline {
10113                        if state
10114                            .lock()
10115                            .tracks
10116                            .get("track")
10117                            .map(|t| t.lock().audio.finished)
10118                            .unwrap_or(false)
10119                        {
10120                            return true;
10121                        }
10122                        tokio::time::sleep(TokioDuration::from_millis(10)).await;
10123                    }
10124                }
10125            }
10126            false
10127        }
10128
10129        tx.send(Message::Request(Action::SetClipPlaybackEnabled(true)))
10130            .await
10131            .unwrap();
10132        tx.send(Message::Request(Action::Play)).await.unwrap();
10133        assert!(
10134            wait_for_track_processed(&mut client_rx, &state).await,
10135            "track did not process on first play"
10136        );
10137        let first_peak = {
10138            let state = state.lock();
10139            let track = state.tracks.get("track").expect("track").lock();
10140            let input = track.audio.ins[0].buffer.lock();
10141            crate::simd::peak_abs(input)
10142        };
10143        assert!(
10144            first_peak > 0.001,
10145            "expected audible input on first play, got {first_peak}"
10146        );
10147
10148        tx.send(Message::Request(Action::SetClipPlaybackEnabled(true)))
10149            .await
10150            .unwrap();
10151        tx.send(Message::Request(Action::Stop)).await.unwrap();
10152        drain_responses(&mut client_rx, 2).await;
10153
10154        tx.send(Message::Request(Action::SetClipPlaybackEnabled(true)))
10155            .await
10156            .unwrap();
10157        tx.send(Message::Request(Action::Play)).await.unwrap();
10158        assert!(
10159            wait_for_track_processed(&mut client_rx, &state).await,
10160            "track did not process on second play"
10161        );
10162        let second_peak = {
10163            let state = state.lock();
10164            let track = state.tracks.get("track").expect("track").lock();
10165            let input = track.audio.ins[0].buffer.lock();
10166            crate::simd::peak_abs(input)
10167        };
10168        assert!(
10169            second_peak > 0.001,
10170            "expected audible input on second play after stop, got {second_peak}"
10171        );
10172
10173        let _ = tx.send(Message::Request(Action::Quit)).await;
10174        tokio::time::sleep(TokioDuration::from_millis(200)).await;
10175        work_handle.abort();
10176        let _ = std::fs::remove_dir_all(&tmp_dir);
10177    }
10178
10179    #[test]
10180    fn modulator_sets_track_volume() {
10181        let (mut engine, _client_rx) = make_engine_with_client();
10182        let track = Track::new("vol-track".to_string(), 0, 2, 0, 0, 128, 48_000.0);
10183        insert_track(&mut engine, track);
10184
10185        engine.modulators = vec![crate::modulator::Modulator {
10186            id: 1,
10187            name: "LFO".to_string(),
10188            shape: crate::modulator::ModulatorShape::Sine,
10189            rate_hz: 1.0,
10190            phase: 0.0,
10191            enabled: true,
10192            targets: vec![crate::modulator::ModulatorTarget::TrackVolume {
10193                track_name: "vol-track".to_string(),
10194                min: -90.0,
10195                max: 20.0,
10196            }],
10197        }];
10198
10199        // At sample 12000 (1/4 period at 48kHz/1Hz), sine value maps to 1.0 -> max 20 dB.
10200        let echoes = engine.apply_modulators(12_000);
10201        let track = engine.state.lock().tracks["vol-track"].lock();
10202        assert!(
10203            (track.level() - 20.0).abs() < 0.01,
10204            "expected 20 dB, got {}",
10205            track.level()
10206        );
10207        assert!(
10208            echoes
10209                .iter()
10210                .any(|a| matches!(a, Action::TrackAutomationLevel(name, _) if name == "vol-track"))
10211        );
10212    }
10213
10214    #[test]
10215    fn modulator_sets_track_balance() {
10216        let (mut engine, _client_rx) = make_engine_with_client();
10217        let track = Track::new("pan-track".to_string(), 0, 2, 0, 0, 128, 48_000.0);
10218        insert_track(&mut engine, track);
10219
10220        engine.modulators = vec![crate::modulator::Modulator {
10221            id: 1,
10222            name: "LFO".to_string(),
10223            shape: crate::modulator::ModulatorShape::Sine,
10224            rate_hz: 1.0,
10225            phase: 0.0,
10226            enabled: true,
10227            targets: vec![crate::modulator::ModulatorTarget::TrackBalance {
10228                track_name: "pan-track".to_string(),
10229                min: -1.0,
10230                max: 1.0,
10231            }],
10232        }];
10233
10234        // At sample 12000 (1/4 period), sine value maps to 1.0 -> max balance 1.0.
10235        let echoes = engine.apply_modulators(12_000);
10236        let track = engine.state.lock().tracks["pan-track"].lock();
10237        assert!(
10238            (track.balance - 1.0).abs() < 0.01,
10239            "expected balance 1.0, got {}",
10240            track.balance
10241        );
10242        assert!(
10243            echoes.iter().any(
10244                |a| matches!(a, Action::TrackAutomationBalance(name, _) if name == "pan-track")
10245            )
10246        );
10247    }
10248
10249    #[tokio::test]
10250    async fn track_set_parent_wires_folder_input_to_child_input_and_child_output_to_folder_output()
10251    {
10252        let (mut engine, mut client_rx) = make_engine_with_client();
10253        let folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
10254        let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
10255        insert_track(&mut engine, folder);
10256        insert_track(&mut engine, child);
10257
10258        engine
10259            .handle_request_inner(
10260                Action::TrackSetParent {
10261                    track_name: "child".to_string(),
10262                    parent_name: Some("folder".to_string()),
10263                },
10264                false,
10265            )
10266            .await;
10267
10268        // Drain client messages so the channel does not block later drops.
10269        while let Ok(Some(_)) =
10270            tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
10271        {}
10272
10273        let state = engine.state.lock();
10274        let folder = state.tracks.get("folder").unwrap().lock();
10275        let child = state.tracks.get("child").unwrap().lock();
10276
10277        assert!(folder.child_tracks.iter().any(|c| c.lock().name == "child"));
10278        assert_eq!(child.parent_track.as_deref(), Some("folder"));
10279
10280        // Folder input -> child input.
10281        for (i, (parent_in, child_in)) in folder.audio.ins.iter().zip(&child.audio.ins).enumerate()
10282        {
10283            assert!(
10284                child_in
10285                    .connections
10286                    .lock()
10287                    .iter()
10288                    .any(|c| Arc::ptr_eq(c, parent_in)),
10289                "folder input {i} is not routed to child input {i}"
10290            );
10291            assert!(
10292                !parent_in
10293                    .connections
10294                    .lock()
10295                    .iter()
10296                    .any(|c| Arc::ptr_eq(c, child_in)),
10297                "folder input {i} should not read from child input {i}"
10298            );
10299        }
10300
10301        // Child output -> folder output.
10302        for (i, (child_out, parent_out)) in
10303            child.audio.outs.iter().zip(&folder.audio.outs).enumerate()
10304        {
10305            assert!(
10306                parent_out
10307                    .connections
10308                    .lock()
10309                    .iter()
10310                    .any(|c| Arc::ptr_eq(c, child_out)),
10311                "child output {i} is not routed to folder output {i}"
10312            );
10313        }
10314
10315        // Child passthrough is restored so audio can flow through.
10316        for (i, child_out) in child.audio.outs.iter().enumerate() {
10317            assert!(
10318                child_out.connections.lock().iter().any(|c| {
10319                    child
10320                        .audio
10321                        .ins
10322                        .get(i)
10323                        .is_some_and(|inp| Arc::ptr_eq(c, inp))
10324                }),
10325                "child output {i} is not connected to child input {i}"
10326            );
10327        }
10328    }
10329
10330    #[tokio::test]
10331    async fn track_set_parent_to_none_restores_root_passthrough() {
10332        let (mut engine, mut client_rx) = make_engine_with_client();
10333        let folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
10334        let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
10335        insert_track(&mut engine, folder);
10336        insert_track(&mut engine, child);
10337
10338        engine
10339            .handle_request_inner(
10340                Action::TrackSetParent {
10341                    track_name: "child".to_string(),
10342                    parent_name: Some("folder".to_string()),
10343                },
10344                false,
10345            )
10346            .await;
10347        engine
10348            .handle_request_inner(
10349                Action::TrackSetParent {
10350                    track_name: "child".to_string(),
10351                    parent_name: None,
10352                },
10353                false,
10354            )
10355            .await;
10356
10357        while let Ok(Some(_)) =
10358            tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
10359        {}
10360
10361        let state = engine.state.lock();
10362        let folder = state.tracks.get("folder").unwrap().lock();
10363        let child = state.tracks.get("child").unwrap().lock();
10364
10365        assert!(folder.child_tracks.is_empty());
10366        assert!(child.parent_track.is_none());
10367
10368        for (i, child_out) in child.audio.outs.iter().enumerate() {
10369            assert!(
10370                child_out.connections.lock().iter().any(|c| {
10371                    child
10372                        .audio
10373                        .ins
10374                        .get(i)
10375                        .is_some_and(|inp| Arc::ptr_eq(c, inp))
10376                }),
10377                "child output {i} should be connected to child input {i} after moving to root"
10378            );
10379        }
10380    }
10381
10382    #[tokio::test]
10383    async fn track_set_parent_wires_folder_midi_to_child_midi() {
10384        let (mut engine, mut client_rx) = make_engine_with_client();
10385        let folder = Track::new_folder("folder".to_string(), 0, 0, 1, 1, 64, 48_000.0);
10386        let child = Track::new("child".to_string(), 0, 0, 1, 1, 64, 48_000.0);
10387        insert_track(&mut engine, folder);
10388        insert_track(&mut engine, child);
10389
10390        engine
10391            .handle_request_inner(
10392                Action::TrackSetParent {
10393                    track_name: "child".to_string(),
10394                    parent_name: Some("folder".to_string()),
10395                },
10396                false,
10397            )
10398            .await;
10399
10400        while let Ok(Some(_)) =
10401            tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
10402        {}
10403
10404        let state = engine.state.lock();
10405        let folder = state.tracks.get("folder").unwrap().lock();
10406        let child = state.tracks.get("child").unwrap().lock();
10407
10408        let folder_midi_in = &folder.midi.ins[0];
10409        let child_midi_in = &child.midi.ins[0];
10410        assert!(
10411            child_midi_in
10412                .lock()
10413                .connections
10414                .iter()
10415                .any(|c| Arc::ptr_eq(c, folder_midi_in)),
10416            "folder MIDI input should be routed to child MIDI input"
10417        );
10418
10419        let child_midi_out = &child.midi.outs[0];
10420        let folder_midi_out = &folder.midi.outs[0];
10421        assert!(
10422            child_midi_out
10423                .lock()
10424                .connections
10425                .iter()
10426                .any(|c| Arc::ptr_eq(c, folder_midi_out)),
10427            "child MIDI output should be routed to folder MIDI output"
10428        );
10429    }
10430
10431    #[test]
10432    fn nested_folder_expands_in_task_graph() {
10433        let (mut engine, _client_rx) = make_engine_with_client();
10434        let outer = Track::new_folder("outer".to_string(), 2, 2, 0, 0, 64, 48_000.0);
10435        let inner = Track::new_folder("inner".to_string(), 2, 2, 0, 0, 64, 48_000.0);
10436        let leaf = Track::new("leaf".to_string(), 2, 2, 0, 0, 64, 48_000.0);
10437        insert_track(&mut engine, outer);
10438        insert_track(&mut engine, inner);
10439        insert_track(&mut engine, leaf);
10440
10441        {
10442            let state = engine.state.lock();
10443            let outer = state.tracks.get("outer").unwrap().clone();
10444            let inner = state.tracks.get("inner").unwrap().clone();
10445            let leaf = state.tracks.get("leaf").unwrap().clone();
10446            outer.lock().child_tracks.push(inner.clone());
10447            inner.lock().child_tracks.push(leaf.clone());
10448            inner.lock().parent_track = Some("outer".to_string());
10449            leaf.lock().parent_track = Some("inner".to_string());
10450        }
10451
10452        let (tasks, deps) = engine.build_task_graph();
10453        let names: Vec<String> = tasks
10454            .iter()
10455            .map(|t| match t {
10456                ProcessTask::Track(t) => format!("track:{}", t.lock().name.clone()),
10457                ProcessTask::FolderInput(t) => format!("in:{}", t.lock().name.clone()),
10458                ProcessTask::FolderOutput(t) => format!("out:{}", t.lock().name.clone()),
10459                ProcessTask::Plugin { track, .. } => {
10460                    format!("plugin:{}", track.lock().name.clone())
10461                }
10462            })
10463            .collect();
10464
10465        let expected = vec![
10466            "in:outer",
10467            "in:inner",
10468            "track:leaf",
10469            "out:inner",
10470            "out:outer",
10471        ];
10472        assert_eq!(names, expected, "task graph should expand nested folders");
10473
10474        // Each task should depend on the previous one.
10475        for window in tasks.windows(2) {
10476            let prev = &window[0];
10477            let next = &window[1];
10478            let prev_key = Engine::task_key(prev);
10479            let next_key = Engine::task_key(next);
10480            assert!(
10481                deps.get(&next_key).is_some_and(|d| d.contains(&prev_key)),
10482                "{:?} should depend on {:?}",
10483                next,
10484                prev
10485            );
10486        }
10487    }
10488
10489    #[test]
10490    fn child_to_plugin_to_folder_output_task_graph_has_no_cycle() {
10491        use crate::message::ConnectableRef;
10492
10493        let plugin_path = Path::new(env!("CARGO_MANIFEST_DIR"))
10494            .parent()
10495            .unwrap()
10496            .join("daw")
10497            .join("plugin-host")
10498            .join("tests")
10499            .join("test_passthrough.clap");
10500        if !plugin_path.exists() {
10501            return;
10502        }
10503        if crate::plugins::ipc::find_plugin_host_binary().is_none() {
10504            return;
10505        }
10506
10507        let (mut engine, _client_rx) = make_engine_with_client();
10508        let mut folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
10509        let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
10510
10511        folder
10512            .load_clap_plugin(
10513                &format!("{}::com.maolan.test.passthrough", plugin_path.display()),
10514                None,
10515            )
10516            .expect("should load CLAP plugin on folder");
10517        folder.clap_plugins[0].processor.lock().setup_audio_ports();
10518        let plugin_id = folder.clap_plugins[0].id;
10519
10520        insert_track(&mut engine, folder);
10521        insert_track(&mut engine, child);
10522
10523        {
10524            let state = engine.state.lock();
10525            let folder = state.tracks.get("folder").unwrap().clone();
10526            let child = state.tracks.get("child").unwrap().clone();
10527            folder.lock().child_tracks.push(child.clone());
10528            child.lock().parent_track = Some("folder".to_string());
10529
10530            folder
10531                .lock()
10532                .connect_audio_connectable(
10533                    ConnectableRef::ChildTrack("child".to_string()),
10534                    0,
10535                    ConnectableRef::ClapPlugin(plugin_id),
10536                    0,
10537                )
10538                .expect("connect child L to plugin L");
10539            folder
10540                .lock()
10541                .connect_audio_connectable(
10542                    ConnectableRef::ChildTrack("child".to_string()),
10543                    1,
10544                    ConnectableRef::ClapPlugin(plugin_id),
10545                    1,
10546                )
10547                .expect("connect child R to plugin R");
10548            folder
10549                .lock()
10550                .connect_audio_connectable(
10551                    ConnectableRef::ClapPlugin(plugin_id),
10552                    0,
10553                    ConnectableRef::TrackOutput,
10554                    0,
10555                )
10556                .expect("connect plugin L to folder output L");
10557            folder
10558                .lock()
10559                .connect_audio_connectable(
10560                    ConnectableRef::ClapPlugin(plugin_id),
10561                    1,
10562                    ConnectableRef::TrackOutput,
10563                    1,
10564                )
10565                .expect("connect plugin R to folder output R");
10566        }
10567
10568        let (tasks, deps) = engine.build_task_graph();
10569
10570        let folder_in_key = tasks
10571            .iter()
10572            .find(|t| matches!(t, ProcessTask::FolderInput(t) if t.lock().name == "folder"))
10573            .map(Engine::task_key)
10574            .expect("folder input task");
10575        let child_key = tasks
10576            .iter()
10577            .find(|t| matches!(t, ProcessTask::Track(t) if t.lock().name == "child"))
10578            .map(Engine::task_key)
10579            .expect("child task");
10580        let plugin_key = tasks
10581            .iter()
10582            .find(|t| {
10583                matches!(
10584                    t,
10585                    ProcessTask::Plugin {
10586                        track,
10587                        kind: PluginKind::Clap,
10588                        index: 0,
10589                    } if track.lock().name == "folder"
10590                )
10591            })
10592            .map(Engine::task_key)
10593            .expect("plugin task");
10594        let folder_out_key = tasks
10595            .iter()
10596            .find(|t| matches!(t, ProcessTask::FolderOutput(t) if t.lock().name == "folder"))
10597            .map(Engine::task_key)
10598            .expect("folder output task");
10599
10600        assert!(
10601            deps.get(&child_key)
10602                .is_some_and(|d| d.contains(&folder_in_key)),
10603            "child task should depend on folder input"
10604        );
10605        assert!(
10606            deps.get(&plugin_key)
10607                .is_some_and(|d| d.contains(&folder_in_key) && d.contains(&child_key)),
10608            "plugin task should depend on folder input and child"
10609        );
10610        assert!(
10611            deps.get(&folder_out_key).is_some_and(|d| {
10612                d.contains(&folder_in_key) && d.contains(&plugin_key) && d.contains(&child_key)
10613            }),
10614            "folder output should depend on folder input, plugin, and child"
10615        );
10616
10617        fn has_cycle(deps: &HashMap<String, Vec<String>>) -> bool {
10618            let mut state: HashMap<String, u8> = HashMap::new();
10619            fn visit(
10620                node: &str,
10621                deps: &HashMap<String, Vec<String>>,
10622                state: &mut HashMap<String, u8>,
10623            ) -> bool {
10624                match state.get(node).copied() {
10625                    Some(1) => return true,
10626                    Some(2) => return false,
10627                    _ => {}
10628                }
10629                state.insert(node.to_string(), 1);
10630                for next in deps.get(node).into_iter().flatten() {
10631                    if visit(next, deps, state) {
10632                        return true;
10633                    }
10634                }
10635                state.insert(node.to_string(), 2);
10636                false
10637            }
10638            for node in deps.keys() {
10639                if visit(node, deps, &mut state) {
10640                    return true;
10641                }
10642            }
10643            false
10644        }
10645
10646        assert!(
10647            !has_cycle(&deps),
10648            "task graph should not contain a cycle when a plugin reads from a child track"
10649        );
10650    }
10651
10652    #[tokio::test]
10653    async fn track_set_parent_wires_child_io_to_folder_even_after_addtrack() {
10654        let (mut engine, mut client_rx) = make_engine_with_client();
10655        let folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
10656        let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
10657        insert_track(&mut engine, folder);
10658        insert_track(&mut engine, child);
10659
10660        engine
10661            .handle_request_inner(
10662                Action::TrackSetParent {
10663                    track_name: "child".to_string(),
10664                    parent_name: Some("folder".to_string()),
10665                },
10666                false,
10667            )
10668            .await;
10669
10670        while let Ok(Some(_)) =
10671            tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
10672        {}
10673
10674        let state = engine.state.lock();
10675        let folder = state.tracks.get("folder").unwrap().lock();
10676        let child = state.tracks.get("child").unwrap().lock();
10677
10678        // Folder input -> child input.
10679        for (i, (parent_in, child_in)) in folder.audio.ins.iter().zip(&child.audio.ins).enumerate()
10680        {
10681            assert!(
10682                child_in
10683                    .connections
10684                    .lock()
10685                    .iter()
10686                    .any(|c| Arc::ptr_eq(c, parent_in)),
10687                "folder input {i} is not routed to child input {i}"
10688            );
10689        }
10690
10691        // Child output -> folder output.
10692        for (i, (child_out, parent_out)) in
10693            child.audio.outs.iter().zip(&folder.audio.outs).enumerate()
10694        {
10695            assert!(
10696                parent_out
10697                    .connections
10698                    .lock()
10699                    .iter()
10700                    .any(|c| Arc::ptr_eq(c, child_out)),
10701                "child output {i} is not routed to folder output {i}"
10702            );
10703        }
10704    }
10705
10706    #[tokio::test]
10707    async fn folder_child_audio_passes_through() {
10708        let (mut engine, mut client_rx) = make_engine_with_client();
10709        let folder = Track::new_folder("folder".to_string(), 1, 1, 0, 0, 64, 48_000.0);
10710        let child = Track::new("child".to_string(), 1, 1, 0, 0, 64, 48_000.0);
10711        insert_track(&mut engine, folder);
10712        insert_track(&mut engine, child);
10713
10714        engine
10715            .handle_request_inner(
10716                Action::TrackSetParent {
10717                    track_name: "child".to_string(),
10718                    parent_name: Some("folder".to_string()),
10719                },
10720                false,
10721            )
10722            .await;
10723        while let Ok(Some(_)) =
10724            tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
10725        {}
10726
10727        {
10728            let state = engine.state.lock();
10729            let folder = state.tracks.get("folder").unwrap().clone();
10730            let child = state.tracks.get("child").unwrap().clone();
10731
10732            folder.lock().input_monitor = vec![true];
10733            child.lock().input_monitor = vec![true];
10734
10735            // Feed a signal into the folder input from an external source.
10736            let source = Arc::new(crate::audio::io::AudioIO::new(64));
10737            for sample in source.buffer.lock().iter_mut() {
10738                *sample = 0.75;
10739            }
10740            crate::audio::io::AudioIO::connect(&source, &folder.lock().audio.ins[0]);
10741
10742            folder.lock().process_folder_input();
10743            child.lock().process();
10744            folder.lock().process_folder_output();
10745
10746            let output = folder.lock().audio.outs[0].buffer.lock();
10747            assert!(
10748                output.iter().any(|s| (*s - 0.75).abs() < 1e-5),
10749                "folder output should contain the child-processed folder input signal, got {:?}",
10750                output.iter().take(8).collect::<Vec<_>>()
10751            );
10752        }
10753    }
10754
10755    #[tokio::test]
10756    async fn remove_folder_track_deletes_descendants_recursively() {
10757        let (mut engine, mut client_rx) = make_engine_with_client();
10758        let folder = Track::new_folder("folder".to_string(), 1, 1, 0, 0, 64, 48_000.0);
10759        let child = Track::new_folder("child".to_string(), 1, 1, 0, 0, 64, 48_000.0);
10760        let grandchild = Track::new("grandchild".to_string(), 1, 1, 0, 0, 64, 48_000.0);
10761        insert_track(&mut engine, folder);
10762        insert_track(&mut engine, child);
10763        insert_track(&mut engine, grandchild);
10764
10765        engine
10766            .handle_request(Action::TrackSetParent {
10767                track_name: "child".to_string(),
10768                parent_name: Some("folder".to_string()),
10769            })
10770            .await;
10771        engine
10772            .handle_request(Action::TrackSetParent {
10773                track_name: "grandchild".to_string(),
10774                parent_name: Some("child".to_string()),
10775            })
10776            .await;
10777
10778        // Drain TrackSetParent notifications so we can inspect the removal notifications.
10779        while let Ok(Some(_)) =
10780            tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
10781        {}
10782
10783        engine
10784            .handle_request(Action::RemoveTrack("folder".to_string()))
10785            .await;
10786
10787        {
10788            let state = engine.state.lock();
10789            assert!(
10790                !state.tracks.contains_key("folder"),
10791                "folder should have been removed"
10792            );
10793            assert!(
10794                !state.tracks.contains_key("child"),
10795                "child should have been removed"
10796            );
10797            assert!(
10798                !state.tracks.contains_key("grandchild"),
10799                "grandchild should have been removed"
10800            );
10801        }
10802
10803        let mut removed_names = Vec::new();
10804        for _ in 0..3 {
10805            let msg = tokio::time::timeout(TokioDuration::from_millis(100), client_rx.recv()).await;
10806            if let Ok(Some(Message::Response(Ok(Action::RemoveTrack(name))))) = msg {
10807                removed_names.push(name);
10808            }
10809        }
10810        assert_eq!(
10811            removed_names,
10812            vec!["grandchild", "child", "folder"],
10813            "descendants should be removed before the folder and clients notified"
10814        );
10815    }
10816
10817    #[tokio::test]
10818    async fn track_set_folder_rejects_master_track() {
10819        let (mut engine, mut client_rx) = make_engine_with_client();
10820        let mut track = Track::new("master".to_string(), 2, 2, 0, 0, 64, 48_000.0);
10821        track.is_master = true;
10822        insert_track(&mut engine, track);
10823
10824        engine
10825            .handle_request_inner(
10826                Action::TrackSetFolder {
10827                    track_name: "master".to_string(),
10828                    is_folder: true,
10829                },
10830                false,
10831            )
10832            .await;
10833
10834        {
10835            let state = engine.state.lock();
10836            assert!(!state.tracks.get("master").unwrap().lock().is_folder);
10837        }
10838
10839        let msg = tokio::time::timeout(TokioDuration::from_millis(100), client_rx.recv()).await;
10840        assert!(
10841            matches!(msg, Ok(Some(Message::Response(Err(_))))),
10842            "master track folder conversion should report an error"
10843        );
10844    }
10845
10846    #[tokio::test]
10847    async fn track_toggle_master_ignored_for_folder_track() {
10848        let (mut engine, mut client_rx) = make_engine_with_client();
10849        let folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
10850        insert_track(&mut engine, folder);
10851
10852        engine
10853            .handle_request_inner(Action::TrackToggleMaster("folder".to_string()), false)
10854            .await;
10855
10856        {
10857            let state = engine.state.lock();
10858            assert!(!state.tracks.get("folder").unwrap().lock().is_master);
10859        }
10860
10861        let msg = tokio::time::timeout(TokioDuration::from_millis(100), client_rx.recv()).await;
10862        assert!(
10863            matches!(
10864                msg,
10865                Ok(Some(Message::Response(Ok(Action::TrackToggleMaster(ref name)))))
10866                    if name == "folder"
10867            ),
10868            "folder track master toggle should still be echoed to clients: {msg:?}"
10869        );
10870    }
10871}