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