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