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