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