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                        track.lock().toggle_master();
2522                        self.notify_clients(Ok(Action::TrackToggleMaster(track_name.clone())))
2523                            .await;
2524                    }
2525                }
2526                Action::TrackToggleArm(ref track_name) => {
2527                    if let Some(track) = self.state.lock().tracks.get(track_name) {
2528                        track.lock().arm();
2529                        self.notify_clients(Ok(Action::TrackToggleArm(track_name.clone())))
2530                            .await;
2531                    }
2532                }
2533                Action::TrackToggleInputMonitor {
2534                    ref track_name,
2535                    lane,
2536                } => {
2537                    if let Some(track) = self.state.lock().tracks.get(track_name) {
2538                        track.lock().toggle_input_monitor(lane);
2539                        self.notify_clients(Ok(Action::TrackToggleInputMonitor {
2540                            track_name: track_name.clone(),
2541                            lane,
2542                        }))
2543                        .await;
2544                    }
2545                }
2546                Action::TrackToggleDiskMonitor {
2547                    ref track_name,
2548                    lane,
2549                } => {
2550                    if let Some(track) = self.state.lock().tracks.get(track_name) {
2551                        track.lock().toggle_disk_monitor(lane);
2552                        self.notify_clients(Ok(Action::TrackToggleDiskMonitor {
2553                            track_name: track_name.clone(),
2554                            lane,
2555                        }))
2556                        .await;
2557                    }
2558                }
2559                Action::TrackToggleMidiInputMonitor {
2560                    ref track_name,
2561                    lane,
2562                } => {
2563                    if let Some(track) = self.state.lock().tracks.get(track_name) {
2564                        track.lock().toggle_midi_input_monitor(lane);
2565                        self.notify_clients(Ok(Action::TrackToggleMidiInputMonitor {
2566                            track_name: track_name.clone(),
2567                            lane,
2568                        }))
2569                        .await;
2570                    }
2571                }
2572                Action::TrackToggleMidiDiskMonitor {
2573                    ref track_name,
2574                    lane,
2575                } => {
2576                    if let Some(track) = self.state.lock().tracks.get(track_name) {
2577                        track.lock().toggle_midi_disk_monitor(lane);
2578                        self.notify_clients(Ok(Action::TrackToggleMidiDiskMonitor {
2579                            track_name: track_name.clone(),
2580                            lane,
2581                        }))
2582                        .await;
2583                    }
2584                }
2585                _ => {}
2586            }
2587        }
2588        for action in mapped_global_actions {
2589            self.handle_request_inner(action, false).await;
2590        }
2591    }
2592
2593    fn upstream_audio_track_names(
2594        &self,
2595        seeds: &std::collections::HashSet<String>,
2596    ) -> std::collections::HashSet<String> {
2597        let state = self.state.lock();
2598        let mut output_to_track: std::collections::HashMap<
2599            *const crate::audio::io::AudioIO,
2600            String,
2601        > = std::collections::HashMap::new();
2602        for (name, track) in &state.tracks {
2603            let t = track.lock();
2604            for out in &t.audio.outs {
2605                output_to_track.insert(std::sync::Arc::as_ptr(out), name.clone());
2606            }
2607        }
2608        let mut upstream = std::collections::HashSet::new();
2609        let mut to_process: Vec<String> = seeds.iter().cloned().collect();
2610        let mut processed = std::collections::HashSet::new();
2611        while let Some(target_name) = to_process.pop() {
2612            if !processed.insert(target_name.clone()) {
2613                continue;
2614            }
2615            if let Some(target_track) = state.tracks.get(&target_name) {
2616                let tt = target_track.lock();
2617                for input in &tt.audio.ins {
2618                    for conn in input.connections.lock().iter() {
2619                        let conn_ptr = std::sync::Arc::as_ptr(conn);
2620                        if let Some(source_name) = output_to_track.get(&conn_ptr)
2621                            && source_name != &target_name
2622                            && !seeds.contains(source_name)
2623                        {
2624                            upstream.insert(source_name.clone());
2625                            to_process.push(source_name.clone());
2626                        }
2627                    }
2628                }
2629            }
2630        }
2631        upstream
2632    }
2633
2634    fn is_track_in_soloed_folder(
2635        &self,
2636        track: &Track,
2637        tracks: &std::collections::HashMap<String, Arc<UnsafeMutex<Box<Track>>>>,
2638    ) -> bool {
2639        let mut current = track.parent_track.as_deref();
2640        while let Some(parent_name) = current {
2641            if let Some(parent) = tracks.get(parent_name) {
2642                let p = parent.lock();
2643                if p.soloed {
2644                    return true;
2645                }
2646                current = p.parent_track.as_deref();
2647            } else {
2648                break;
2649            }
2650        }
2651        false
2652    }
2653
2654    fn folder_has_soloed_descendant(
2655        &self,
2656        folder_name: &str,
2657        tracks: &std::collections::HashMap<String, Arc<UnsafeMutex<Box<Track>>>>,
2658    ) -> bool {
2659        for track in tracks.values() {
2660            let t = track.lock();
2661            if !t.soloed {
2662                continue;
2663            }
2664            let mut current = t.parent_track.as_deref();
2665            while let Some(parent_name) = current {
2666                if parent_name == folder_name {
2667                    return true;
2668                }
2669                if let Some(parent) = tracks.get(parent_name) {
2670                    current = parent.lock().parent_track.as_deref();
2671                } else {
2672                    break;
2673                }
2674            }
2675        }
2676        false
2677    }
2678
2679    fn refresh_realtime_infection(&self) {
2680        let state = self.state.lock();
2681        let live_seeds: std::collections::HashSet<String> = state
2682            .tracks
2683            .iter()
2684            .filter_map(|(name, track)| {
2685                let t = track.lock();
2686                if t.armed && t.input_monitor.iter().any(|&m| m) {
2687                    Some(name.clone())
2688                } else {
2689                    None
2690                }
2691            })
2692            .collect();
2693        let mut output_owner: std::collections::HashMap<*const crate::audio::io::AudioIO, String> =
2694            std::collections::HashMap::new();
2695        for (name, track) in state.tracks.iter() {
2696            let t = track.lock();
2697            for out in &t.audio.outs {
2698                output_owner.insert(std::sync::Arc::as_ptr(out), name.clone());
2699            }
2700        }
2701
2702        let mut infected = live_seeds.clone();
2703        let mut mixed_nodes = std::collections::HashSet::new();
2704        loop {
2705            let mut changed = false;
2706            for (name, track) in state.tracks.iter() {
2707                let t = track.lock();
2708                let mut upstream_owners = std::collections::HashSet::new();
2709                for input in &t.audio.ins {
2710                    for conn in input.connections.lock().iter() {
2711                        if let Some(owner) = output_owner.get(&std::sync::Arc::as_ptr(conn)) {
2712                            upstream_owners.insert(owner.clone());
2713                        }
2714                    }
2715                }
2716                if upstream_owners.is_empty() {
2717                    continue;
2718                }
2719                let has_realtime = upstream_owners
2720                    .iter()
2721                    .any(|owner| infected.contains(owner) || live_seeds.contains(owner));
2722                let has_playback = upstream_owners
2723                    .iter()
2724                    .any(|owner| !infected.contains(owner) && !live_seeds.contains(owner));
2725                if has_realtime && has_playback {
2726                    mixed_nodes.insert(name.clone());
2727                }
2728                if has_realtime && infected.insert(name.clone()) {
2729                    changed = true;
2730                }
2731            }
2732            if !changed {
2733                break;
2734            }
2735        }
2736
2737        for (name, track) in state.tracks.iter() {
2738            let forced = infected.contains(name) && !live_seeds.contains(name);
2739            let t = track.lock();
2740            t.set_shared_realtime_mixed(mixed_nodes.contains(name));
2741            t.set_force_realtime_domain(forced);
2742        }
2743    }
2744
2745    fn apply_mute_solo_policy(&mut self) {
2746        let mut newly_disabled_tracks = Vec::new();
2747        {
2748            let tracks = &self.state.lock().tracks;
2749            let soloed: std::collections::HashSet<String> = tracks
2750                .iter()
2751                .filter_map(|(name, t)| {
2752                    if t.lock().soloed {
2753                        Some(name.clone())
2754                    } else {
2755                        None
2756                    }
2757                })
2758                .collect();
2759            let any_soloed = !soloed.is_empty();
2760            let upstream = if any_soloed {
2761                self.upstream_audio_track_names(&soloed)
2762            } else {
2763                std::collections::HashSet::new()
2764            };
2765            for track in tracks.values() {
2766                let t = track.lock();
2767                let was_enabled = t.output_enabled;
2768                let in_soloed_folder = self.is_track_in_soloed_folder(t, tracks);
2769                let folder_with_soloed_child =
2770                    t.is_folder && self.folder_has_soloed_descendant(&t.name, tracks);
2771                let enabled = if t.is_master {
2772                    !t.muted
2773                } else if any_soloed {
2774                    (t.soloed
2775                        || upstream.contains(&t.name)
2776                        || in_soloed_folder
2777                        || folder_with_soloed_child)
2778                        && !t.muted
2779                } else {
2780                    !t.muted
2781                };
2782                t.set_output_enabled(enabled);
2783                if was_enabled && !enabled {
2784                    newly_disabled_tracks.push(t.name.clone());
2785                }
2786            }
2787        }
2788        let mut note_off_events = Vec::new();
2789        for track_name in newly_disabled_tracks {
2790            note_off_events.extend(self.note_off_events_for_track(&track_name));
2791        }
2792        if !note_off_events.is_empty() {
2793            self.pending_hw_midi_out_events_by_device
2794                .extend(note_off_events);
2795        }
2796    }
2797
2798    fn sanitize_file_stem(name: &str) -> String {
2799        let mut out = String::with_capacity(name.len());
2800        for c in name.chars() {
2801            if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
2802                out.push(c);
2803            } else {
2804                out.push('_');
2805            }
2806        }
2807        if out.is_empty() {
2808            "track".to_string()
2809        } else {
2810            out
2811        }
2812    }
2813
2814    fn next_recording_file_name(track_name: &str) -> String {
2815        let ts = SystemTime::now()
2816            .duration_since(UNIX_EPOCH)
2817            .map(|d| d.as_secs())
2818            .unwrap_or(0);
2819        format!("{}_{}.wav", Self::sanitize_file_stem(track_name), ts)
2820    }
2821
2822    fn next_midi_recording_file_name(track_name: &str) -> String {
2823        let ts = SystemTime::now()
2824            .duration_since(UNIX_EPOCH)
2825            .map(|d| d.as_secs())
2826            .unwrap_or(0);
2827        format!("{}_{}.mid", Self::sanitize_file_stem(track_name), ts)
2828    }
2829
2830    fn append_recorded_cycle(&mut self) {
2831        if !self.playing || !self.record_enabled {
2832            return;
2833        }
2834        for (name, track_handle) in &self.state.lock().tracks {
2835            let track = track_handle.lock();
2836            if !track.armed {
2837                continue;
2838            }
2839            let audio_channels = track.record_tap_outs.len();
2840            let audio_frames = track
2841                .record_tap_outs
2842                .first()
2843                .map(|ch| ch.len())
2844                .unwrap_or(0);
2845            let frames = audio_frames.max(self.current_cycle_samples());
2846            if frames == 0 {
2847                continue;
2848            }
2849            let segments = self.recording_segments_for_cycle(frames);
2850            for (segment_start, segment_end, frame_offset) in segments {
2851                let segment_len = segment_end.saturating_sub(segment_start);
2852                if segment_len == 0 {
2853                    continue;
2854                }
2855
2856                if audio_channels > 0 && audio_frames > 0 {
2857                    let audio_entry =
2858                        self.audio_recordings
2859                            .entry(name.clone())
2860                            .or_insert_with(|| RecordingSession {
2861                                start_sample: segment_start,
2862                                samples: Vec::with_capacity(segment_len * audio_channels * 2),
2863                                channels: audio_channels,
2864                                file_name: Self::next_recording_file_name(name),
2865                                stripe_peaks: vec![Vec::new(); audio_channels],
2866                                current_stripe_frames: 0,
2867                            });
2868                    if audio_entry.channels != audio_channels {
2869                        continue;
2870                    }
2871                    if let Some(entry) = self.audio_recordings.get_mut(name.as_str()) {
2872                        let from = frame_offset.min(audio_frames);
2873                        let to = frame_offset.saturating_add(segment_len).min(audio_frames);
2874                        for frame in from..to {
2875                            let is_new_stripe =
2876                                entry.current_stripe_frames % RECORDING_STRIPE_FRAMES == 0;
2877                            for ch in 0..audio_channels {
2878                                let sample = track.record_tap_outs[ch][frame].clamp(-1.0, 1.0);
2879                                if is_new_stripe {
2880                                    entry.stripe_peaks[ch].push([sample, sample]);
2881                                } else {
2882                                    let idx = entry.stripe_peaks[ch].len() - 1;
2883                                    entry.stripe_peaks[ch][idx][0] =
2884                                        entry.stripe_peaks[ch][idx][0].min(sample);
2885                                    entry.stripe_peaks[ch][idx][1] =
2886                                        entry.stripe_peaks[ch][idx][1].max(sample);
2887                                }
2888                                entry.samples.push(track.record_tap_outs[ch][frame]);
2889                            }
2890                            entry.current_stripe_frames += 1;
2891                        }
2892                    }
2893                }
2894
2895                let entry = self.midi_recordings.entry(name.clone()).or_insert_with(|| {
2896                    MidiRecordingSession {
2897                        start_sample: segment_start,
2898                        events: Vec::new(),
2899                        file_name: Self::next_midi_recording_file_name(name),
2900                    }
2901                });
2902                let from = frame_offset;
2903                let to = frame_offset.saturating_add(segment_len);
2904                for event in &track.record_tap_midi_in {
2905                    let frame = event.frame as usize;
2906                    if frame < from || frame >= to {
2907                        continue;
2908                    }
2909                    let abs_sample = segment_start as u64 + (frame - from) as u64;
2910                    entry.events.push((abs_sample, event.data.clone()));
2911                }
2912
2913                if self.punch_enabled
2914                    && let Some((_, punch_end)) = self.punch_range_samples
2915                    && segment_end == punch_end
2916                {
2917                    if let Some(done) = self.audio_recordings.remove(name.as_str()) {
2918                        self.completed_audio_recordings.push((name.clone(), done));
2919                    }
2920                    if let Some(done) = self.midi_recordings.remove(name.as_str()) {
2921                        self.completed_midi_recordings.push((name.clone(), done));
2922                    }
2923                } else if self.loop_enabled
2924                    && let Some((_, loop_end)) = self.loop_range_samples
2925                    && segment_end == loop_end
2926                {
2927                    if let Some(done) = self.audio_recordings.remove(name.as_str()) {
2928                        self.completed_audio_recordings.push((name.clone(), done));
2929                    }
2930                    if let Some(done) = self.midi_recordings.remove(name.as_str()) {
2931                        self.completed_midi_recordings.push((name.clone(), done));
2932                    }
2933                }
2934            }
2935        }
2936    }
2937
2938    async fn flush_completed_recordings(&mut self) {
2939        if self.completed_audio_recordings.is_empty() && self.completed_midi_recordings.is_empty() {
2940            return;
2941        }
2942        let Some(audio_dir) = self.session_audio_dir() else {
2943            self.completed_audio_recordings.clear();
2944            self.completed_midi_recordings.clear();
2945            return;
2946        };
2947        let Some(midi_dir) = self.session_midi_dir() else {
2948            self.completed_audio_recordings.clear();
2949            self.completed_midi_recordings.clear();
2950            return;
2951        };
2952        if std::fs::create_dir_all(&audio_dir).is_err()
2953            || std::fs::create_dir_all(&midi_dir).is_err()
2954        {
2955            self.completed_audio_recordings.clear();
2956            self.completed_midi_recordings.clear();
2957            return;
2958        }
2959        let rate = self
2960            .hw_driver
2961            .as_ref()
2962            .map(|o| o.lock().sample_rate())
2963            .unwrap_or(48_000);
2964        let completed_audio = std::mem::take(&mut self.completed_audio_recordings);
2965        for (track_name, rec) in completed_audio {
2966            self.flush_recording_entry(&audio_dir, rate, track_name, rec)
2967                .await;
2968        }
2969        let completed_midi = std::mem::take(&mut self.completed_midi_recordings);
2970        for (track_name, rec) in completed_midi {
2971            self.flush_midi_recording_entry(&midi_dir, rate as u32, track_name, rec)
2972                .await;
2973        }
2974    }
2975
2976    async fn flush_recordings(&mut self) {
2977        let Some(audio_dir) = self.session_audio_dir() else {
2978            if !self.audio_recordings.is_empty()
2979                || !self.midi_recordings.is_empty()
2980                || !self.completed_audio_recordings.is_empty()
2981                || !self.completed_midi_recordings.is_empty()
2982            {
2983                self.notify_clients(Err("Recording stopped: session path is not set".to_string()))
2984                    .await;
2985            }
2986            self.audio_recordings.clear();
2987            self.midi_recordings.clear();
2988            self.completed_audio_recordings.clear();
2989            self.completed_midi_recordings.clear();
2990            return;
2991        };
2992        if std::fs::create_dir_all(&audio_dir).is_err() {
2993            self.notify_clients(Err(format!(
2994                "Recording stopped: failed to create audio directory {}",
2995                audio_dir.display()
2996            )))
2997            .await;
2998            self.audio_recordings.clear();
2999            self.midi_recordings.clear();
3000            self.completed_audio_recordings.clear();
3001            self.completed_midi_recordings.clear();
3002            return;
3003        }
3004        let Some(midi_dir) = self.session_midi_dir() else {
3005            self.audio_recordings.clear();
3006            self.midi_recordings.clear();
3007            self.completed_audio_recordings.clear();
3008            self.completed_midi_recordings.clear();
3009            return;
3010        };
3011        if std::fs::create_dir_all(&midi_dir).is_err() {
3012            self.audio_recordings.clear();
3013            self.midi_recordings.clear();
3014            self.completed_audio_recordings.clear();
3015            self.completed_midi_recordings.clear();
3016            return;
3017        }
3018        let rate = self
3019            .hw_driver
3020            .as_ref()
3021            .map(|o| o.lock().sample_rate())
3022            .unwrap_or(48_000);
3023        let completed_audio = std::mem::take(&mut self.completed_audio_recordings);
3024        for (track_name, rec) in completed_audio {
3025            self.flush_recording_entry(&audio_dir, rate, track_name, rec)
3026                .await;
3027        }
3028        let completed_midi = std::mem::take(&mut self.completed_midi_recordings);
3029        for (track_name, rec) in completed_midi {
3030            self.flush_midi_recording_entry(&midi_dir, rate as u32, track_name, rec)
3031                .await;
3032        }
3033        let recordings = std::mem::take(&mut self.audio_recordings);
3034        for (track_name, rec) in recordings {
3035            self.flush_recording_entry(&audio_dir, rate, track_name, rec)
3036                .await;
3037        }
3038        let midi_recordings = std::mem::take(&mut self.midi_recordings);
3039        for (track_name, rec) in midi_recordings {
3040            self.flush_midi_recording_entry(&midi_dir, rate as u32, track_name, rec)
3041                .await;
3042        }
3043    }
3044
3045    fn compute_peaks_from_stripes(
3046        stripe_peaks: &[Vec<[f32; 2]>],
3047        total_frames: usize,
3048        channels: usize,
3049    ) -> serde_json::Value {
3050        const MAX_PEAK_BINS: usize = 32_768;
3051        if total_frames == 0 || stripe_peaks.is_empty() {
3052            return serde_json::json!({"peaks": []});
3053        }
3054        let target_bins = total_frames.clamp(1024, MAX_PEAK_BINS);
3055        let mut peaks = vec![vec![[0.0_f32, 0.0_f32]; target_bins]; channels];
3056        for (ch, channel_peaks) in peaks.iter_mut().enumerate() {
3057            let mut touched = vec![false; target_bins];
3058            let empty = Vec::new();
3059            let channel_stripes = stripe_peaks.get(ch).unwrap_or(&empty);
3060            for (stripe_idx, stripe) in channel_stripes.iter().enumerate() {
3061                let stripe_start = stripe_idx * RECORDING_STRIPE_FRAMES;
3062                let stripe_end = ((stripe_idx + 1) * RECORDING_STRIPE_FRAMES).min(total_frames);
3063                let start_bin = (stripe_start * target_bins) / total_frames.max(1);
3064                let end_bin = ((stripe_end.saturating_sub(1)) * target_bins / total_frames.max(1))
3065                    .min(target_bins - 1);
3066                for bin in start_bin..=end_bin {
3067                    if !touched[bin] {
3068                        channel_peaks[bin] = *stripe;
3069                        touched[bin] = true;
3070                    } else {
3071                        channel_peaks[bin][0] = channel_peaks[bin][0].min(stripe[0]);
3072                        channel_peaks[bin][1] = channel_peaks[bin][1].max(stripe[1]);
3073                    }
3074                }
3075            }
3076        }
3077        serde_json::json!({
3078            "peaks": peaks.iter().map(|ch| {
3079                ch.iter().map(|pair| serde_json::json!([pair[0], pair[1]])).collect::<Vec<_>>()
3080            }).collect::<Vec<_>>()
3081        })
3082    }
3083
3084    async fn flush_recording_entry(
3085        &mut self,
3086        audio_dir: &Path,
3087        rate: i32,
3088        track_name: String,
3089        rec: RecordingSession,
3090    ) {
3091        if rec.samples.is_empty() || rec.channels == 0 {
3092            return;
3093        }
3094
3095        let trim_frames = self.hw_output_latency_frames;
3096        let trim_samples = trim_frames * rec.channels;
3097        let samples = if trim_samples > 0 && rec.samples.len() > trim_samples {
3098            &rec.samples[trim_samples..]
3099        } else {
3100            &rec.samples[..]
3101        };
3102        if samples.is_empty() {
3103            return;
3104        }
3105        let file_path = audio_dir.join(&rec.file_name);
3106        let write_result =
3107            crate::audio_codec::write_wav_f32(&file_path, samples, rec.channels, rate as u32);
3108        if let Err(e) = write_result {
3109            tracing::error!("flush_recording_entry: WAV write failed: {}", e);
3110            self.notify_clients(Err(format!(
3111                "Failed to write recording {}: {}",
3112                file_path.display(),
3113                e
3114            )))
3115            .await;
3116            return;
3117        }
3118
3119        let total_frames = rec.current_stripe_frames;
3120        let peaks_json =
3121            Self::compute_peaks_from_stripes(&rec.stripe_peaks, total_frames, rec.channels);
3122        let peaks_file_name = format!("{}.json", rec.file_name);
3123        let peaks_rel = format!("peaks/{}", peaks_file_name);
3124        let peaks_path = self.session_peaks_dir().map(|d| d.join(&peaks_file_name));
3125        if let Some(peaks_dir) = self.session_peaks_dir() {
3126            let _ = std::fs::create_dir_all(&peaks_dir);
3127        }
3128        if let Some(ref path) = peaks_path
3129            && let Err(e) = std::fs::write(
3130                path,
3131                serde_json::to_string_pretty(&peaks_json).unwrap_or_default(),
3132            )
3133        {
3134            tracing::warn!("Failed to write peaks file {}: {}", path.display(), e);
3135        }
3136        let length = samples.len() / rec.channels;
3137        let start_sample = rec.start_sample.saturating_add(trim_frames);
3138        let clip_rel_name = format!("audio/{}", rec.file_name);
3139        let clip = AudioClip::new(
3140            clip_rel_name.clone(),
3141            start_sample,
3142            start_sample.saturating_add(length.max(1)),
3143        );
3144        let (audio_ins, audio_outs) = if let Some(track) = self.state.lock().tracks.get(&track_name)
3145        {
3146            let track = track.lock();
3147            let audio_ins = track.audio.ins.len();
3148            let audio_outs = track.audio.outs.len();
3149            track.audio.clips.push(clip.clone());
3150            (audio_ins, audio_outs)
3151        } else {
3152            tracing::warn!(
3153                "flush_recording_entry: track '{}' not found in engine state",
3154                track_name
3155            );
3156            (0, 0)
3157        };
3158        self.notify_clients(Ok(Action::AddClip {
3159            name: clip_rel_name,
3160            track_name: track_name.clone(),
3161            start: start_sample,
3162            length,
3163            offset: 0,
3164            input_channel: 0,
3165            muted: false,
3166            peaks_file: peaks_path.is_some().then_some(peaks_rel),
3167            kind: Kind::Audio,
3168            fade_enabled: clip.fade_enabled,
3169            fade_in_samples: clip.fade_in_samples,
3170            fade_out_samples: clip.fade_out_samples,
3171            source_name: None,
3172            source_offset: None,
3173            source_length: None,
3174            preview_name: None,
3175            pitch_correction_points: vec![],
3176            pitch_correction_frame_likeness: None,
3177            pitch_correction_inertia_ms: None,
3178            pitch_correction_formant_compensation: None,
3179            plugin_graph_json: Some(Self::default_clip_plugin_graph_json(audio_ins, audio_outs)),
3180        }))
3181        .await;
3182        if let Some(track) = self.state.lock().tracks.get(&track_name).cloned() {
3183            tokio::task::spawn_blocking(move || {
3184                track.lock().preload_clips();
3185                tracing::debug!("Preloaded clips for track '{}' after recording", track_name);
3186            });
3187        }
3188    }
3189
3190    async fn flush_track_recording(&mut self, track_name: &str) {
3191        let Some(audio_dir) = self.session_audio_dir() else {
3192            self.audio_recordings.remove(track_name);
3193            self.midi_recordings.remove(track_name);
3194            self.completed_audio_recordings
3195                .retain(|(name, _)| name != track_name);
3196            self.completed_midi_recordings
3197                .retain(|(name, _)| name != track_name);
3198            return;
3199        };
3200        let Some(midi_dir) = self.session_midi_dir() else {
3201            self.audio_recordings.remove(track_name);
3202            self.midi_recordings.remove(track_name);
3203            self.completed_audio_recordings
3204                .retain(|(name, _)| name != track_name);
3205            self.completed_midi_recordings
3206                .retain(|(name, _)| name != track_name);
3207            return;
3208        };
3209        if std::fs::create_dir_all(&audio_dir).is_err()
3210            || std::fs::create_dir_all(&midi_dir).is_err()
3211        {
3212            return;
3213        }
3214        let rate = self
3215            .hw_driver
3216            .as_ref()
3217            .map(|o| o.lock().sample_rate())
3218            .unwrap_or(48_000);
3219        let mut i = 0;
3220        while i < self.completed_audio_recordings.len() {
3221            if self.completed_audio_recordings[i].0 == track_name {
3222                let (name, rec) = self.completed_audio_recordings.remove(i);
3223                self.flush_recording_entry(&audio_dir, rate, name, rec)
3224                    .await;
3225            } else {
3226                i += 1;
3227            }
3228        }
3229        let mut j = 0;
3230        while j < self.completed_midi_recordings.len() {
3231            if self.completed_midi_recordings[j].0 == track_name {
3232                let (name, rec) = self.completed_midi_recordings.remove(j);
3233                self.flush_midi_recording_entry(&midi_dir, rate as u32, name, rec)
3234                    .await;
3235            } else {
3236                j += 1;
3237            }
3238        }
3239
3240        let Some(rec) = self.audio_recordings.remove(track_name) else {
3241            if let Some(mrec) = self.midi_recordings.remove(track_name) {
3242                self.flush_midi_recording_entry(
3243                    &midi_dir,
3244                    rate as u32,
3245                    track_name.to_string(),
3246                    mrec,
3247                )
3248                .await;
3249            }
3250            return;
3251        };
3252        self.flush_recording_entry(&audio_dir, rate, track_name.to_string(), rec)
3253            .await;
3254        if let Some(mrec) = self.midi_recordings.remove(track_name) {
3255            self.flush_midi_recording_entry(&midi_dir, rate as u32, track_name.to_string(), mrec)
3256                .await;
3257        }
3258    }
3259
3260    async fn flush_midi_recording_entry(
3261        &mut self,
3262        midi_dir: &Path,
3263        sample_rate: u32,
3264        track_name: String,
3265        mut rec: MidiRecordingSession,
3266    ) {
3267        if rec.events.is_empty() {
3268            return;
3269        }
3270        rec.events.sort_by_key(|(sample, _)| *sample);
3271        let clip_rel_name = format!("midi/{}", rec.file_name);
3272        let clip_len_samples = rec
3273            .events
3274            .last()
3275            .map(|(s, _)| s.saturating_sub(rec.start_sample as u64) as usize + 1)
3276            .unwrap_or(1);
3277
3278        for (sample, _) in &mut rec.events {
3279            *sample = sample.saturating_sub(rec.start_sample as u64);
3280        }
3281        let path = midi_dir.join(&rec.file_name);
3282        if let Err(e) = Self::write_midi_file(&path, sample_rate, &rec.events) {
3283            self.notify_clients(Err(format!(
3284                "Failed to write MIDI recording {}: {}",
3285                path.display(),
3286                e
3287            )))
3288            .await;
3289            return;
3290        }
3291        let mut clip = MIDIClip::new(
3292            clip_rel_name.clone(),
3293            rec.start_sample,
3294            rec.start_sample.saturating_add(clip_len_samples.max(1)),
3295        );
3296        clip.offset = 0;
3297        if let Some(track) = self.state.lock().tracks.get(&track_name) {
3298            track.lock().midi.clips.push(clip);
3299        }
3300        self.notify_clients(Ok(Action::AddClip {
3301            name: clip_rel_name,
3302            track_name: track_name.clone(),
3303            start: rec.start_sample,
3304            length: clip_len_samples,
3305            offset: 0,
3306            input_channel: 0,
3307            muted: false,
3308            peaks_file: None,
3309            kind: Kind::MIDI,
3310            fade_enabled: true,
3311            fade_in_samples: 240,
3312            fade_out_samples: 240,
3313            source_name: None,
3314            source_offset: None,
3315            source_length: None,
3316            preview_name: None,
3317            pitch_correction_points: vec![],
3318            pitch_correction_frame_likeness: None,
3319            pitch_correction_inertia_ms: None,
3320            pitch_correction_formant_compensation: None,
3321            plugin_graph_json: None,
3322        }))
3323        .await;
3324        if let Some(track) = self.state.lock().tracks.get(&track_name).cloned() {
3325            tokio::task::spawn_blocking(move || {
3326                track.lock().preload_clips();
3327                tracing::debug!(
3328                    "Preloaded clips for track '{}' after MIDI recording",
3329                    track_name
3330                );
3331            });
3332        }
3333    }
3334
3335    fn write_midi_file(
3336        path: &Path,
3337        sample_rate: u32,
3338        events: &[(u64, Vec<u8>)],
3339    ) -> Result<(), String> {
3340        let ppq: u16 = 480;
3341        let ticks_per_second: u64 = 960;
3342        let arena = Arena::new();
3343        let mut track_events: Vec<TrackEvent<'_>> = vec![TrackEvent {
3344            delta: u28::new(0),
3345            kind: TrackEventKind::Meta(MetaMessage::Tempo(u24::new(500_000))),
3346        }];
3347        let mut prev_ticks = 0_u64;
3348        for (sample, data) in events {
3349            let ticks = sample.saturating_mul(ticks_per_second) / sample_rate.max(1) as u64;
3350            let delta = ticks.saturating_sub(prev_ticks).min(u32::MAX as u64) as u32;
3351            prev_ticks = ticks;
3352            let Ok(live) = LiveEvent::parse(data) else {
3353                continue;
3354            };
3355            let kind = live.as_track_event(&arena);
3356            track_events.push(TrackEvent {
3357                delta: u28::new(delta),
3358                kind,
3359            });
3360        }
3361        track_events.push(TrackEvent {
3362            delta: u28::new(0),
3363            kind: TrackEventKind::Meta(MetaMessage::EndOfTrack),
3364        });
3365
3366        let smf = Smf {
3367            header: Header::new(Format::SingleTrack, Timing::Metrical(u15::new(ppq))),
3368            tracks: vec![track_events],
3369        };
3370        let mut file = File::create(path).map_err(|e| e.to_string())?;
3371        smf.write_std(&mut file).map_err(|e| e.to_string())
3372    }
3373
3374    pub async fn init(&mut self) {
3375        let max_threads = num_cpus::get();
3376        for id in 0..max_threads {
3377            let (tx, rx) = channel::<Message>(32);
3378            let tx_thread = self.tx.clone();
3379            let handler = tokio::spawn(async move {
3380                let wrk = Worker::new(id, rx, tx_thread, 8);
3381                wrk.await.work().await;
3382            });
3383            self.workers.push(WorkerData::new(tx.clone(), handler));
3384        }
3385    }
3386
3387    async fn notify_clients(&mut self, action: Result<Action, String>) {
3388        self.clients.retain(|client| !client.is_closed());
3389        for client in self.clients.iter() {
3390            if client
3391                .send(Message::Response(action.clone()))
3392                .await
3393                .is_err()
3394            {}
3395        }
3396    }
3397
3398    fn spawn_plugin_host_stderr_reader(&self, stderr: std::process::ChildStderr, source: String) {
3399        let tx = self.tx.clone();
3400        std::thread::spawn(move || {
3401            use std::io::{BufRead, BufReader};
3402            let reader = BufReader::new(stderr);
3403            for line in reader.lines() {
3404                if let Ok(line) = line
3405                    && !line.is_empty()
3406                {
3407                    let _ = tx.blocking_send(Message::Request(Action::Log {
3408                        source: source.clone(),
3409                        message: line,
3410                    }));
3411                }
3412            }
3413        });
3414    }
3415
3416    fn set_osc_enabled_with<F>(&mut self, enabled: bool, start_server: F) -> Result<(), String>
3417    where
3418        F: FnOnce(Sender<Message>) -> Result<OscServer, String>,
3419    {
3420        if enabled {
3421            if self.osc_server.is_none() {
3422                self.osc_server = Some(start_server(self.tx.clone())?);
3423            }
3424        } else if let Some(mut server) = self.osc_server.take() {
3425            server.stop();
3426        }
3427        Ok(())
3428    }
3429
3430    fn track_handle_by_name(&self, track_name: &str) -> Option<Arc<UnsafeMutex<Box<Track>>>> {
3431        self.state.lock().tracks.get(track_name).cloned()
3432    }
3433
3434    fn track_handle_or_err(
3435        &self,
3436        track_name: &str,
3437    ) -> Result<Arc<UnsafeMutex<Box<Track>>>, String> {
3438        self.track_handle_by_name(track_name)
3439            .ok_or_else(|| format!("Track not found: {track_name}"))
3440    }
3441
3442    fn add_clip_to_track(&self, request: ClipAddRequest<'_>) {
3443        if let Some(track) = self.state.lock().tracks.get(request.track_name) {
3444            let track = track.lock();
3445            if track.is_master || track.is_folder {
3446                return;
3447            }
3448            match request.kind {
3449                Kind::Audio => {
3450                    let mut clip = AudioClip::new(
3451                        request.name.to_string(),
3452                        request.start,
3453                        request.start.saturating_add(request.length.max(1)),
3454                    );
3455                    clip.offset = request.offset;
3456                    let max_lane = track.audio.ins.len().saturating_sub(1);
3457                    clip.input_channel = request.input_channel.min(max_lane);
3458                    clip.muted = request.muted;
3459                    clip.peaks_file = request.peaks_file;
3460                    clip.fade_enabled = request.fade_enabled;
3461                    clip.fade_in_samples = request.fade_in_samples;
3462                    clip.fade_out_samples = request.fade_out_samples;
3463                    clip.pitch_correction_preview_name = request.preview_name;
3464                    clip.pitch_correction_source_name = request.source_name;
3465                    clip.pitch_correction_source_offset = request.source_offset;
3466                    clip.pitch_correction_source_length = request.source_length;
3467                    clip.pitch_correction_points = request.pitch_correction_points;
3468                    clip.pitch_correction_frame_likeness = request.pitch_correction_frame_likeness;
3469                    clip.pitch_correction_inertia_ms = request.pitch_correction_inertia_ms;
3470                    clip.pitch_correction_formant_compensation =
3471                        request.pitch_correction_formant_compensation;
3472                    clip.plugin_graph_json = request.plugin_graph_json;
3473                    track.audio.clips.push(clip);
3474                    #[cfg(unix)]
3475                    track.clip_pitch_shifters.clear();
3476                }
3477                Kind::MIDI => {
3478                    let mut clip = MIDIClip::new(
3479                        request.name.to_string(),
3480                        request.start,
3481                        request.start.saturating_add(request.length.max(1)),
3482                    );
3483                    clip.offset = request.offset;
3484                    let max_lane = track.midi.ins.len().saturating_sub(1);
3485                    clip.input_channel = request.input_channel.min(max_lane);
3486                    clip.muted = request.muted;
3487                    track.midi.clips.push(clip);
3488                }
3489            }
3490        }
3491    }
3492
3493    fn audio_clip_from_data(data: &crate::message::AudioClipData) -> AudioClip {
3494        let mut clip = AudioClip::new(
3495            data.name.clone(),
3496            data.start,
3497            data.start.saturating_add(data.length.max(1)),
3498        );
3499        clip.offset = data.offset;
3500        clip.input_channel = data.input_channel;
3501        clip.muted = data.muted;
3502        clip.peaks_file = data.peaks_file.clone();
3503        clip.fade_enabled = data.fade_enabled;
3504        clip.fade_in_samples = data.fade_in_samples;
3505        clip.fade_out_samples = data.fade_out_samples;
3506        clip.pitch_correction_preview_name = data.preview_name.clone();
3507        clip.pitch_correction_source_name = data.source_name.clone();
3508        clip.pitch_correction_source_offset = data.source_offset;
3509        clip.pitch_correction_source_length = data.source_length;
3510        clip.pitch_correction_points = data.pitch_correction_points.clone();
3511        clip.pitch_correction_frame_likeness = data.pitch_correction_frame_likeness;
3512        clip.pitch_correction_inertia_ms = data.pitch_correction_inertia_ms;
3513        clip.pitch_correction_formant_compensation = data.pitch_correction_formant_compensation;
3514        clip.plugin_graph_json = data.plugin_graph_json.clone();
3515        clip.grouped_clips = data
3516            .grouped_clips
3517            .iter()
3518            .map(Self::audio_clip_from_data)
3519            .collect();
3520        for child in &mut clip.grouped_clips {
3521            child.fade_enabled = false;
3522            child.fade_in_samples = 0;
3523            child.fade_out_samples = 0;
3524        }
3525        clip
3526    }
3527
3528    fn midi_clip_from_data(data: &crate::message::MidiClipData) -> MIDIClip {
3529        let mut clip = MIDIClip::new(
3530            data.name.clone(),
3531            data.start,
3532            data.start.saturating_add(data.length.max(1)),
3533        );
3534        clip.offset = data.offset;
3535        clip.input_channel = data.input_channel;
3536        clip.muted = data.muted;
3537        clip.grouped_clips = data
3538            .grouped_clips
3539            .iter()
3540            .map(Self::midi_clip_from_data)
3541            .collect();
3542        clip
3543    }
3544
3545    fn add_grouped_clip_to_track(
3546        &self,
3547        track_name: &str,
3548        kind: Kind,
3549        audio_clip: Option<crate::message::AudioClipData>,
3550        midi_clip: Option<crate::message::MidiClipData>,
3551    ) {
3552        if let Some(track) = self.state.lock().tracks.get(track_name) {
3553            let track = track.lock();
3554            if track.is_master {
3555                return;
3556            }
3557            match kind {
3558                Kind::Audio => {
3559                    if let Some(mut clip) = audio_clip.map(|clip| Self::audio_clip_from_data(&clip))
3560                    {
3561                        let max_lane = track.audio.ins.len().saturating_sub(1);
3562                        clip.input_channel = clip.input_channel.min(max_lane);
3563                        track.audio.clips.push(clip);
3564                        #[cfg(unix)]
3565                        track.clip_pitch_shifters.clear();
3566                    }
3567                }
3568                Kind::MIDI => {
3569                    if let Some(mut clip) = midi_clip.map(|clip| Self::midi_clip_from_data(&clip)) {
3570                        let max_lane = track.midi.ins.len().saturating_sub(1);
3571                        clip.input_channel = clip.input_channel.min(max_lane);
3572                        track.midi.clips.push(clip);
3573                    }
3574                }
3575            }
3576        }
3577    }
3578
3579    fn remove_clips_from_track(&self, track_name: &str, kind: Kind, clip_indices: &[usize]) {
3580        if let Some(track) = self.state.lock().tracks.get(track_name) {
3581            let track = track.lock();
3582            let mut indices = clip_indices.to_vec();
3583            indices.sort_unstable();
3584            indices.dedup();
3585            match kind {
3586                Kind::Audio => {
3587                    for idx in indices.into_iter().rev() {
3588                        if idx < track.audio.clips.len() {
3589                            track.audio.clips.remove(idx);
3590                        }
3591                    }
3592                    #[cfg(unix)]
3593                    track.clip_pitch_shifters.clear();
3594                }
3595                Kind::MIDI => {
3596                    for idx in indices.into_iter().rev() {
3597                        if idx < track.midi.clips.len() {
3598                            track.midi.clips.remove(idx);
3599                        }
3600                    }
3601                }
3602            }
3603        }
3604    }
3605
3606    fn rename_clip_references(
3607        &self,
3608        track_name: &str,
3609        kind: Kind,
3610        clip_index: usize,
3611        new_name: &str,
3612    ) {
3613        let Some(track) = self.state.lock().tracks.get(track_name) else {
3614            return;
3615        };
3616        let track = track.lock();
3617        let old_name = match kind {
3618            Kind::Audio => {
3619                if clip_index >= track.audio.clips.len() {
3620                    return;
3621                }
3622                track.audio.clips[clip_index].name.clone()
3623            }
3624            Kind::MIDI => {
3625                if clip_index >= track.midi.clips.len() {
3626                    return;
3627                }
3628                track.midi.clips[clip_index].name.clone()
3629            }
3630        };
3631
3632        let new_file_name = match kind {
3633            Kind::Audio => format!("audio/{}.wav", new_name),
3634            Kind::MIDI => {
3635                let ext = std::path::Path::new(&old_name)
3636                    .extension()
3637                    .and_then(|e| e.to_str())
3638                    .map(|s| s.to_ascii_lowercase())
3639                    .filter(|e| e == "mid" || e == "midi")
3640                    .unwrap_or_else(|| "mid".to_string());
3641                format!("midi/{}.{}", new_name, ext)
3642            }
3643        };
3644        let _ = track;
3645
3646        for (_, other_track) in self.state.lock().tracks.iter() {
3647            let other_track = other_track.lock();
3648            match kind {
3649                Kind::Audio => {
3650                    for clip in &mut other_track.audio.clips {
3651                        if clip.name == old_name {
3652                            clip.name = new_file_name.clone();
3653                        }
3654                        if clip.pitch_correction_source_name.as_deref() == Some(old_name.as_str()) {
3655                            clip.pitch_correction_source_name = Some(new_file_name.clone());
3656                        }
3657                    }
3658                }
3659                Kind::MIDI => {
3660                    for clip in &mut other_track.midi.clips {
3661                        if clip.name == old_name {
3662                            clip.name = new_file_name.clone();
3663                        }
3664                    }
3665                }
3666            }
3667        }
3668    }
3669
3670    fn set_clip_fade(
3671        &self,
3672        track_name: &str,
3673        clip_index: usize,
3674        kind: Kind,
3675        fade_enabled: bool,
3676        fade_in_samples: usize,
3677        fade_out_samples: usize,
3678    ) {
3679        let Some(track) = self.state.lock().tracks.get(track_name) else {
3680            return;
3681        };
3682        let track = track.lock();
3683        match kind {
3684            Kind::Audio => {
3685                if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3686                    clip.fade_enabled = fade_enabled;
3687                    clip.fade_in_samples = fade_in_samples;
3688                    clip.fade_out_samples = fade_out_samples;
3689                }
3690            }
3691            Kind::MIDI => {}
3692        }
3693    }
3694
3695    fn set_clip_bounds(
3696        &self,
3697        track_name: &str,
3698        clip_index: usize,
3699        kind: Kind,
3700        start: usize,
3701        length: usize,
3702        offset: usize,
3703    ) {
3704        let Some(track) = self.state.lock().tracks.get(track_name) else {
3705            return;
3706        };
3707        let track = track.lock();
3708        match kind {
3709            Kind::Audio => {
3710                if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3711                    clip.start = start;
3712                    clip.end = start.saturating_add(length.max(1));
3713                    clip.offset = offset;
3714                    clip.pitch_correction_preview_name = None;
3715                    clip.pitch_correction_source_name = None;
3716                    clip.pitch_correction_source_offset = None;
3717                    clip.pitch_correction_source_length = None;
3718                    clip.pitch_correction_points.clear();
3719                    clip.pitch_correction_frame_likeness = None;
3720                    clip.pitch_correction_inertia_ms = None;
3721                    clip.pitch_correction_formant_compensation = None;
3722                }
3723                #[cfg(unix)]
3724                track.clip_pitch_shifters.clear();
3725            }
3726            Kind::MIDI => {
3727                if let Some(clip) = track.midi.clips.get_mut(clip_index) {
3728                    clip.start = start;
3729                    clip.end = start.saturating_add(length.max(1));
3730                    clip.offset = offset;
3731                }
3732            }
3733        }
3734    }
3735
3736    fn set_clip_source_name(&self, track_name: &str, clip_index: usize, kind: Kind, name: String) {
3737        let Some(track) = self.state.lock().tracks.get(track_name) else {
3738            return;
3739        };
3740        let track = track.lock();
3741        match kind {
3742            Kind::Audio => {
3743                if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3744                    clip.name = name;
3745                }
3746                #[cfg(unix)]
3747                track.clip_pitch_shifters.clear();
3748            }
3749            Kind::MIDI => {
3750                if let Some(clip) = track.midi.clips.get_mut(clip_index) {
3751                    clip.name = name;
3752                }
3753            }
3754        }
3755    }
3756
3757    fn set_clip_muted(&self, track_name: &str, clip_index: usize, kind: Kind, muted: bool) {
3758        let Some(track) = self.state.lock().tracks.get(track_name) else {
3759            return;
3760        };
3761        let track = track.lock();
3762        match kind {
3763            Kind::Audio => {
3764                if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3765                    clip.muted = muted;
3766                }
3767            }
3768            Kind::MIDI => {
3769                if let Some(clip) = track.midi.clips.get_mut(clip_index) {
3770                    clip.muted = muted;
3771                }
3772            }
3773        }
3774    }
3775
3776    #[allow(clippy::too_many_arguments)]
3777    fn set_clip_pitch_correction(
3778        &self,
3779        track_name: &str,
3780        clip_index: usize,
3781        preview_name: Option<String>,
3782        source_name: Option<String>,
3783        source_offset: Option<usize>,
3784        source_length: Option<usize>,
3785        pitch_correction_points: Vec<crate::message::PitchCorrectionPointData>,
3786        pitch_correction_frame_likeness: Option<f32>,
3787        pitch_correction_inertia_ms: Option<u16>,
3788        pitch_correction_formant_compensation: Option<bool>,
3789    ) {
3790        if let Some(track) = self.state.lock().tracks.get(track_name) {
3791            let track = track.lock();
3792            if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3793                clip.pitch_correction_preview_name = preview_name;
3794                clip.pitch_correction_source_name = source_name;
3795                clip.pitch_correction_source_offset = source_offset;
3796                clip.pitch_correction_source_length = source_length;
3797                clip.pitch_correction_points = pitch_correction_points;
3798                clip.pitch_correction_frame_likeness = pitch_correction_frame_likeness;
3799                clip.pitch_correction_inertia_ms = pitch_correction_inertia_ms;
3800                clip.pitch_correction_formant_compensation = pitch_correction_formant_compensation;
3801            }
3802            #[cfg(unix)]
3803            track.clip_pitch_shifters.clear();
3804        }
3805    }
3806
3807    async fn request_hw_cycle(&mut self) {
3808        if self.awaiting_hwfinished {
3809            tracing::debug!("request_hw_cycle skipped (already awaiting)");
3810            return;
3811        }
3812        tracing::debug!("request_hw_cycle sending TracksFinished");
3813        self.apply_hw_out_gain_and_meter().await;
3814        if let Some((after_frames, loop_start, cycle_end_sample)) =
3815            self.scheduled_loop_wrap_for_next_cycle()
3816        {
3817            self.notified_loop_wrap_sample = Some(cycle_end_sample);
3818            self.notify_clients(Ok(Action::TransportPositionAt {
3819                sample: loop_start,
3820                after_frames,
3821            }))
3822            .await;
3823        } else {
3824            self.notified_loop_wrap_sample = None;
3825        }
3826        if let Some(worker) = &self.hw_worker {
3827            if !self.pending_hw_midi_out_events_by_device.is_empty() {
3828                let out_events = std::mem::take(&mut self.pending_hw_midi_out_events_by_device);
3829                if let Err(e) = worker.tx.send(Message::HWMidiOutEvents(out_events)).await {
3830                    error!("Error sending HWMidiOutEvents {e}");
3831                }
3832            }
3833            match worker.tx.send(Message::TracksFinished).await {
3834                Ok(_) => {
3835                    self.awaiting_hwfinished = true;
3836                }
3837                Err(e) => {
3838                    error!("Error sending TracksFinished {e}");
3839                }
3840            }
3841        }
3842    }
3843
3844    async fn clear_hw_midi_output_state(&mut self, send_panic: bool) {
3845        self.pending_hw_midi_out_events.clear();
3846        self.pending_hw_midi_out_events_by_device.clear();
3847        {
3848            let state = self.state.lock();
3849            for track in state.tracks.values() {
3850                track.lock().take_hw_midi_out_events();
3851            }
3852        }
3853
3854        let panic_events = if send_panic {
3855            self.note_off_events_for_all_active_tracks()
3856        } else {
3857            vec![]
3858        };
3859
3860        if let Some(worker) = &self.hw_worker {
3861            if let Err(e) = worker.tx.send(Message::ClearHWMidiOutEvents).await {
3862                error!("Error clearing pending HWMidiOutEvents {e}");
3863            }
3864            if !panic_events.is_empty()
3865                && let Err(e) = worker.tx.send(Message::HWMidiOutEvents(panic_events)).await
3866            {
3867                error!("Error sending transport restart MIDI panic events {e}");
3868            }
3869        } else if !panic_events.is_empty() {
3870            self.pending_hw_midi_out_events_by_device
3871                .extend(panic_events);
3872        }
3873    }
3874
3875    fn invalidate_track_cycle_state(&mut self) {
3876        self.track_process_epoch = self.track_process_epoch.saturating_add(1);
3877        self.task_processing_started_at.clear();
3878        self.cycle_tasks.clear();
3879        self.cycle_task_deps.clear();
3880        self.cycle_tasks_running.clear();
3881        self.cycle_tasks_finished.clear();
3882        let state = self.state.lock();
3883        for track in state.tracks.values() {
3884            let t = track.lock();
3885            t.audio.finished = false;
3886            t.audio.processing = false;
3887        }
3888    }
3889
3890    fn force_stalled_task_completions(&mut self) {
3891        let now = Instant::now();
3892        let running: Vec<ProcessTask> = self.cycle_tasks_running.clone();
3893        for task in running {
3894            let key = Self::task_key(&task);
3895            let Some(started) = self.task_processing_started_at.get(&key).copied() else {
3896                continue;
3897            };
3898            if now.duration_since(started) < Self::TRACK_PROCESS_TIMEOUT {
3899                continue;
3900            }
3901            if Self::task_running_finished_contains(&self.cycle_tasks_finished, &task) {
3902                self.task_processing_started_at.remove(&key);
3903                continue;
3904            }
3905            let track = match &task {
3906                ProcessTask::Track(t)
3907                | ProcessTask::FolderInput(t)
3908                | ProcessTask::FolderOutput(t) => t.clone(),
3909                ProcessTask::Plugin { track, .. } => track.clone(),
3910            };
3911            {
3912                let t = track.lock();
3913                if t.audio.finished || !t.audio.processing {
3914                    self.task_processing_started_at.remove(&key);
3915                    continue;
3916                }
3917                for out in &t.audio.outs {
3918                    out.buffer.lock().fill(0.0);
3919                    *out.finished.lock() = true;
3920                }
3921                t.audio.processing = false;
3922                t.audio.finished = true;
3923            }
3924            self.cycle_tasks_running
3925                .retain(|t| Self::task_key(t) != key);
3926            self.cycle_tasks_finished.push(task.clone());
3927            self.task_processing_started_at.remove(&key);
3928            tracing::warn!(
3929                "Task '{}' exceeded process timeout ({} ms); forcing silent completion for cycle",
3930                Self::task_track_name(&task),
3931                Self::TRACK_PROCESS_TIMEOUT.as_millis()
3932            );
3933        }
3934    }
3935
3936    fn should_publish_hw_out_meters(&mut self) -> bool {
3937        let now = Instant::now();
3938        match self.last_hw_out_meter_publish {
3939            Some(last) if now.duration_since(last) < Self::METER_PUBLISH_INTERVAL => false,
3940            _ => {
3941                self.last_hw_out_meter_publish = Some(now);
3942                true
3943            }
3944        }
3945    }
3946
3947    fn should_publish_track_meters(&mut self) -> bool {
3948        let now = Instant::now();
3949        match self.last_track_meter_publish {
3950            Some(last) if now.duration_since(last) < Self::METER_PUBLISH_INTERVAL => false,
3951            _ => {
3952                self.last_track_meter_publish = Some(now);
3953                true
3954            }
3955        }
3956    }
3957
3958    fn should_publish_hw_out_linear(&mut self, peaks_linear: &[f32]) -> bool {
3959        #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
3960        {
3961            self.hw_out_meter_publish_phase = !self.hw_out_meter_publish_phase;
3962            if !self.hw_out_meter_publish_phase {
3963                return false;
3964            }
3965            let changed = if self.last_hw_out_meter_linear.len() != peaks_linear.len() {
3966                true
3967            } else {
3968                self.last_hw_out_meter_linear
3969                    .iter()
3970                    .zip(peaks_linear.iter())
3971                    .any(|(prev, next)| (prev - next).abs() >= Self::HW_OUT_METER_LINEAR_EPSILON)
3972            };
3973            if !changed {
3974                return false;
3975            }
3976            self.last_hw_out_meter_linear.clear();
3977            self.last_hw_out_meter_linear
3978                .extend_from_slice(peaks_linear);
3979            true
3980        }
3981        #[cfg(not(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd")))]
3982        {
3983            let _ = peaks_linear;
3984            false
3985        }
3986    }
3987
3988    async fn maybe_notify_hw_out_meter(&mut self, _meter_db: Vec<f32>) {
3989        {}
3990    }
3991
3992    fn collect_changed_track_meters(
3993        &mut self,
3994        _tracks: &[(String, Arc<UnsafeMutex<Box<Track>>>)],
3995    ) -> Vec<(String, Vec<f32>)> {
3996        Vec::new()
3997    }
3998
3999    async fn apply_hw_out_gain_and_meter(&mut self) {
4000        let gain = if self.hw_out_muted {
4001            0.0
4002        } else {
4003            10.0_f32.powf(self.hw_out_level_db / 20.0)
4004        };
4005        let should_notify_interval = self.should_publish_hw_out_meters();
4006        if let Some(oss) = self.hw_driver.clone() {
4007            let hw = oss.lock();
4008            hw.set_output_gain_balance(gain, self.hw_out_balance);
4009            if !should_notify_interval {
4010                return;
4011            }
4012        } else {
4013            #[cfg(unix)]
4014            {
4015                if let Some(jack) = self.jack_runtime.clone() {
4016                    jack.lock().set_output_gain_linear(gain);
4017                    jack.lock().set_output_balance(self.hw_out_balance);
4018                    if !should_notify_interval {
4019                        return;
4020                    }
4021                } else {
4022                    return;
4023                }
4024            }
4025            #[cfg(not(unix))]
4026            {
4027                return;
4028            }
4029        }
4030        let peaks_linear = if let Some(oss) = self.hw_driver.clone() {
4031            oss.lock().output_meter_linear(gain, self.hw_out_balance)
4032        } else {
4033            #[cfg(unix)]
4034            {
4035                if let Some(jack) = self.jack_runtime.clone() {
4036                    let outs = jack.lock().audio_outs();
4037                    let out_count = outs.len();
4038                    let b = if out_count == 2 {
4039                        self.hw_out_balance.clamp(-1.0, 1.0)
4040                    } else {
4041                        0.0
4042                    };
4043                    let mut meters_linear = Vec::with_capacity(out_count);
4044                    for (channel_idx, channel) in outs.iter().enumerate() {
4045                        let balance_gain = if out_count == 2 {
4046                            if channel_idx == 0 {
4047                                (1.0 - b).clamp(0.0, 1.0)
4048                            } else {
4049                                (1.0 + b).clamp(0.0, 1.0)
4050                            }
4051                        } else {
4052                            1.0
4053                        };
4054                        let buf = channel.buffer.lock();
4055                        let peak = crate::simd::peak_abs(buf) * gain * balance_gain;
4056                        meters_linear.push(peak);
4057                    }
4058                    meters_linear
4059                } else {
4060                    return;
4061                }
4062            }
4063            #[cfg(not(unix))]
4064            {
4065                return;
4066            }
4067        };
4068        if self.hw_out_peak_hold_linear.len() != peaks_linear.len() {
4069            self.hw_out_peak_hold_linear.resize(peaks_linear.len(), 0.0);
4070        }
4071        let mut held_peaks = Vec::with_capacity(peaks_linear.len());
4072        for (idx, peak_now) in peaks_linear.iter().copied().enumerate() {
4073            let held = self.hw_out_peak_hold_linear[idx] * 0.92;
4074            let next = peak_now.max(held);
4075            self.hw_out_peak_hold_linear[idx] = next;
4076            held_peaks.push(next);
4077        }
4078        let should_notify =
4079            should_notify_interval && self.should_publish_hw_out_linear(&held_peaks);
4080        let meter_db: Vec<f32> = held_peaks
4081            .into_iter()
4082            .map(Self::meter_linear_to_db)
4083            .collect();
4084        self.latest_hw_out_meter_db = Arc::new(meter_db.clone());
4085        if should_notify {
4086            self.maybe_notify_hw_out_meter(meter_db).await;
4087        }
4088    }
4089
4090    fn preload_track_clips_spawn(&self) {
4091        let tracks: Vec<_> = self.state.lock().tracks.values().cloned().collect();
4092        for track in tracks {
4093            tokio::task::spawn_blocking(move || {
4094                track.lock().preload_clips();
4095            });
4096        }
4097    }
4098
4099    async fn preload_track_clips(&self) {
4100        let tracks: Vec<_> = self.state.lock().tracks.values().cloned().collect();
4101        if tracks.is_empty() {
4102            return;
4103        }
4104        let mut handles = Vec::with_capacity(tracks.len());
4105        for track in tracks {
4106            handles.push(tokio::task::spawn_blocking(move || {
4107                track.lock().preload_clips();
4108            }));
4109        }
4110        for handle in handles {
4111            if let Err(e) = handle.await {
4112                tracing::warn!("Clip preload task panicked: {e}");
4113            }
4114        }
4115    }
4116
4117    fn build_task_graph(
4118        &self,
4119    ) -> (
4120        Vec<ProcessTask>,
4121        std::collections::HashMap<String, Vec<String>>,
4122    ) {
4123        let state = self.state.lock();
4124        let ordered: Vec<(String, Arc<UnsafeMutex<Box<Track>>>)> = state
4125            .tracks
4126            .iter()
4127            .map(|(name, track)| (name.clone(), track.clone()))
4128            .collect();
4129        let mut tasks = Vec::new();
4130        let mut deps = std::collections::HashMap::new();
4131
4132        for (_name, track) in &ordered {
4133            let t = track.lock();
4134            if t.parent_track.is_some() {
4135                continue;
4136            }
4137            self.append_track_tasks(track.clone(), None, &mut tasks, &mut deps);
4138        }
4139
4140        (tasks, deps)
4141    }
4142
4143    fn append_track_tasks(
4144        &self,
4145        track: Arc<UnsafeMutex<Box<Track>>>,
4146        predecessor: Option<String>,
4147        tasks: &mut Vec<ProcessTask>,
4148        deps: &mut std::collections::HashMap<String, Vec<String>>,
4149    ) -> (String, String) {
4150        use crate::message::ConnectableRef;
4151        let t = track.lock();
4152        if t.is_folder {
4153            let folder_input = ProcessTask::FolderInput(track.clone());
4154            let folder_input_key = Self::task_key(&folder_input);
4155            tasks.push(folder_input.clone());
4156            let folder_input_deps: Vec<_> = predecessor.into_iter().collect();
4157            deps.insert(folder_input_key.clone(), folder_input_deps);
4158
4159            let mut source_keys: std::collections::HashMap<ConnectableRef, String> =
4160                std::collections::HashMap::new();
4161            let mut target_keys: std::collections::HashMap<ConnectableRef, String> =
4162                std::collections::HashMap::new();
4163            source_keys.insert(ConnectableRef::TrackInput, folder_input_key.clone());
4164            target_keys.insert(ConnectableRef::TrackInput, folder_input_key.clone());
4165
4166            let mut plugin_keys: Vec<String> = Vec::new();
4167            for idx in 0..t.clap_plugins.len() {
4168                let plugin_task = ProcessTask::Plugin {
4169                    track: track.clone(),
4170                    kind: PluginKind::Clap,
4171                    index: idx,
4172                };
4173                let plugin_key = Self::task_key(&plugin_task);
4174                let id = t.clap_plugins[idx].id;
4175                source_keys.insert(ConnectableRef::ClapPlugin(id), plugin_key.clone());
4176                target_keys.insert(ConnectableRef::ClapPlugin(id), plugin_key.clone());
4177                tasks.push(plugin_task);
4178                deps.insert(plugin_key.clone(), vec![folder_input_key.clone()]);
4179                plugin_keys.push(plugin_key);
4180            }
4181            for idx in 0..t.vst3_plugins.len() {
4182                let plugin_task = ProcessTask::Plugin {
4183                    track: track.clone(),
4184                    kind: PluginKind::Vst3,
4185                    index: idx,
4186                };
4187                let plugin_key = Self::task_key(&plugin_task);
4188                let id = t.vst3_plugins[idx].id;
4189                source_keys.insert(ConnectableRef::Vst3Plugin(id), plugin_key.clone());
4190                target_keys.insert(ConnectableRef::Vst3Plugin(id), plugin_key.clone());
4191                tasks.push(plugin_task);
4192                deps.insert(plugin_key.clone(), vec![folder_input_key.clone()]);
4193                plugin_keys.push(plugin_key);
4194            }
4195            #[cfg(all(unix, not(target_os = "macos")))]
4196            for idx in 0..t.lv2_plugins.len() {
4197                let plugin_task = ProcessTask::Plugin {
4198                    track: track.clone(),
4199                    kind: PluginKind::Lv2,
4200                    index: idx,
4201                };
4202                let plugin_key = Self::task_key(&plugin_task);
4203                let id = t.lv2_plugins[idx].id;
4204                source_keys.insert(ConnectableRef::Lv2Plugin(id), plugin_key.clone());
4205                target_keys.insert(ConnectableRef::Lv2Plugin(id), plugin_key.clone());
4206                tasks.push(plugin_task);
4207                deps.insert(plugin_key.clone(), vec![folder_input_key.clone()]);
4208                plugin_keys.push(plugin_key);
4209            }
4210
4211            let mut child_keys = Vec::new();
4212            for child_track in &t.child_tracks {
4213                let (child_first, child_last) = self.append_track_tasks(
4214                    child_track.clone(),
4215                    Some(folder_input_key.clone()),
4216                    tasks,
4217                    deps,
4218                );
4219                let child_name = child_track.lock().name.clone();
4220                source_keys.insert(
4221                    ConnectableRef::ChildTrack(child_name.clone()),
4222                    child_last.clone(),
4223                );
4224                target_keys.insert(ConnectableRef::ChildTrack(child_name), child_first.clone());
4225                child_keys.push((child_first, child_last.clone()));
4226            }
4227
4228            let folder_output = ProcessTask::FolderOutput(track.clone());
4229            let folder_output_key = Self::task_key(&folder_output);
4230            source_keys.insert(ConnectableRef::TrackOutput, folder_output_key.clone());
4231            target_keys.insert(ConnectableRef::TrackOutput, folder_output_key.clone());
4232            tasks.push(folder_output.clone());
4233            let mut folder_output_deps = vec![folder_input_key.clone()];
4234            folder_output_deps.extend(plugin_keys);
4235            folder_output_deps.extend(child_keys.iter().map(|(_, last)| last.clone()));
4236            deps.insert(folder_output_key.clone(), folder_output_deps);
4237
4238            // Add cross-connectable dependencies based on the track's routing graph.
4239            // This includes child->plugin, plugin->folder output, plugin->plugin, etc.
4240            for conn in t.connectable_connections() {
4241                let Some(source_key) = source_keys.get(&conn.from) else {
4242                    continue;
4243                };
4244                let Some(target_key) = target_keys.get(&conn.to) else {
4245                    continue;
4246                };
4247                if source_key == target_key {
4248                    continue;
4249                }
4250                let entry = deps.entry(target_key.clone()).or_default();
4251                if !entry.contains(source_key) {
4252                    entry.push(source_key.clone());
4253                }
4254            }
4255
4256            (folder_input_key, folder_output_key)
4257        } else {
4258            let task = ProcessTask::Track(track.clone());
4259            let task_key = Self::task_key(&task);
4260            tasks.push(task.clone());
4261            deps.insert(
4262                task_key.clone(),
4263                predecessor.into_iter().collect::<Vec<_>>(),
4264            );
4265            (task_key.clone(), task_key)
4266        }
4267    }
4268
4269    fn task_track_name(task: &ProcessTask) -> String {
4270        match task {
4271            ProcessTask::Track(t) | ProcessTask::FolderInput(t) | ProcessTask::FolderOutput(t) => {
4272                t.lock().name.clone()
4273            }
4274            ProcessTask::Plugin { track, .. } => track.lock().name.clone(),
4275        }
4276    }
4277
4278    fn task_key(task: &ProcessTask) -> String {
4279        match task {
4280            ProcessTask::Track(t) => format!("Track:{:p}", std::sync::Arc::as_ptr(t)),
4281            ProcessTask::FolderInput(t) => {
4282                format!("FolderInput:{:p}", std::sync::Arc::as_ptr(t))
4283            }
4284            ProcessTask::FolderOutput(t) => {
4285                format!("FolderOutput:{:p}", std::sync::Arc::as_ptr(t))
4286            }
4287            ProcessTask::Plugin { track, kind, index } => format!(
4288                "Plugin:{:?}:{:p}:{}",
4289                kind,
4290                std::sync::Arc::as_ptr(track),
4291                index
4292            ),
4293        }
4294    }
4295
4296    fn task_running_finished_contains(haystack: &[ProcessTask], needle: &ProcessTask) -> bool {
4297        let needle_key = Self::task_key(needle);
4298        haystack.iter().any(|t| Self::task_key(t) == needle_key)
4299    }
4300
4301    fn task_ready(&self, task: &ProcessTask) -> bool {
4302        match task {
4303            ProcessTask::Track(t) | ProcessTask::FolderInput(t) => {
4304                let track = t.lock();
4305                track.audio.ready()
4306            }
4307            ProcessTask::Plugin { .. } | ProcessTask::FolderOutput(_) => true,
4308        }
4309    }
4310
4311    fn task_dependencies_satisfied(&self, task: &ProcessTask) -> bool {
4312        let key = Self::task_key(task);
4313        let Some(deps) = self.cycle_task_deps.get(&key) else {
4314            return true;
4315        };
4316        let finished_keys: std::collections::HashSet<String> = self
4317            .cycle_tasks_finished
4318            .iter()
4319            .map(Self::task_key)
4320            .collect();
4321        deps.iter().all(|d| finished_keys.contains(d))
4322    }
4323
4324    fn prepare_task_track(&self, task: &ProcessTask) {
4325        let track = match task {
4326            ProcessTask::Track(t) | ProcessTask::FolderInput(t) | ProcessTask::FolderOutput(t) => t,
4327            ProcessTask::Plugin { track, .. } => track,
4328        };
4329        let t = track.lock();
4330        t.set_transport_sample(self.transport_sample);
4331        t.set_loop_config(self.loop_enabled, self.loop_range_samples);
4332        t.set_transport_timing(self.tempo_bpm, self.tsig_num, self.tsig_denom);
4333        t.process_epoch = self.track_process_epoch;
4334        t.set_clip_playback_enabled(self.clip_playback_enabled && self.playing);
4335        t.set_record_tap_enabled(self.playing && self.record_enabled);
4336        t.audio.processing = true;
4337    }
4338
4339    async fn send_tasks(&mut self) -> bool {
4340        if !self.playing {
4341            return false;
4342        }
4343        self.refresh_realtime_infection();
4344        self.force_stalled_task_completions();
4345
4346        if self.cycle_tasks.is_empty() {
4347            let (tasks, deps) = self.build_task_graph();
4348            let task_names: Vec<String> = tasks.iter().map(Self::task_track_name).collect();
4349            tracing::debug!(
4350                "send_tasks rebuilt graph: {} tasks ({:?})",
4351                tasks.len(),
4352                task_names
4353            );
4354            self.cycle_tasks = tasks;
4355            self.cycle_task_deps = deps;
4356            self.cycle_tasks_running.clear();
4357            self.cycle_tasks_finished.clear();
4358        }
4359
4360        let mut finished = true;
4361        let mut dispatched = 0;
4362        loop {
4363            let next_task = {
4364                let mut next = None;
4365                tracing::debug!(
4366                    "selecting next: cycle={} running={} finished={}",
4367                    self.cycle_tasks.len(),
4368                    self.cycle_tasks_running.len(),
4369                    self.cycle_tasks_finished.len()
4370                );
4371                for task in &self.cycle_tasks {
4372                    let in_running =
4373                        Self::task_running_finished_contains(&self.cycle_tasks_running, task);
4374                    let in_finished =
4375                        Self::task_running_finished_contains(&self.cycle_tasks_finished, task);
4376                    tracing::debug!(
4377                        "checking task {} in_running={} in_finished={}",
4378                        Self::task_track_name(task),
4379                        in_running,
4380                        in_finished
4381                    );
4382                    if in_finished || in_running {
4383                        continue;
4384                    }
4385                    finished = false;
4386                    if !self.task_dependencies_satisfied(task) {
4387                        continue;
4388                    }
4389                    if !self.task_ready(task) {
4390                        continue;
4391                    }
4392                    next = Some(task.clone());
4393                    break;
4394                }
4395                next
4396            };
4397
4398            let Some(task) = next_task else {
4399                tracing::debug!(
4400                    "send_tasks returning finished={} (dispatched {})",
4401                    finished,
4402                    dispatched
4403                );
4404                return finished;
4405            };
4406            let Some(worker_index) = self.take_ready_worker_index() else {
4407                self.force_stalled_task_completions();
4408                tracing::debug!(
4409                    "send_tasks returning false (no ready worker; dispatched {})",
4410                    dispatched
4411                );
4412                return false;
4413            };
4414
4415            if Self::task_running_finished_contains(&self.cycle_tasks_finished, &task)
4416                || Self::task_running_finished_contains(&self.cycle_tasks_running, &task)
4417            {
4418                continue;
4419            }
4420            dispatched += 1;
4421            let task_key = Self::task_key(&task);
4422            tracing::debug!(
4423                "send_tasks dispatching {} (running={} finished={})",
4424                Self::task_track_name(&task),
4425                self.cycle_tasks_running.len(),
4426                self.cycle_tasks_finished.len()
4427            );
4428            self.prepare_task_track(&task);
4429            self.cycle_tasks_running.push(task.clone());
4430            tracing::debug!(
4431                "inserted task {} -> running_size={}",
4432                Self::task_track_name(&task),
4433                self.cycle_tasks_running.len()
4434            );
4435            self.task_processing_started_at
4436                .insert(task_key.clone(), Instant::now());
4437            let worker = &self.workers[worker_index];
4438            if let Err(e) = worker.tx.send(Message::ProcessTask(task.clone())).await {
4439                self.cycle_tasks_running
4440                    .retain(|t| Self::task_key(t) != task_key);
4441                self.task_processing_started_at.remove(&task_key);
4442                self.notify_clients(Err(format!("Failed to send task to worker: {}", e)))
4443                    .await;
4444            }
4445        }
4446    }
4447
4448    async fn on_all_tracks_finished(&mut self) {
4449        if self.transport_restart_pending {
4450            let state = self.state.lock();
4451            for track in state.tracks.values() {
4452                track.lock().take_hw_midi_out_events();
4453            }
4454        } else if self.hw_worker.is_some() {
4455            self.active_hw_notes_cycle_start = self.active_hw_notes_by_track.clone();
4456            let mut out_events = self.collect_hw_midi_output_events_by_device();
4457            if self.loop_enabled
4458                && let Some((_, loop_end)) = self.loop_range_samples
4459            {
4460                let cycle_end = self
4461                    .transport_sample
4462                    .saturating_add(self.current_cycle_samples());
4463                if self.transport_sample < loop_end && cycle_end >= loop_end {
4464                    let wrap_frame = loop_end
4465                        .saturating_sub(self.transport_sample)
4466                        .min(self.current_cycle_samples())
4467                        as u32;
4468                    out_events.extend(self.note_off_events_for_active_snapshot(
4469                        &self.active_hw_notes_cycle_start,
4470                        wrap_frame,
4471                    ));
4472                    out_events.sort_by(|a, b| {
4473                        a.event
4474                            .frame
4475                            .cmp(&b.event.frame)
4476                            .then_with(|| a.device.cmp(&b.device))
4477                    });
4478                }
4479            }
4480            self.pending_hw_midi_out_events_by_device.extend(out_events);
4481        } else {
4482            self.pending_hw_midi_out_events = self.collect_hw_midi_output_events();
4483        }
4484        self.request_hw_cycle().await;
4485    }
4486
4487    fn take_ready_worker_index(&mut self) -> Option<usize> {
4488        while !self.ready_workers.is_empty() {
4489            let worker_index = self.ready_workers.remove(0);
4490            if worker_index < self.workers.len() {
4491                return Some(worker_index);
4492            }
4493        }
4494        None
4495    }
4496
4497    fn push_ready_worker(&mut self, worker_index: usize) {
4498        self.ready_workers.push(worker_index);
4499    }
4500
4501    async fn publish_track_meters(&mut self) {
4502        if !self.should_publish_track_meters() {
4503            return;
4504        }
4505        let tracks: Vec<(String, Arc<UnsafeMutex<Box<Track>>>)> = self
4506            .state
4507            .lock()
4508            .tracks
4509            .iter()
4510            .map(|(name, track)| (name.clone(), track.clone()))
4511            .collect();
4512        let mut snapshot = Vec::with_capacity(tracks.len());
4513        for (name, track) in &tracks {
4514            let linear = self
4515                .track_meter_linear_by_track
4516                .get(name)
4517                .cloned()
4518                .unwrap_or_else(|| track.lock().output_meter_linear());
4519            let output_db = linear
4520                .iter()
4521                .copied()
4522                .map(Self::meter_linear_to_db)
4523                .collect::<Vec<_>>();
4524            snapshot.push((name.clone(), output_db));
4525        }
4526        self.latest_track_meter_snapshot = Arc::new(snapshot);
4527        let meters = self.collect_changed_track_meters(&tracks);
4528        for (track_name, output_db) in meters {
4529            self.notify_clients(Ok(Action::TrackMeters {
4530                track_name,
4531                output_db,
4532            }))
4533            .await;
4534        }
4535    }
4536
4537    async fn publish_clap_state_dirty(&mut self) {
4538        let tracks: Vec<(String, Arc<UnsafeMutex<Box<Track>>>)> = self
4539            .state
4540            .lock()
4541            .tracks
4542            .iter()
4543            .map(|(name, track)| (name.clone(), track.clone()))
4544            .collect();
4545        for (track_name, track) in &tracks {
4546            let dirty = track.lock().take_dirty_clap_instances();
4547            for instance_id in dirty {
4548                self.notify_clients(Ok(Action::TrackClapStateDirty {
4549                    track_name: track_name.clone(),
4550                    instance_id,
4551                }))
4552                .await;
4553            }
4554        }
4555    }
4556
4557    fn reset_meters_after_stop(&mut self) {
4558        self.last_hw_out_meter_publish = None;
4559        self.last_track_meter_publish = None;
4560        self.hw_out_peak_hold_linear.fill(0.0);
4561        #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
4562        {
4563            self.last_hw_out_meter_linear.clear();
4564        }
4565        let hw_channels = self.latest_hw_out_meter_db.len();
4566        self.latest_hw_out_meter_db = Arc::new(vec![-90.0; hw_channels]);
4567
4568        let tracks: Vec<(String, Arc<UnsafeMutex<Box<Track>>>)> = self
4569            .state
4570            .lock()
4571            .tracks
4572            .iter()
4573            .map(|(name, track)| (name.clone(), track.clone()))
4574            .collect();
4575        self.track_meter_linear_by_track.clear();
4576        let mut snapshot = Vec::with_capacity(tracks.len());
4577        for (name, track) in tracks {
4578            let t = track.lock();
4579            t.clear_output_meters();
4580            let width = t.output_meter_linear().len();
4581            let zero_linear = vec![0.0; width];
4582            self.track_meter_linear_by_track
4583                .insert(name.clone(), zero_linear);
4584            snapshot.push((name, vec![-90.0; width]));
4585        }
4586        self.latest_track_meter_snapshot = Arc::new(snapshot);
4587    }
4588
4589    pub fn check_if_leads_to_kind(
4590        &self,
4591        kind: Kind,
4592        current_track_name: &str,
4593        target_track_name: &str,
4594    ) -> bool {
4595        routing::would_create_cycle(
4596            &target_track_name.to_string(),
4597            &current_track_name.to_string(),
4598            |track_name| self.connected_neighbors(kind, track_name),
4599        )
4600    }
4601
4602    fn connected_neighbors(&self, kind: Kind, current_track_name: &str) -> Vec<String> {
4603        let state = self.state.lock();
4604        let mut found_neighbors = Vec::new();
4605
4606        if let Some(current_track_handle) = state.tracks.get(current_track_name) {
4607            let current_track = current_track_handle.lock();
4608
4609            match kind {
4610                Kind::Audio => {
4611                    for out_port in &current_track.audio.outs {
4612                        let conns = out_port.connections.lock();
4613                        for conn in conns.iter() {
4614                            for (name, next_track_handle) in &state.tracks {
4615                                let next_track = next_track_handle.lock();
4616                                let is_connected =
4617                                    next_track.audio.ins.iter().any(|ins_port| {
4618                                        Arc::ptr_eq(&ins_port.buffer, &conn.buffer)
4619                                    });
4620
4621                                if is_connected {
4622                                    found_neighbors.push(name.clone());
4623                                }
4624                            }
4625                        }
4626                    }
4627                }
4628                Kind::MIDI => {
4629                    for out_port in &current_track.midi.outs {
4630                        let conns = out_port.lock().connections.clone();
4631                        for conn in conns.iter() {
4632                            for (name, next_track_handle) in &state.tracks {
4633                                let next_track = next_track_handle.lock();
4634                                let is_connected = next_track
4635                                    .midi
4636                                    .ins
4637                                    .iter()
4638                                    .any(|ins_port| Arc::ptr_eq(ins_port, conn));
4639
4640                                if is_connected {
4641                                    found_neighbors.push(name.clone());
4642                                }
4643                            }
4644                        }
4645                    }
4646                }
4647            }
4648        }
4649        found_neighbors
4650    }
4651
4652    async fn handle_request(&mut self, a: Action) {
4653        match a {
4654            Action::Log { source, message } => {
4655                self.notify_clients(Ok(Action::Log { source, message }))
4656                    .await;
4657            }
4658            Action::Undo => {
4659                let actions = match self.history.undo() {
4660                    Some(actions) => actions,
4661                    None => {
4662                        self.notify_clients(Ok(Action::Undo)).await;
4663                        self.notify_clients(Ok(Action::HistoryState {
4664                            dirty: self.history.is_dirty(),
4665                        }))
4666                        .await;
4667                        return;
4668                    }
4669                };
4670
4671                let was_suspended = self.history_suspended;
4672                self.history_suspended = true;
4673                for action in actions {
4674                    self.handle_request_inner(action, false).await;
4675                }
4676                self.history_suspended = was_suspended;
4677                self.notify_clients(Ok(Action::Undo)).await;
4678                self.notify_clients(Ok(Action::HistoryState {
4679                    dirty: self.history.is_dirty(),
4680                }))
4681                .await;
4682            }
4683            Action::Redo => {
4684                let actions = match self.history.redo() {
4685                    Some(actions) => actions,
4686                    None => {
4687                        self.notify_clients(Ok(Action::Redo)).await;
4688                        self.notify_clients(Ok(Action::HistoryState {
4689                            dirty: self.history.is_dirty(),
4690                        }))
4691                        .await;
4692                        return;
4693                    }
4694                };
4695
4696                let was_suspended = self.history_suspended;
4697                self.history_suspended = true;
4698                for action in actions {
4699                    self.handle_request_inner(action, false).await;
4700                }
4701                self.history_suspended = was_suspended;
4702                self.notify_clients(Ok(Action::Redo)).await;
4703                self.notify_clients(Ok(Action::HistoryState {
4704                    dirty: self.history.is_dirty(),
4705                }))
4706                .await;
4707            }
4708            Action::ApplyGroupedActions(actions) => {
4709                self.handle_request_inner(Action::BeginHistoryGroup, true)
4710                    .await;
4711                for action in actions {
4712                    self.handle_request_inner(action, true).await;
4713                }
4714                self.handle_request_inner(Action::EndHistoryGroup, true)
4715                    .await;
4716            }
4717            other => {
4718                self.handle_request_inner(other, true).await;
4719            }
4720        }
4721    }
4722
4723    fn find_audio_io_owner(
4724        &self,
4725        state: &crate::state::State,
4726        io: &std::sync::Arc<crate::audio::io::AudioIO>,
4727    ) -> Option<(String, usize)> {
4728        for (name, track) in &state.tracks {
4729            let t = track.lock();
4730            for (i, out) in t.audio.outs.iter().enumerate() {
4731                if std::sync::Arc::ptr_eq(out, io) {
4732                    return Some((name.clone(), i));
4733                }
4734            }
4735            for (i, inp) in t.audio.ins.iter().enumerate() {
4736                if std::sync::Arc::ptr_eq(inp, io) {
4737                    return Some((name.clone(), i));
4738                }
4739            }
4740        }
4741        None
4742    }
4743
4744    fn find_midi_io_owner(
4745        &self,
4746        state: &crate::state::State,
4747        io: &std::sync::Arc<crate::mutex::UnsafeMutex<Box<crate::midi::io::MIDIIO>>>,
4748    ) -> Option<(String, usize, bool)> {
4749        for (name, track) in &state.tracks {
4750            let t = track.lock();
4751            for (i, out) in t.midi.outs.iter().enumerate() {
4752                if std::sync::Arc::ptr_eq(out, io) {
4753                    return Some((name.clone(), i, false));
4754                }
4755            }
4756            for (i, inp) in t.midi.ins.iter().enumerate() {
4757                if std::sync::Arc::ptr_eq(inp, io) {
4758                    return Some((name.clone(), i, true));
4759                }
4760            }
4761        }
4762        None
4763    }
4764
4765    fn collect_descendant_track_names(&self, name: &str, out: &mut Vec<String>) {
4766        // Clone the child arcs while briefly holding the parent lock, then release it before
4767        // recursing so we never nest locks on the same thread.
4768        let child_arcs: Vec<Arc<UnsafeMutex<Box<Track>>>> = {
4769            let state = self.state.lock();
4770            if let Some(track) = state.tracks.get(name) {
4771                track.lock().child_tracks.clone()
4772            } else {
4773                Vec::new()
4774            }
4775        };
4776        for child in child_arcs {
4777            let child_name = { child.lock().name.clone() };
4778            self.collect_descendant_track_names(&child_name, out);
4779            out.push(child_name);
4780        }
4781    }
4782
4783    async fn remove_single_track(&mut self, name: &str) {
4784        let children: Vec<Arc<UnsafeMutex<Box<Track>>>> = {
4785            let state = self.state.lock();
4786            if let Some(removed) = state.tracks.get(name).cloned() {
4787                removed.lock().child_tracks.clone()
4788            } else {
4789                Vec::new()
4790            }
4791        };
4792        let parent_name: Option<String> = {
4793            let state = self.state.lock();
4794            state
4795                .tracks
4796                .get(name)
4797                .map(|t| t.lock().parent_track.clone())
4798                .unwrap_or(None)
4799        };
4800        if let Some(parent_name) = parent_name {
4801            let state = self.state.lock();
4802            if let Some(parent) = state.tracks.get(&parent_name).cloned() {
4803                let parent = parent.lock();
4804                parent.child_tracks.retain(|c| c.lock().name != *name);
4805            }
4806        }
4807        if let Some(removed_track) = self.state.lock().tracks.get(name).cloned() {
4808            for child in children {
4809                let removed = removed_track.lock();
4810                child.lock().disconnect_from_parent(removed);
4811                child.lock().parent_track = None;
4812            }
4813        }
4814        self.state.lock().tracks.remove(name);
4815        self.audio_recordings.remove(name);
4816        self.midi_recordings.remove(name);
4817        self.midi_hw_in_routes.retain(|r| r.to_track != *name);
4818        self.midi_hw_out_routes.retain(|r| r.from_track != *name);
4819        if self
4820            .pending_midi_learn
4821            .as_ref()
4822            .is_some_and(|(track_name, _, _)| track_name == name)
4823        {
4824            self.pending_midi_learn = None;
4825        }
4826    }
4827
4828    async fn handle_request_inner(&mut self, mut action_to_process: Action, record_history: bool) {
4829        let a = action_to_process.clone();
4830        let suppress_timing_history = self.playing
4831            && matches!(
4832                &action_to_process,
4833                Action::SetTempo(_) | Action::SetTimeSignature { .. }
4834            );
4835        let mut extra_inverse_actions: Vec<Action> = Vec::new();
4836        if record_history
4837            && !self.history_suspended
4838            && let Action::RemoveTrack(ref track_name) = action_to_process
4839        {
4840            for route in self
4841                .midi_hw_in_routes
4842                .iter()
4843                .filter(|route| &route.to_track == track_name)
4844            {
4845                extra_inverse_actions.push(Action::Connect {
4846                    from_track: format!("midi:hw:in:{}", route.device),
4847                    from_port: 0,
4848                    to_track: route.to_track.clone(),
4849                    to_port: route.to_port,
4850                    kind: Kind::MIDI,
4851                });
4852            }
4853            for route in self
4854                .midi_hw_out_routes
4855                .iter()
4856                .filter(|route| &route.from_track == track_name)
4857            {
4858                extra_inverse_actions.push(Action::Connect {
4859                    from_track: route.from_track.clone(),
4860                    from_port: route.from_port,
4861                    to_track: format!("midi:hw:out:{}", route.device),
4862                    to_port: 0,
4863                    kind: Kind::MIDI,
4864                });
4865            }
4866        }
4867        if record_history
4868            && !self.history_suspended
4869            && matches!(action_to_process, Action::ClearAllMidiLearnBindings)
4870        {
4871            if let Some(binding) = self.global_midi_learn_play_pause.clone() {
4872                extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
4873                    target: crate::message::GlobalMidiLearnTarget::PlayPause,
4874                    binding: Some(binding),
4875                });
4876            }
4877            if let Some(binding) = self.global_midi_learn_stop.clone() {
4878                extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
4879                    target: crate::message::GlobalMidiLearnTarget::Stop,
4880                    binding: Some(binding),
4881                });
4882            }
4883            if let Some(binding) = self.global_midi_learn_record_toggle.clone() {
4884                extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
4885                    target: crate::message::GlobalMidiLearnTarget::RecordToggle,
4886                    binding: Some(binding),
4887                });
4888            }
4889        }
4890        let mut inverse_actions = if record_history
4891            && !suppress_timing_history
4892            && should_record(&action_to_process)
4893            && !self.history_suspended
4894        {
4895            let state = self.state.lock();
4896            create_inverse_actions(&action_to_process, state).map(|mut actions| {
4897                actions.extend(extra_inverse_actions);
4898                actions
4899            })
4900        } else {
4901            None
4902        };
4903        if record_history && !suppress_timing_history && !self.history_suspended {
4904            match &action_to_process {
4905                Action::SetTempo(_) => {
4906                    inverse_actions = Some(vec![Action::SetTempo(self.tempo_bpm)]);
4907                }
4908                Action::SetLoopEnabled(_) => {
4909                    inverse_actions = Some(vec![Action::SetLoopEnabled(self.loop_enabled)]);
4910                }
4911                Action::SetLoopRange(_) => {
4912                    inverse_actions = Some(vec![
4913                        Action::SetLoopRange(self.loop_range_samples),
4914                        Action::SetLoopEnabled(self.loop_enabled),
4915                    ]);
4916                }
4917                Action::SetPunchEnabled(_) => {
4918                    inverse_actions = Some(vec![Action::SetPunchEnabled(self.punch_enabled)]);
4919                }
4920                Action::SetPunchRange(_) => {
4921                    inverse_actions = Some(vec![
4922                        Action::SetPunchRange(self.punch_range_samples),
4923                        Action::SetPunchEnabled(self.punch_enabled),
4924                    ]);
4925                }
4926                Action::SetMetronomeEnabled(_) => {
4927                    inverse_actions =
4928                        Some(vec![Action::SetMetronomeEnabled(self.metronome_enabled)]);
4929                }
4930                Action::SetTimeSignature { .. } => {
4931                    inverse_actions = Some(vec![Action::SetTimeSignature {
4932                        numerator: self.tsig_num,
4933                        denominator: self.tsig_denom,
4934                    }]);
4935                }
4936                Action::SetClipPlaybackEnabled(_) => {
4937                    inverse_actions = Some(vec![Action::SetClipPlaybackEnabled(
4938                        self.clip_playback_enabled,
4939                    )]);
4940                }
4941                Action::SetRecordEnabled(_) => {
4942                    inverse_actions = Some(vec![Action::SetRecordEnabled(self.record_enabled)]);
4943                }
4944                Action::SetGlobalMidiLearnBinding { target, .. } => {
4945                    let binding = match target {
4946                        crate::message::GlobalMidiLearnTarget::PlayPause => {
4947                            self.global_midi_learn_play_pause.clone()
4948                        }
4949                        crate::message::GlobalMidiLearnTarget::Stop => {
4950                            self.global_midi_learn_stop.clone()
4951                        }
4952                        crate::message::GlobalMidiLearnTarget::RecordToggle => {
4953                            self.global_midi_learn_record_toggle.clone()
4954                        }
4955                    };
4956                    inverse_actions = Some(vec![Action::SetGlobalMidiLearnBinding {
4957                        target: *target,
4958                        binding,
4959                    }]);
4960                }
4961                _ => {}
4962            }
4963        }
4964
4965        match action_to_process {
4966            Action::Play => {
4967                tracing::debug!(
4968                    "Action::Play pressed, transport_sample={}",
4969                    self.transport_sample
4970                );
4971                self.playing = true;
4972                self.transport_restart_pending = true;
4973                self.notified_loop_wrap_sample = None;
4974                self.invalidate_track_cycle_state();
4975                if let Some(driver) = self.hw_driver.as_mut() {
4976                    driver.lock().set_playing(true);
4977                }
4978                #[cfg(unix)]
4979                if let Some(jack) = &self.jack_runtime
4980                    && let Err(e) = jack.lock().transport_start()
4981                {
4982                    self.notify_clients(Err(e)).await;
4983                }
4984                self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
4985                    .await;
4986                self.preload_track_clips().await;
4987                {
4988                    let echoes = self.apply_modulators(self.transport_sample);
4989                    for action in echoes {
4990                        self.notify_clients(Ok(action)).await;
4991                    }
4992                }
4993                let send_result = self.send_tasks().await;
4994                tracing::debug!("send_tasks after Play returned finished={}", send_result);
4995                if !self.awaiting_hwfinished
4996                    && !self.handling_hwfinished
4997                    && send_result
4998                    && self.hw_worker.is_some()
4999                {
5000                    self.transport_restart_pending = false;
5001                    self.request_hw_cycle().await;
5002                }
5003            }
5004            Action::Pause => {
5005                self.clip_playback_enabled = false;
5006                for track in self.state.lock().tracks.values() {
5007                    track.lock().set_clip_playback_enabled(false);
5008                }
5009                if !self.playing {
5010                    self.playing = true;
5011                    self.transport_restart_pending = true;
5012                    self.notified_loop_wrap_sample = None;
5013                    self.invalidate_track_cycle_state();
5014                    if let Some(driver) = self.hw_driver.as_mut() {
5015                        driver.lock().set_playing(true);
5016                    }
5017                    #[cfg(unix)]
5018                    if let Some(jack) = &self.jack_runtime
5019                        && let Err(e) = jack.lock().transport_start()
5020                    {
5021                        self.notify_clients(Err(e)).await;
5022                    }
5023                    self.preload_track_clips().await;
5024                    if !self.awaiting_hwfinished
5025                        && !self.handling_hwfinished
5026                        && self.send_tasks().await
5027                        && self.hw_worker.is_some()
5028                    {
5029                        self.transport_restart_pending = false;
5030                        self.request_hw_cycle().await;
5031                    }
5032                }
5033                self.notify_clients(Ok(Action::Pause)).await;
5034                self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
5035                    .await;
5036            }
5037            Action::Stop => {
5038                self.playing = false;
5039                self.transport_panic_flush_pending = false;
5040                self.transport_restart_pending = false;
5041                self.notified_loop_wrap_sample = None;
5042                self.invalidate_track_cycle_state();
5043                if let Some(driver) = self.hw_driver.as_mut() {
5044                    driver.lock().set_playing(false);
5045                }
5046                #[cfg(unix)]
5047                if let Some(jack) = &self.jack_runtime
5048                    && let Err(e) = jack.lock().transport_stop()
5049                {
5050                    self.notify_clients(Err(e)).await;
5051                }
5052                let panic_events = self.note_off_events_for_all_active_tracks();
5053                if let Some(worker) = &self.hw_worker {
5054                    if !panic_events.is_empty()
5055                        && let Err(e) = worker.tx.send(Message::HWMidiOutEvents(panic_events)).await
5056                    {
5057                        error!("Error sending stop MIDI panic events {e}");
5058                    }
5059                } else {
5060                    self.pending_hw_midi_out_events_by_device
5061                        .extend(panic_events);
5062                }
5063                self.reset_meters_after_stop();
5064                self.flush_recordings().await;
5065                self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
5066                    .await;
5067            }
5068            Action::JumpToEnd => {
5069                self.transport_sample = self.normalize_transport_sample(self.session_end_sample());
5070                self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
5071                    .await;
5072            }
5073            Action::Panic => {
5074                let panic_events = self.panic_events_for_all_hw_midi_outputs();
5075                if let Some(worker) = &self.hw_worker {
5076                    if !panic_events.is_empty() {
5077                        if let Err(e) = worker.tx.send(Message::ClearHWMidiOutEvents).await {
5078                            error!("Error clearing HW MIDI queue for panic {e}");
5079                        }
5080                        self.midi_hub
5081                            .lock()
5082                            .write_events_blocking(&panic_events, Duration::from_millis(250));
5083                    }
5084                } else if !panic_events.is_empty() {
5085                    self.pending_hw_midi_out_events_by_device
5086                        .extend(panic_events);
5087                }
5088            }
5089            Action::SetClipPlaybackEnabled(enabled) => {
5090                self.clip_playback_enabled = enabled;
5091                for track in self.state.lock().tracks.values() {
5092                    track.lock().set_clip_playback_enabled(enabled);
5093                }
5094            }
5095            Action::TransportPosition(sample) => {
5096                self.transport_sample = self.normalize_transport_sample(sample);
5097                self.notified_loop_wrap_sample = None;
5098                {
5099                    let echoes = self.apply_modulators(self.transport_sample);
5100                    for action in echoes {
5101                        self.notify_clients(Ok(action)).await;
5102                    }
5103                }
5104                #[cfg(unix)]
5105                if let Some(jack) = &self.jack_runtime
5106                    && let Err(e) = jack.lock().transport_locate(self.transport_sample)
5107                {
5108                    self.notify_clients(Err(e)).await;
5109                }
5110                if self.playing {
5111                    self.transport_restart_pending = true;
5112                    self.invalidate_track_cycle_state();
5113                    self.transport_panic_flush_pending = self.hw_worker.is_some();
5114                    self.clear_hw_midi_output_state(true).await;
5115                    if !self.awaiting_hwfinished && !self.handling_hwfinished {
5116                        if self.hw_worker.is_some() {
5117                            self.request_hw_cycle().await;
5118                        } else if self.send_tasks().await {
5119                            self.transport_restart_pending = false;
5120                            self.request_hw_cycle().await;
5121                        }
5122                    }
5123                }
5124            }
5125            Action::SetLoopEnabled(enabled) => {
5126                self.loop_enabled = enabled && self.loop_range_samples.is_some();
5127                self.notified_loop_wrap_sample = None;
5128            }
5129            Action::SetLoopRange(range) => {
5130                self.loop_range_samples = range.and_then(|(start, end)| {
5131                    if end > start {
5132                        Some((start, end))
5133                    } else {
5134                        None
5135                    }
5136                });
5137                self.loop_enabled = self.loop_range_samples.is_some();
5138                self.notified_loop_wrap_sample = None;
5139                if self.loop_enabled
5140                    && let Some((loop_start, loop_end)) = self.loop_range_samples
5141                    && self.transport_sample >= loop_end
5142                {
5143                    self.transport_sample = loop_start;
5144                    self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
5145                        .await;
5146                }
5147            }
5148            Action::SetPunchEnabled(enabled) => {
5149                self.punch_enabled = enabled && self.punch_range_samples.is_some();
5150            }
5151            Action::SetPunchRange(range) => {
5152                self.punch_range_samples = range.and_then(|(start, end)| {
5153                    if end > start {
5154                        Some((start, end))
5155                    } else {
5156                        None
5157                    }
5158                });
5159                self.punch_enabled = self.punch_range_samples.is_some();
5160            }
5161            Action::SetMetronomeEnabled(enabled) => {
5162                self.metronome_enabled = enabled;
5163                if enabled {
5164                    self.ensure_metronome_track().await;
5165                }
5166                if let Some(track) = self.state.lock().tracks.get(Self::METRONOME_TRACK).cloned() {
5167                    track.lock().set_metronome_enabled(enabled);
5168                }
5169            }
5170            Action::SetTempo(bpm) => {
5171                self.tempo_bpm = bpm.max(1.0);
5172            }
5173            Action::SetTimeSignature {
5174                numerator,
5175                denominator,
5176            } => {
5177                self.tsig_num = numerator.max(1);
5178                self.tsig_denom = denominator.max(1);
5179            }
5180            Action::SetOscEnabled(enabled) => {
5181                if let Err(err) = self.set_osc_enabled_with(enabled, OscServer::start) {
5182                    self.notify_clients(Err(err)).await;
5183                }
5184            }
5185            Action::SetRecordEnabled(enabled) => {
5186                self.record_enabled = enabled;
5187                if !enabled {
5188                    if self.awaiting_hwfinished {
5189                        self.append_recorded_cycle();
5190                    }
5191                    self.flush_recordings().await;
5192                } else if self.session_dir.is_none() {
5193                    self.notify_clients(Err(
5194                        "Recording enabled but session path is not set".to_string()
5195                    ))
5196                    .await;
5197                }
5198            }
5199            Action::SetModulators(ref modulators) => {
5200                self.modulators = modulators.clone();
5201                let echoes = self.apply_modulators(self.transport_sample);
5202                for action in echoes {
5203                    self.notify_clients(Ok(action)).await;
5204                }
5205            }
5206            Action::SetStepRecording(enabled) => {
5207                self.step_recording_enabled = enabled;
5208            }
5209            Action::BeginHistoryGroup if self.history_group.is_none() => {
5210                self.history_group = Some(UndoEntry {
5211                    forward_actions: vec![],
5212                    inverse_actions: vec![],
5213                });
5214            }
5215            Action::EndHistoryGroup => {
5216                if let Some(mut group) = self.history_group.take()
5217                    && !group.forward_actions.is_empty()
5218                    && !group.inverse_actions.is_empty()
5219                {
5220                    let mut add_tracks = Vec::new();
5221                    let mut connections = Vec::new();
5222                    let mut rest = Vec::new();
5223                    for action in group.inverse_actions {
5224                        if matches!(action, Action::AddTrack { .. }) {
5225                            add_tracks.push(action);
5226                        } else if matches!(action, Action::Connect { .. }) {
5227                            connections.push(action);
5228                        } else {
5229                            rest.push(action);
5230                        }
5231                    }
5232                    group.inverse_actions = add_tracks;
5233                    group.inverse_actions.extend(rest);
5234                    group.inverse_actions.extend(connections);
5235                    self.history.record(group);
5236                }
5237            }
5238            Action::SetSessionPath(ref path) => {
5239                self.session_dir = Some(Path::new(path).to_path_buf());
5240                self.ensure_session_subdirs();
5241                #[cfg(all(unix, not(target_os = "macos")))]
5242                let _lv2_dir = self.session_plugins_dir();
5243                for track in self.state.lock().tracks.values() {
5244                    track.lock().set_session_base_dir(self.session_dir.clone());
5245                }
5246            }
5247            Action::MarkHistorySavePoint => {
5248                self.history.mark_save_point();
5249                self.notify_clients(Ok(Action::HistoryState {
5250                    dirty: self.history.is_dirty(),
5251                }))
5252                .await;
5253            }
5254            Action::ClearHistory => {
5255                self.history.clear();
5256                self.history.mark_save_point();
5257            }
5258            Action::BeginSessionRestore => {
5259                self.history_suspended = true;
5260                self.history.clear();
5261            }
5262            Action::EndSessionRestore => {
5263                self.history.clear();
5264                self.history_suspended = false;
5265                self.preload_track_clips_spawn();
5266            }
5267            Action::Quit => {
5268                self.flush_recordings().await;
5269                // Stop the HW worker before notifying the GUI so the
5270                // OSS audio channels are halted and closed from the
5271                // worker's own thread. The GUI calls exit(0) upon
5272                // receiving the Quit response, which skips Rust
5273                // destructors. Without this, the kernel's dsp_close
5274                // drains pending audio buffers for up to CHN_TIMEOUT
5275                // (5s) during process teardown.
5276                if let Some(worker) = self.hw_worker.take() {
5277                    if let Some(hw) = &self.hw_driver {
5278                        hw.lock().request_stop();
5279                    }
5280                    // Send MIDI panic (All Sound Off) for any active
5281                    // notes before stopping the worker.
5282                    let panic_events = self.panic_events_for_all_hw_midi_outputs();
5283                    if !panic_events.is_empty() {
5284                        let _ = worker.tx.send(Message::HWMidiOutEvents(panic_events)).await;
5285                    }
5286                    // Send Quit to the worker so it stops its audio
5287                    // cycle loop and releases the driver.
5288                    if let Err(e) = worker.tx.send(Message::Request(a.clone())).await {
5289                        error!("Error sending quit message to HW worker: {e}");
5290                    }
5291                    worker
5292                        .handle
5293                        .await
5294                        .unwrap_or_else(|e| error!("Error waiting for HW worker to quit: {e}"));
5295                }
5296                // Explicitly close audio and MIDI fds before sending
5297                // the Quit response. The GUI calls exit(0) upon
5298                // receiving it, which skips destructors — any
5299                // still-open device fd would trigger the kernel's
5300                // 5-second drain during process teardown.
5301                if let Some(hw) = &self.hw_driver {
5302                    hw.lock().close_fds();
5303                }
5304                self.midi_hub.lock().close_all();
5305                self.hw_driver = None;
5306                self.notify_clients(Ok(Action::Quit)).await;
5307                self.ready_workers.clear();
5308                while !self.workers.is_empty() {
5309                    let worker = self.workers.remove(0);
5310                    if let Err(e) = worker.tx.send(Message::Request(a.clone())).await {
5311                        error!("Error sending quit message to worker: {e}");
5312                    }
5313                    worker
5314                        .handle
5315                        .await
5316                        .unwrap_or_else(|e| error!("Error waiting for worker to quit: {e}"));
5317                }
5318                #[cfg(unix)]
5319                {
5320                    self.jack_runtime = None;
5321                }
5322                self.osc_server = None;
5323                return;
5324            }
5325            Action::AddTrack {
5326                ref name,
5327                audio_ins,
5328                midi_ins,
5329                audio_outs,
5330                midi_outs,
5331                folder,
5332            } => {
5333                let tracks = &mut self.state.lock().tracks;
5334                if tracks.contains_key(name) {
5335                    self.notify_clients(Err(format!("Track {} already exists", name)))
5336                        .await;
5337                    return;
5338                }
5339                let maybe_hw = if let Some(oss) = &self.hw_driver {
5340                    let hw = oss.lock();
5341                    Some((hw.cycle_samples(), hw.sample_rate() as f64))
5342                } else {
5343                    #[cfg(unix)]
5344                    if let Some(jack) = &self.jack_runtime {
5345                        let j = jack.lock();
5346                        Some((j.buffer_size, j.sample_rate as f64))
5347                    } else {
5348                        None
5349                    }
5350                    #[cfg(not(unix))]
5351                    None
5352                };
5353
5354                if let Some((chsamples, sample_rate)) = maybe_hw {
5355                    let track = if folder {
5356                        Track::new_folder(
5357                            name.clone(),
5358                            audio_ins,
5359                            audio_outs,
5360                            midi_ins,
5361                            midi_outs,
5362                            chsamples,
5363                            sample_rate,
5364                        )
5365                    } else {
5366                        Track::new(
5367                            name.clone(),
5368                            audio_ins,
5369                            audio_outs,
5370                            midi_ins,
5371                            midi_outs,
5372                            chsamples,
5373                            sample_rate,
5374                        )
5375                    };
5376                    tracks.insert(name.clone(), Arc::new(UnsafeMutex::new(Box::new(track))));
5377                    if let Some(track) = tracks.get(name) {
5378                        let t = track.lock();
5379                        t.set_clip_playback_enabled(self.clip_playback_enabled);
5380                        t.set_transport_timing(self.tempo_bpm, self.tsig_num, self.tsig_denom);
5381                        t.set_session_base_dir(self.session_dir.clone());
5382                    }
5383                } else {
5384                    self.notify_clients(Err(
5385                        "Engine needs to open audio device before adding audio track".to_string(),
5386                    ))
5387                    .await;
5388                }
5389            }
5390            Action::TrackAddAudioInput(ref name) => {
5391                let track = match self.track_handle_or_err(name) {
5392                    Ok(track) => track,
5393                    Err(e) => {
5394                        self.notify_clients(Err(e)).await;
5395                        return;
5396                    }
5397                };
5398                if let Err(e) = track.lock().add_audio_input() {
5399                    self.notify_clients(Err(e)).await;
5400                    return;
5401                }
5402            }
5403            Action::TrackAddAudioOutput(ref name) => {
5404                let track = match self.track_handle_or_err(name) {
5405                    Ok(track) => track,
5406                    Err(e) => {
5407                        self.notify_clients(Err(e)).await;
5408                        return;
5409                    }
5410                };
5411                if let Err(e) = track.lock().add_audio_output() {
5412                    self.notify_clients(Err(e)).await;
5413                    return;
5414                }
5415            }
5416            Action::TrackRemoveAudioInput(ref name) => {
5417                let track = match self.track_handle_or_err(name) {
5418                    Ok(track) => track,
5419                    Err(e) => {
5420                        self.notify_clients(Err(e)).await;
5421                        return;
5422                    }
5423                };
5424                if let Err(e) = track.lock().remove_audio_input() {
5425                    self.notify_clients(Err(e)).await;
5426                    return;
5427                }
5428            }
5429            Action::TrackRemoveAudioOutput(ref name) => {
5430                let track = match self.track_handle_or_err(name) {
5431                    Ok(track) => track,
5432                    Err(e) => {
5433                        self.notify_clients(Err(e)).await;
5434                        return;
5435                    }
5436                };
5437                let (hw_outputs, track_inputs) = {
5438                    let state = self.state.lock();
5439                    let hw_outputs = self.all_hw_output_audio_ports();
5440                    let track_inputs = state
5441                        .tracks
5442                        .iter()
5443                        .filter(|(track_name, _)| *track_name != name)
5444                        .flat_map(|(_, handle)| handle.lock().audio.ins.clone())
5445                        .collect::<Vec<_>>();
5446                    (hw_outputs, track_inputs)
5447                };
5448                if let Err(e) = track.lock().remove_audio_output(&hw_outputs, &track_inputs) {
5449                    self.notify_clients(Err(e)).await;
5450                    return;
5451                }
5452            }
5453            Action::RenameTrack {
5454                ref old_name,
5455                ref new_name,
5456            } => {
5457                if self.state.lock().tracks.contains_key(new_name) {
5458                    self.notify_clients(Err(format!("Track '{}' already exists", new_name)))
5459                        .await;
5460                    return;
5461                }
5462
5463                let Some(track) = self.state.lock().tracks.remove(old_name) else {
5464                    self.notify_clients(Err(format!("Track '{}' not found", old_name)))
5465                        .await;
5466                    return;
5467                };
5468
5469                track.lock().name = new_name.clone();
5470                self.state.lock().tracks.insert(new_name.clone(), track);
5471                for other in self.state.lock().tracks.values() {
5472                    let other = other.lock();
5473                    if other.parent_track.as_deref() == Some(old_name.as_str()) {
5474                        other.parent_track = Some(new_name.clone());
5475                    }
5476                }
5477
5478                if let Some(recording) = self.audio_recordings.remove(old_name) {
5479                    self.audio_recordings.insert(new_name.clone(), recording);
5480                }
5481                if let Some(recording) = self.midi_recordings.remove(old_name) {
5482                    self.midi_recordings.insert(new_name.clone(), recording);
5483                }
5484
5485                for route in &mut self.midi_hw_in_routes {
5486                    if route.to_track == *old_name {
5487                        route.to_track = new_name.clone();
5488                    }
5489                }
5490                for route in &mut self.midi_hw_out_routes {
5491                    if route.from_track == *old_name {
5492                        route.from_track = new_name.clone();
5493                    }
5494                }
5495                if let Some((armed_track, target, device)) = self.pending_midi_learn.clone()
5496                    && armed_track == *old_name
5497                {
5498                    self.pending_midi_learn = Some((new_name.clone(), target, device));
5499                }
5500
5501                self.notify_clients(Ok(Action::RenameTrack {
5502                    old_name: old_name.clone(),
5503                    new_name: new_name.clone(),
5504                }))
5505                .await;
5506            }
5507            Action::RemoveTrack(ref name) => {
5508                let mut descendant_names = Vec::new();
5509                self.collect_descendant_track_names(name, &mut descendant_names);
5510                let names_to_remove: Vec<String> = descendant_names
5511                    .iter()
5512                    .cloned()
5513                    .chain(std::iter::once(name.clone()))
5514                    .collect();
5515
5516                let combined_inverse = if record_history && !self.history_suspended {
5517                    let state = self.state.lock();
5518                    let mut inv = Vec::new();
5519                    for n in &names_to_remove {
5520                        if let Some(mut actions) =
5521                            create_inverse_actions(&Action::RemoveTrack(n.clone()), state)
5522                        {
5523                            inv.append(&mut actions);
5524                        }
5525                        for route in self.midi_hw_in_routes.iter().filter(|r| &r.to_track == n) {
5526                            inv.push(Action::Connect {
5527                                from_track: format!("midi:hw:in:{}", route.device),
5528                                from_port: 0,
5529                                to_track: route.to_track.clone(),
5530                                to_port: route.to_port,
5531                                kind: Kind::MIDI,
5532                            });
5533                        }
5534                        for route in self
5535                            .midi_hw_out_routes
5536                            .iter()
5537                            .filter(|r| &r.from_track == n)
5538                        {
5539                            inv.push(Action::Connect {
5540                                from_track: route.from_track.clone(),
5541                                from_port: route.from_port,
5542                                to_track: format!("midi:hw:out:{}", route.device),
5543                                to_port: 0,
5544                                kind: Kind::MIDI,
5545                            });
5546                        }
5547                    }
5548
5549                    // Reorder so all AddTrack actions come first, then everything else, then
5550                    // explicit Connect actions. This mirrors EndHistoryGroup and guarantees that
5551                    // tracks are recreated before they are re-parented or reconnected.
5552                    let mut add_tracks = Vec::new();
5553                    let mut connections = Vec::new();
5554                    let mut rest = Vec::new();
5555                    for action in inv {
5556                        match action {
5557                            Action::AddTrack { .. } => add_tracks.push(action),
5558                            Action::Connect { .. } => connections.push(action),
5559                            _ => rest.push(action),
5560                        }
5561                    }
5562                    let mut ordered = add_tracks;
5563                    ordered.extend(rest);
5564                    ordered.extend(connections);
5565                    ordered
5566                } else {
5567                    Vec::new()
5568                };
5569
5570                for n in &descendant_names {
5571                    self.remove_single_track(n).await;
5572                    self.notify_clients(Ok(Action::RemoveTrack(n.clone())))
5573                        .await;
5574                }
5575                self.remove_single_track(name).await;
5576
5577                if record_history && !self.history_suspended && !combined_inverse.is_empty() {
5578                    self.history.record(UndoEntry {
5579                        forward_actions: vec![Action::RemoveTrack(name.clone())],
5580                        inverse_actions: combined_inverse,
5581                    });
5582                }
5583
5584                // The outer code already computed a per-action inverse for the original
5585                // RemoveTrack. We have recorded a combined inverse for the whole subtree, so
5586                // suppress that default recording.
5587                inverse_actions = None;
5588            }
5589            Action::TrackLevel(ref name, level) => {
5590                if name == "hw:out" {
5591                    self.hw_out_level_db = level;
5592                } else if let Some(track) = self.state.lock().tracks.get(name) {
5593                    track.lock().set_level(level);
5594                }
5595            }
5596            Action::TrackBalance(ref name, balance) => {
5597                if name == "hw:out" {
5598                    self.hw_out_balance = balance.clamp(-1.0, 1.0);
5599                } else if let Some(track) = self.state.lock().tracks.get(name) {
5600                    track.lock().set_balance(balance);
5601                }
5602            }
5603            Action::TrackAutomationLevel(ref name, level) => {
5604                tracing::debug!(%name, level, "engine received TrackAutomationLevel");
5605                if name == "hw:out" {
5606                    self.hw_out_level_db = level;
5607                } else if let Some(track) = self.state.lock().tracks.get(name) {
5608                    track.lock().set_level(level);
5609                }
5610            }
5611            Action::TrackAutomationBalance(ref name, balance) => {
5612                if name == "hw:out" {
5613                    self.hw_out_balance = balance.clamp(-1.0, 1.0);
5614                } else if let Some(track) = self.state.lock().tracks.get(name) {
5615                    track.lock().set_balance(balance);
5616                }
5617            }
5618            Action::TrackMidiCc {
5619                ref track_name,
5620                channel,
5621                cc,
5622                value,
5623            } => {
5624                if let Some(track) = self.state.lock().tracks.get(track_name) {
5625                    track
5626                        .lock()
5627                        .pending_automation_midi_events
5628                        .push(MidiEvent::new(
5629                            0,
5630                            vec![0xB0 | channel.min(15), cc.min(127), value.min(127)],
5631                        ));
5632                }
5633            }
5634            Action::RequestMeterSnapshot => {
5635                self.notify_clients(Ok(Action::MeterSnapshot {
5636                    hw_out_db: self.latest_hw_out_meter_db.clone(),
5637                    track_meters: self.latest_track_meter_snapshot.clone(),
5638                }))
5639                .await;
5640                return;
5641            }
5642            Action::TrackMeters { .. } => {}
5643            Action::MeterSnapshot { .. } => {}
5644            Action::TrackToggleArm(ref name) => {
5645                if self.reject_if_track_frozen(name, "arming/disarming").await {
5646                    return;
5647                }
5648                if let Some(track) = self.state.lock().tracks.get(name).cloned() {
5649                    track.lock().arm();
5650                    let armed = track.lock().armed;
5651                    if !armed && self.audio_recordings.contains_key(name) {
5652                        self.flush_track_recording(name).await;
5653                    }
5654                } else {
5655                    tracing::warn!(
5656                        "TrackToggleArm for '{}' but track not found in engine",
5657                        name
5658                    );
5659                }
5660            }
5661            Action::TrackToggleMute(ref name) => {
5662                if name == "hw:out" {
5663                    self.hw_out_muted = !self.hw_out_muted;
5664                } else if let Some(track) = self.state.lock().tracks.get(name) {
5665                    track.lock().mute();
5666                }
5667            }
5668            Action::TrackTogglePhase(ref name) => {
5669                if let Some(track) = self.state.lock().tracks.get(name) {
5670                    track.lock().invert_phase();
5671                }
5672            }
5673            Action::TrackToggleSolo(ref name) => {
5674                if name == "hw:out" {
5675                    return;
5676                }
5677                if let Some(track) = self.state.lock().tracks.get(name) {
5678                    track.lock().solo();
5679                }
5680            }
5681            Action::TrackToggleMaster(ref name) => {
5682                if let Some(track) = self.state.lock().tracks.get(name) {
5683                    track.lock().toggle_master();
5684                }
5685            }
5686            Action::TrackToggleInputMonitor {
5687                ref track_name,
5688                lane,
5689            } => {
5690                if let Some(track) = self.state.lock().tracks.get(track_name) {
5691                    track.lock().toggle_input_monitor(lane);
5692                }
5693            }
5694            Action::TrackToggleDiskMonitor {
5695                ref track_name,
5696                lane,
5697            } => {
5698                if let Some(track) = self.state.lock().tracks.get(track_name) {
5699                    track.lock().toggle_disk_monitor(lane);
5700                }
5701            }
5702            Action::TrackToggleMidiInputMonitor {
5703                ref track_name,
5704                lane,
5705            } => {
5706                if let Some(track) = self.state.lock().tracks.get(track_name) {
5707                    track.lock().toggle_midi_input_monitor(lane);
5708                }
5709            }
5710            Action::TrackToggleMidiDiskMonitor {
5711                ref track_name,
5712                lane,
5713            } => {
5714                if let Some(track) = self.state.lock().tracks.get(track_name) {
5715                    track.lock().toggle_midi_disk_monitor(lane);
5716                }
5717            }
5718            Action::TrackSetColor {
5719                ref track_name,
5720                color,
5721            } => {
5722                if let Some(track) = self.state.lock().tracks.get(track_name) {
5723                    track.lock().color = color;
5724                }
5725            }
5726            Action::TrackArmMidiLearn {
5727                ref track_name,
5728                target,
5729            } => {
5730                if let Err(e) = self.track_handle_or_err(track_name) {
5731                    self.notify_clients(Err(e)).await;
5732                    return;
5733                }
5734                self.pending_midi_learn = Some((track_name.clone(), target, None));
5735            }
5736            Action::GlobalArmMidiLearn { target } => {
5737                self.pending_global_midi_learn = Some(target);
5738            }
5739            Action::TrackSetMidiLearnBinding {
5740                ref track_name,
5741                target,
5742                ref binding,
5743            } => {
5744                if let Some(binding) = binding.as_ref() {
5745                    let conflicts = self.midi_learn_slot_conflicts(
5746                        binding,
5747                        Some(MidiLearnSlot::Track(track_name.clone(), target)),
5748                    );
5749                    if !conflicts.is_empty() {
5750                        self.notify_clients(Err(format!(
5751                            "MIDI learn conflict for '{}' {:?}: {}",
5752                            track_name,
5753                            target,
5754                            conflicts.join(", ")
5755                        )))
5756                        .await;
5757                        return;
5758                    }
5759                }
5760                let track = match self.track_handle_or_err(track_name) {
5761                    Ok(track) => track,
5762                    Err(e) => {
5763                        self.notify_clients(Err(e)).await;
5764                        return;
5765                    }
5766                };
5767                match target {
5768                    crate::message::TrackMidiLearnTarget::Volume => {
5769                        track.lock().midi_learn_volume = binding.clone();
5770                    }
5771                    crate::message::TrackMidiLearnTarget::Balance => {
5772                        track.lock().midi_learn_balance = binding.clone();
5773                    }
5774                    crate::message::TrackMidiLearnTarget::Mute => {
5775                        track.lock().midi_learn_mute = binding.clone();
5776                    }
5777                    crate::message::TrackMidiLearnTarget::Solo => {
5778                        track.lock().midi_learn_solo = binding.clone();
5779                    }
5780                    crate::message::TrackMidiLearnTarget::Arm => {
5781                        track.lock().midi_learn_arm = binding.clone();
5782                    }
5783                    crate::message::TrackMidiLearnTarget::InputMonitor => {
5784                        track.lock().midi_learn_input_monitor = binding.clone();
5785                    }
5786                    crate::message::TrackMidiLearnTarget::DiskMonitor => {
5787                        track.lock().midi_learn_disk_monitor = binding.clone();
5788                    }
5789                }
5790            }
5791            Action::SetGlobalMidiLearnBinding {
5792                target,
5793                ref binding,
5794            } => {
5795                if let Some(binding) = binding.as_ref() {
5796                    let conflicts = self
5797                        .midi_learn_slot_conflicts(binding, Some(MidiLearnSlot::Global(target)));
5798                    if !conflicts.is_empty() {
5799                        self.notify_clients(Err(format!(
5800                            "Global MIDI learn conflict for {:?}: {}",
5801                            target,
5802                            conflicts.join(", ")
5803                        )))
5804                        .await;
5805                        return;
5806                    }
5807                }
5808                match target {
5809                    crate::message::GlobalMidiLearnTarget::PlayPause => {
5810                        self.global_midi_learn_play_pause = binding.clone();
5811                    }
5812                    crate::message::GlobalMidiLearnTarget::Stop => {
5813                        self.global_midi_learn_stop = binding.clone();
5814                    }
5815                    crate::message::GlobalMidiLearnTarget::RecordToggle => {
5816                        self.global_midi_learn_record_toggle = binding.clone();
5817                    }
5818                }
5819            }
5820            Action::TrackSetFolder {
5821                ref track_name,
5822                is_folder,
5823            } => {
5824                let track = match self.track_handle_or_err(track_name) {
5825                    Ok(track) => track,
5826                    Err(e) => {
5827                        self.notify_clients(Err(e)).await;
5828                        return;
5829                    }
5830                };
5831                {
5832                    let track = track.lock();
5833                    track.is_folder = is_folder;
5834                    track.ensure_default_audio_passthrough();
5835                    track.ensure_default_midi_passthrough();
5836                }
5837                self.notify_clients(Ok(Action::TrackSetFolder {
5838                    track_name: track_name.clone(),
5839                    is_folder,
5840                }))
5841                .await;
5842            }
5843            Action::TrackSetParent {
5844                ref track_name,
5845                ref parent_name,
5846            } => {
5847                let track = match self.track_handle_or_err(track_name) {
5848                    Ok(track) => track,
5849                    Err(e) => {
5850                        self.notify_clients(Err(e)).await;
5851                        return;
5852                    }
5853                };
5854                if parent_name.as_deref() == Some(track_name.as_str()) {
5855                    self.notify_clients(Err("Track cannot be its own parent".to_string()))
5856                        .await;
5857                    return;
5858                }
5859
5860                // Validate the new parent is a folder (if any).
5861                if let Some(parent_name) = parent_name {
5862                    let state = self.state.lock();
5863                    let parent = state.tracks.get(parent_name);
5864                    if parent.is_none() {
5865                        self.notify_clients(Err(format!(
5866                            "Parent track '{}' does not exist",
5867                            parent_name
5868                        )))
5869                        .await;
5870                        return;
5871                    }
5872                    if !parent.unwrap().lock().is_folder {
5873                        self.notify_clients(Err(format!(
5874                            "Track '{}' is not a folder",
5875                            parent_name
5876                        )))
5877                        .await;
5878                        return;
5879                    }
5880                }
5881
5882                // Disconnect from the old parent and update its child list.
5883                {
5884                    let old_parent_name = track.lock().parent_track.clone();
5885                    if let Some(old_parent_name) = old_parent_name {
5886                        let state = self.state.lock();
5887                        if let (Some(parent_arc), Some(child_arc)) = (
5888                            state.tracks.get(&old_parent_name).cloned(),
5889                            state.tracks.get(track_name).cloned(),
5890                        ) {
5891                            {
5892                                let parent = parent_arc.lock();
5893                                parent.child_tracks.retain(|c| c.lock().name != *track_name);
5894                            }
5895                            {
5896                                let child = child_arc.lock();
5897                                let parent = parent_arc.lock();
5898                                child.disconnect_from_parent(parent);
5899                            }
5900                        }
5901                    }
5902                }
5903
5904                let mut disconnect_actions = Vec::new();
5905
5906                // Remove all existing audio and MIDI connections involving this track.
5907                {
5908                    let state = self.state.lock();
5909                    let hw_inputs = self.all_hw_input_audio_ports();
5910                    let hw_outputs = self.all_hw_output_audio_ports();
5911                    if let Some(child_arc) = state.tracks.get(track_name).cloned() {
5912                        let child = child_arc.lock();
5913                        for (port_idx, inp) in child.audio.ins.iter().enumerate() {
5914                            let sources = inp.connections.lock().clone();
5915                            for src in sources {
5916                                let _ = AudioIO::disconnect(&src, inp);
5917                                if let Some((src_name, src_port)) =
5918                                    self.find_audio_io_owner(state, &src)
5919                                {
5920                                    disconnect_actions.push(Action::Disconnect {
5921                                        from_track: src_name,
5922                                        from_port: src_port,
5923                                        to_track: track_name.clone(),
5924                                        to_port: port_idx,
5925                                        kind: Kind::Audio,
5926                                    });
5927                                } else if let Some(src_port) = hw_inputs
5928                                    .iter()
5929                                    .position(|hw_in| std::sync::Arc::ptr_eq(hw_in, &src))
5930                                {
5931                                    disconnect_actions.push(Action::Disconnect {
5932                                        from_track: "hw:in".to_string(),
5933                                        from_port: src_port,
5934                                        to_track: track_name.clone(),
5935                                        to_port: port_idx,
5936                                        kind: Kind::Audio,
5937                                    });
5938                                }
5939                            }
5940                        }
5941                        for (port_idx, out) in child.audio.outs.iter().enumerate() {
5942                            let targets = out.connections.lock().clone();
5943                            for tgt in targets {
5944                                let _ = AudioIO::disconnect(out, &tgt);
5945                                if let Some((tgt_name, tgt_port)) =
5946                                    self.find_audio_io_owner(state, &tgt)
5947                                {
5948                                    disconnect_actions.push(Action::Disconnect {
5949                                        from_track: track_name.clone(),
5950                                        from_port: port_idx,
5951                                        to_track: tgt_name,
5952                                        to_port: tgt_port,
5953                                        kind: Kind::Audio,
5954                                    });
5955                                } else if let Some(tgt_port) = hw_outputs
5956                                    .iter()
5957                                    .position(|hw_out| std::sync::Arc::ptr_eq(hw_out, &tgt))
5958                                {
5959                                    disconnect_actions.push(Action::Disconnect {
5960                                        from_track: track_name.clone(),
5961                                        from_port: port_idx,
5962                                        to_track: "hw:out".to_string(),
5963                                        to_port: tgt_port,
5964                                        kind: Kind::Audio,
5965                                    });
5966                                }
5967                            }
5968                        }
5969
5970                        // Remove MIDI hardware routes.
5971                        for route in self
5972                            .midi_hw_in_routes
5973                            .iter()
5974                            .filter(|r| r.to_track == *track_name)
5975                        {
5976                            disconnect_actions.push(Action::Disconnect {
5977                                from_track: format!("midi:hw:in:{}", route.device),
5978                                from_port: 0,
5979                                to_track: track_name.clone(),
5980                                to_port: route.to_port,
5981                                kind: Kind::MIDI,
5982                            });
5983                        }
5984                        self.midi_hw_in_routes.retain(|r| r.to_track != *track_name);
5985
5986                        for route in self
5987                            .midi_hw_out_routes
5988                            .iter()
5989                            .filter(|r| r.from_track == *track_name)
5990                        {
5991                            disconnect_actions.push(Action::Disconnect {
5992                                from_track: track_name.clone(),
5993                                from_port: route.from_port,
5994                                to_track: format!("midi:hw:out:{}", route.device),
5995                                to_port: 0,
5996                                kind: Kind::MIDI,
5997                            });
5998                        }
5999                        self.midi_hw_out_routes
6000                            .retain(|r| r.from_track != *track_name);
6001
6002                        // Remove track-to-track MIDI connections where this track is the source.
6003                        for (port_idx, out) in child.midi.outs.iter().enumerate() {
6004                            let targets = out.lock().connections.clone();
6005                            for tgt in targets {
6006                                if let Some((tgt_name, tgt_port, _)) =
6007                                    self.find_midi_io_owner(state, &tgt)
6008                                {
6009                                    let _ = MIDIIO::disconnect(out, &tgt);
6010                                    disconnect_actions.push(Action::Disconnect {
6011                                        from_track: track_name.clone(),
6012                                        from_port: port_idx,
6013                                        to_track: tgt_name,
6014                                        to_port: tgt_port,
6015                                        kind: Kind::MIDI,
6016                                    });
6017                                }
6018                            }
6019                        }
6020                    }
6021
6022                    // Remove track-to-track MIDI connections where this track is the target.
6023                    let child_input_arcs: Vec<_> =
6024                        if let Some(child_arc) = state.tracks.get(track_name).cloned() {
6025                            let child = child_arc.lock();
6026                            child.midi.ins.clone()
6027                        } else {
6028                            Vec::new()
6029                        };
6030                    for (other_name, other_track) in &state.tracks {
6031                        if other_name == track_name {
6032                            continue;
6033                        }
6034                        let other = other_track.lock();
6035                        for (out_port, out) in other.midi.outs.iter().enumerate() {
6036                            let targets = out.lock().connections.clone();
6037                            for tgt in targets {
6038                                if let Some(to_port) = child_input_arcs
6039                                    .iter()
6040                                    .position(|inp| std::sync::Arc::ptr_eq(inp, &tgt))
6041                                {
6042                                    let _ = MIDIIO::disconnect(out, &tgt);
6043                                    disconnect_actions.push(Action::Disconnect {
6044                                        from_track: other_name.clone(),
6045                                        from_port: out_port,
6046                                        to_track: track_name.clone(),
6047                                        to_port,
6048                                        kind: Kind::MIDI,
6049                                    });
6050                                }
6051                            }
6052                        }
6053                    }
6054                }
6055
6056                // Apply the parent change.
6057                {
6058                    track.lock().parent_track = parent_name.clone();
6059                }
6060
6061                // Connect to the new parent and add to its child list.
6062                if let Some(parent_name) = parent_name {
6063                    let state = self.state.lock();
6064                    if let (Some(parent_arc), Some(child_arc)) = (
6065                        state.tracks.get(parent_name).cloned(),
6066                        state.tracks.get(track_name).cloned(),
6067                    ) {
6068                        {
6069                            let parent = parent_arc.lock();
6070                            parent.child_tracks.push(child_arc.clone());
6071                        }
6072                        {
6073                            let child = child_arc.lock();
6074                            let parent = parent_arc.lock();
6075                            // Folder input -> child input (one-to-one when counts match).
6076                            if parent.audio.ins.len() == child.audio.ins.len() {
6077                                for (parent_in, child_in) in
6078                                    parent.audio.ins.iter().zip(child.audio.ins.iter())
6079                                {
6080                                    Track::connect_directed_audio(parent_in, child_in);
6081                                }
6082                            }
6083                            // Child output -> folder output (one-to-one when counts match).
6084                            if parent.audio.outs.len() == child.audio.outs.len() {
6085                                for (child_out, parent_out) in
6086                                    child.audio.outs.iter().zip(parent.audio.outs.iter())
6087                                {
6088                                    AudioIO::connect(child_out, parent_out);
6089                                }
6090                            }
6091                            // Folder MIDI input -> child MIDI input (one-to-one when counts match).
6092                            if parent.midi.ins.len() == child.midi.ins.len() {
6093                                for (parent_in, child_in) in
6094                                    parent.midi.ins.iter().zip(child.midi.ins.iter())
6095                                {
6096                                    let child_in_lock = child_in.lock();
6097                                    if !child_in_lock
6098                                        .connections
6099                                        .iter()
6100                                        .any(|c| Arc::ptr_eq(c, parent_in))
6101                                    {
6102                                        child_in_lock.connections.push(parent_in.clone());
6103                                    }
6104                                }
6105                            }
6106                            // Child MIDI output -> folder MIDI output (one-to-one when counts match).
6107                            if parent.midi.outs.len() == child.midi.outs.len() {
6108                                for (child_out, parent_out) in
6109                                    child.midi.outs.iter().zip(parent.midi.outs.iter())
6110                                {
6111                                    let child_out_lock = child_out.lock();
6112                                    if !child_out_lock
6113                                        .connections
6114                                        .iter()
6115                                        .any(|c| Arc::ptr_eq(c, parent_out))
6116                                    {
6117                                        child_out_lock.connections.push(parent_out.clone());
6118                                    }
6119                                }
6120                            }
6121                            child.invalidate_audio_route_cache();
6122                            parent.invalidate_audio_route_cache();
6123                            child.invalidate_midi_route_cache();
6124                            parent.invalidate_midi_route_cache();
6125                        }
6126                    }
6127                }
6128
6129                // Restore default input->output passthrough so audio/MIDI can flow
6130                // through the track whether it is a root track or a folder child.
6131                {
6132                    let state = self.state.lock();
6133                    if let Some(child_arc) = state.tracks.get(track_name).cloned() {
6134                        let child = child_arc.lock();
6135                        child.ensure_default_audio_passthrough();
6136                        child.ensure_default_midi_passthrough();
6137                    }
6138                }
6139
6140                for action in disconnect_actions {
6141                    self.notify_clients(Ok(action)).await;
6142                }
6143
6144                self.notify_clients(Ok(Action::TrackSetParent {
6145                    track_name: track_name.clone(),
6146                    parent_name: parent_name.clone(),
6147                }))
6148                .await;
6149            }
6150            Action::TrackToggleFolder { ref track_name } => {
6151                let track = match self.track_handle_or_err(track_name) {
6152                    Ok(track) => track,
6153                    Err(e) => {
6154                        self.notify_clients(Err(e)).await;
6155                        return;
6156                    }
6157                };
6158                {
6159                    let t = track.lock();
6160                    t.folder_open = !t.folder_open;
6161                }
6162                self.notify_clients(Ok(Action::TrackToggleFolder {
6163                    track_name: track_name.clone(),
6164                }))
6165                .await;
6166
6167                self.notify_clients(Ok(Action::TrackSetFolder {
6168                    track_name: track_name.clone(),
6169                    is_folder: track.lock().is_folder,
6170                }))
6171                .await;
6172            }
6173            Action::TrackSetMidiLaneChannel {
6174                ref track_name,
6175                lane,
6176                channel,
6177            } => {
6178                let track = match self.track_handle_or_err(track_name) {
6179                    Ok(track) => track,
6180                    Err(e) => {
6181                        self.notify_clients(Err(e)).await;
6182                        return;
6183                    }
6184                };
6185                track.lock().set_midi_lane_channel(lane, channel);
6186            }
6187            Action::TrackSetFrozen {
6188                ref track_name,
6189                frozen,
6190            } => {
6191                let track = match self.track_handle_or_err(track_name) {
6192                    Ok(track) => track,
6193                    Err(e) => {
6194                        self.notify_clients(Err(e)).await;
6195                        return;
6196                    }
6197                };
6198                track.lock().set_frozen(frozen);
6199            }
6200            Action::TrackOfflineBounce {
6201                track_name,
6202                output_path,
6203                start_sample,
6204                length_samples,
6205                automation_lanes,
6206                apply_fader,
6207            } => {
6208                if self.offline_bounce_jobs.contains_key(&track_name) {
6209                    self.notify_clients(Err(format!(
6210                        "Offline bounce for track '{}' is already in progress",
6211                        track_name
6212                    )))
6213                    .await;
6214                    return;
6215                }
6216                if let Err(e) = self.track_handle_or_err(&track_name) {
6217                    self.notify_clients(Err(e)).await;
6218                    return;
6219                }
6220                if length_samples == 0 {
6221                    self.notify_clients(Err(format!(
6222                        "Track '{}' has no renderable content for offline bounce",
6223                        track_name
6224                    )))
6225                    .await;
6226                    return;
6227                }
6228                let Some(worker_index) = self.take_ready_worker_index() else {
6229                    self.pending_requests
6230                        .push_front(Action::TrackOfflineBounce {
6231                            track_name,
6232                            output_path,
6233                            start_sample,
6234                            length_samples,
6235                            automation_lanes,
6236                            apply_fader,
6237                        });
6238                    return;
6239                };
6240                let cancel = Arc::new(AtomicBool::new(false));
6241                self.offline_bounce_jobs.insert(
6242                    track_name.clone(),
6243                    OfflineBounceJob {
6244                        cancel: cancel.clone(),
6245                    },
6246                );
6247                let track_name_clone = track_name.clone();
6248                let worker = &self.workers[worker_index];
6249                let job = crate::message::OfflineBounceWork {
6250                    state: self.state.clone(),
6251                    track_name,
6252                    output_path,
6253                    start_sample,
6254                    length_samples,
6255                    tempo_bpm: self.tempo_bpm,
6256                    tsig_num: self.tsig_num,
6257                    tsig_denom: self.tsig_denom,
6258                    automation_lanes,
6259                    cancel,
6260                    apply_fader,
6261                };
6262                if let Err(e) = worker.tx.send(Message::ProcessOfflineBounce(job)).await {
6263                    self.offline_bounce_jobs.remove(&track_name_clone);
6264                    self.notify_clients(Err(format!("Failed to schedule offline bounce: {e}")))
6265                        .await;
6266                }
6267                return;
6268            }
6269            Action::TrackOfflineBounceCancel { .. } => {}
6270            Action::TrackOfflineBounceCancelAll => {}
6271            Action::TrackOfflineBounceCanceled { .. } => {}
6272            Action::TrackOfflineBounceProgress { .. } => {}
6273            Action::PianoKey {
6274                ref track_name,
6275                note,
6276                velocity,
6277                on,
6278            } => {
6279                if let Some(track) = self.state.lock().tracks.get(track_name) {
6280                    let status = if on { 0x90 } else { 0x80 };
6281                    let event = MidiEvent::new(0, vec![status, note.min(127), velocity.min(127)]);
6282                    track.lock().push_hw_midi_events(&[event]);
6283                }
6284            }
6285            Action::ModifyMidiNotes { .. }
6286            | Action::ModifyMidiControllers { .. }
6287            | Action::DeleteMidiControllers { .. }
6288            | Action::InsertMidiControllers { .. }
6289            | Action::DeleteMidiNotes { .. }
6290            | Action::InsertMidiNotes { .. } => {
6291                if let Err(e) = self.apply_midi_edit_action(&action_to_process) {
6292                    self.notify_clients(Err(e)).await;
6293                    return;
6294                }
6295            }
6296            Action::SetMidiSysExEvents { .. } => {
6297                if let Err(e) = self.apply_midi_edit_action(&action_to_process) {
6298                    self.notify_clients(Err(e)).await;
6299                    return;
6300                }
6301            }
6302            Action::TrackClearDefaultPassthrough { ref track_name } => {
6303                if self
6304                    .reject_if_track_frozen(track_name, "plugin graph editing")
6305                    .await
6306                {
6307                    return;
6308                }
6309                let track = match self.track_handle_or_err(track_name) {
6310                    Ok(track) => track,
6311                    Err(e) => {
6312                        self.notify_clients(Err(e)).await;
6313                        return;
6314                    }
6315                };
6316                track.lock().clear_default_passthrough();
6317            }
6318            Action::TrackClearPlugins { ref track_name } => {
6319                if self
6320                    .reject_if_track_frozen(track_name, "plugin graph editing")
6321                    .await
6322                {
6323                    return;
6324                }
6325                let track = match self.track_handle_or_err(track_name) {
6326                    Ok(track) => track,
6327                    Err(e) => {
6328                        self.notify_clients(Err(e)).await;
6329                        return;
6330                    }
6331                };
6332                track.lock().clear_plugins();
6333                self.notify_clients(Ok(Action::Log {
6334                    source: "engine".to_string(),
6335                    message: format!("Cleared plugins from track '{track_name}'"),
6336                }))
6337                .await;
6338            }
6339            #[cfg(all(unix, not(target_os = "macos")))]
6340            Action::ClipSetLv2PluginState { ref track_name, .. } => {
6341                self.notify_clients(Err(format!(
6342                    "Track '{}': clip LV2 plugin state changes are not supported",
6343                    track_name
6344                )))
6345                .await;
6346            }
6347            Action::TrackGetClapNoteNames { ref track_name } => {
6348                let track = match self.track_handle_or_err(track_name) {
6349                    Ok(track) => track,
6350                    Err(e) => {
6351                        self.notify_clients(Err(e)).await;
6352                        return;
6353                    }
6354                };
6355                let note_names = track.lock().get_clap_note_names();
6356                self.notify_clients(Ok(Action::TrackClapNoteNames {
6357                    track_name: track_name.clone(),
6358                    note_names,
6359                }))
6360                .await;
6361            }
6362            Action::TrackGetPluginGraph { ref track_name } => {
6363                let track = match self.track_handle_or_err(track_name) {
6364                    Ok(track) => track,
6365                    Err(e) => {
6366                        self.notify_clients(Err(e)).await;
6367                        return;
6368                    }
6369                };
6370                let (plugins, connections, connectable_connections) = {
6371                    let track = track.lock();
6372                    (
6373                        track.plugin_graph_plugins(),
6374                        track.plugin_graph_connections(),
6375                        track.connectable_connections(),
6376                    )
6377                };
6378                self.notify_clients(Ok(Action::TrackPluginGraph {
6379                    track_name: track_name.clone(),
6380                    plugins,
6381                    connections,
6382                    connectable_connections,
6383                }))
6384                .await;
6385                return;
6386            }
6387            Action::TrackPluginGraph { .. } => {}
6388            Action::TrackConnectPluginAudio {
6389                ref track_name,
6390                ref from_node,
6391                from_port,
6392                ref to_node,
6393                to_port,
6394            } => {
6395                if self
6396                    .reject_if_track_frozen(track_name, "plugin routing changes")
6397                    .await
6398                {
6399                    return;
6400                }
6401                let track = match self.track_handle_or_err(track_name) {
6402                    Ok(track) => track,
6403                    Err(e) => {
6404                        self.notify_clients(Err(e)).await;
6405                        return;
6406                    }
6407                };
6408                if let Err(e) = track.lock().connect_plugin_audio(
6409                    from_node.clone(),
6410                    from_port,
6411                    to_node.clone(),
6412                    to_port,
6413                ) {
6414                    self.notify_clients(Err(e)).await;
6415                    return;
6416                }
6417            }
6418            Action::TrackConnectPluginMidi {
6419                ref track_name,
6420                ref from_node,
6421                from_port,
6422                ref to_node,
6423                to_port,
6424            } => {
6425                if self
6426                    .reject_if_track_frozen(track_name, "plugin routing changes")
6427                    .await
6428                {
6429                    return;
6430                }
6431                let track = match self.track_handle_or_err(track_name) {
6432                    Ok(track) => track,
6433                    Err(e) => {
6434                        self.notify_clients(Err(e)).await;
6435                        return;
6436                    }
6437                };
6438                if let Err(e) = track.lock().connect_plugin_midi(
6439                    from_node.clone(),
6440                    from_port,
6441                    to_node.clone(),
6442                    to_port,
6443                ) {
6444                    self.notify_clients(Err(e)).await;
6445                    return;
6446                }
6447            }
6448            Action::TrackDisconnectPluginAudio {
6449                ref track_name,
6450                ref from_node,
6451                from_port,
6452                ref to_node,
6453                to_port,
6454            } => {
6455                if self
6456                    .reject_if_track_frozen(track_name, "plugin routing changes")
6457                    .await
6458                {
6459                    return;
6460                }
6461                let track = match self.track_handle_or_err(track_name) {
6462                    Ok(track) => track,
6463                    Err(e) => {
6464                        self.notify_clients(Err(e)).await;
6465                        return;
6466                    }
6467                };
6468                if let Err(e) = track.lock().disconnect_plugin_audio(
6469                    from_node.clone(),
6470                    from_port,
6471                    to_node.clone(),
6472                    to_port,
6473                ) {
6474                    self.notify_clients(Err(e)).await;
6475                    return;
6476                }
6477            }
6478            Action::TrackDisconnectPluginMidi {
6479                ref track_name,
6480                ref from_node,
6481                from_port,
6482                ref to_node,
6483                to_port,
6484            } => {
6485                if self
6486                    .reject_if_track_frozen(track_name, "plugin routing changes")
6487                    .await
6488                {
6489                    return;
6490                }
6491                let track = match self.track_handle_or_err(track_name) {
6492                    Ok(track) => track,
6493                    Err(e) => {
6494                        self.notify_clients(Err(e)).await;
6495                        return;
6496                    }
6497                };
6498                if let Err(e) = track.lock().disconnect_plugin_midi(
6499                    from_node.clone(),
6500                    from_port,
6501                    to_node.clone(),
6502                    to_port,
6503                ) {
6504                    self.notify_clients(Err(e)).await;
6505                    return;
6506                }
6507            }
6508            Action::TrackConnectAudio {
6509                ref track_name,
6510                ref from,
6511                from_port,
6512                ref to,
6513                to_port,
6514            } => {
6515                if self
6516                    .reject_if_track_frozen(track_name, "routing changes")
6517                    .await
6518                {
6519                    return;
6520                }
6521                let track = match self.track_handle_or_err(track_name) {
6522                    Ok(track) => track,
6523                    Err(e) => {
6524                        self.notify_clients(Err(e)).await;
6525                        return;
6526                    }
6527                };
6528                if let Err(e) = track.lock().connect_audio_connectable(
6529                    from.clone(),
6530                    from_port,
6531                    to.clone(),
6532                    to_port,
6533                ) {
6534                    self.notify_clients(Err(e)).await;
6535                    return;
6536                }
6537            }
6538            Action::TrackDisconnectAudio {
6539                ref track_name,
6540                ref from,
6541                from_port,
6542                ref to,
6543                to_port,
6544            } => {
6545                if self
6546                    .reject_if_track_frozen(track_name, "routing changes")
6547                    .await
6548                {
6549                    return;
6550                }
6551                let track = match self.track_handle_or_err(track_name) {
6552                    Ok(track) => track,
6553                    Err(e) => {
6554                        self.notify_clients(Err(e)).await;
6555                        return;
6556                    }
6557                };
6558                if let Err(e) = track.lock().disconnect_audio_connectable(
6559                    from.clone(),
6560                    from_port,
6561                    to.clone(),
6562                    to_port,
6563                ) {
6564                    self.notify_clients(Err(e)).await;
6565                    return;
6566                }
6567            }
6568            Action::TrackConnectMidi {
6569                ref track_name,
6570                ref from,
6571                from_port,
6572                ref to,
6573                to_port,
6574            } => {
6575                if self
6576                    .reject_if_track_frozen(track_name, "routing changes")
6577                    .await
6578                {
6579                    return;
6580                }
6581                let track = match self.track_handle_or_err(track_name) {
6582                    Ok(track) => track,
6583                    Err(e) => {
6584                        self.notify_clients(Err(e)).await;
6585                        return;
6586                    }
6587                };
6588                if let Err(e) = track.lock().connect_midi_connectable(
6589                    from.clone(),
6590                    from_port,
6591                    to.clone(),
6592                    to_port,
6593                ) {
6594                    self.notify_clients(Err(e)).await;
6595                    return;
6596                }
6597            }
6598            Action::TrackDisconnectMidi {
6599                ref track_name,
6600                ref from,
6601                from_port,
6602                ref to,
6603                to_port,
6604            } => {
6605                if self
6606                    .reject_if_track_frozen(track_name, "routing changes")
6607                    .await
6608                {
6609                    return;
6610                }
6611                let track = match self.track_handle_or_err(track_name) {
6612                    Ok(track) => track,
6613                    Err(e) => {
6614                        self.notify_clients(Err(e)).await;
6615                        return;
6616                    }
6617                };
6618                if let Err(e) = track.lock().disconnect_midi_connectable(
6619                    from.clone(),
6620                    from_port,
6621                    to.clone(),
6622                    to_port,
6623                ) {
6624                    self.notify_clients(Err(e)).await;
6625                    return;
6626                }
6627            }
6628            #[cfg(all(unix, not(target_os = "macos")))]
6629            Action::ListLv2Plugins => {
6630                match crate::plugins::scan_plugins::<crate::plugins::types::Lv2PluginInfo>("lv2") {
6631                    Ok(plugins) => {
6632                        self.notify_clients(Ok(Action::Lv2Plugins(plugins))).await;
6633                    }
6634                    Err(e) => {
6635                        tracing::error!("LV2 plugin scan failed: {e}");
6636                        self.notify_clients(Ok(Action::Lv2PluginsUnavailable { error: e }))
6637                            .await;
6638                    }
6639                }
6640                return;
6641            }
6642            #[cfg(all(unix, not(target_os = "macos")))]
6643            Action::Lv2Plugins(_) => {}
6644            #[cfg(all(unix, not(target_os = "macos")))]
6645            Action::Lv2PluginsUnavailable { .. } => {}
6646            Action::ListVst3Plugins => {
6647                match crate::plugins::scan_plugins::<crate::plugins::types::Vst3PluginInfo>("vst3")
6648                {
6649                    Ok(plugins) => {
6650                        self.notify_clients(Ok(Action::Vst3Plugins(plugins))).await;
6651                    }
6652                    Err(e) => {
6653                        tracing::error!("VST3 plugin scan failed: {e}");
6654                        self.notify_clients(Ok(Action::Vst3PluginsUnavailable { error: e }))
6655                            .await;
6656                    }
6657                }
6658                return;
6659            }
6660            Action::Vst3Plugins(_) => {}
6661            Action::Vst3PluginsUnavailable { .. } => {}
6662            Action::ListClapPlugins => {
6663                match crate::plugins::scan_plugins::<crate::plugins::types::ClapPluginInfo>("clap")
6664                {
6665                    Ok(plugins) => {
6666                        self.notify_clients(Ok(Action::ClapPlugins(plugins))).await;
6667                    }
6668                    Err(e) => {
6669                        tracing::error!("CLAP plugin scan failed: {e}");
6670                        self.notify_clients(Ok(Action::ClapPluginsUnavailable { error: e }))
6671                            .await;
6672                    }
6673                }
6674                return;
6675            }
6676            Action::ListClapPluginsWithCapabilities => {
6677                match crate::plugins::scan_plugins::<crate::plugins::types::ClapPluginInfo>("clap")
6678                {
6679                    Ok(plugins) => {
6680                        self.notify_clients(Ok(Action::ClapPlugins(plugins))).await;
6681                    }
6682                    Err(e) => {
6683                        tracing::error!("CLAP plugin scan failed: {e}");
6684                        self.notify_clients(Ok(Action::ClapPluginsUnavailable { error: e }))
6685                            .await;
6686                    }
6687                }
6688                return;
6689            }
6690            Action::ClapPlugins(_) => {}
6691            Action::ClapPluginsUnavailable { .. } => {}
6692            Action::TrackLoadClapPlugin {
6693                ref track_name,
6694                ref plugin_path,
6695                instance_id,
6696            } => {
6697                if self
6698                    .reject_if_track_frozen(track_name, "CLAP plugin loading")
6699                    .await
6700                {
6701                    return;
6702                }
6703                let track = match self.track_handle_or_err(track_name) {
6704                    Ok(track) => track,
6705                    Err(e) => {
6706                        self.notify_clients(Err(e)).await;
6707                        return;
6708                    }
6709                };
6710                let track = track.lock();
6711                if track.audio.processing {
6712                    self.notify_clients(Err(format!(
6713                        "Track '{}' is currently processing audio; stop playback before loading CLAP plugins",
6714                        track_name
6715                    )))
6716                    .await;
6717                    return;
6718                }
6719                if let Err(e) = track.load_clap_plugin(plugin_path, instance_id) {
6720                    self.notify_clients(Err(e)).await;
6721                    return;
6722                }
6723                self.notify_clients(Ok(Action::Log {
6724                    source: "engine".to_string(),
6725                    message: format!("CLAP plugin loaded on track '{track_name}': {plugin_path}"),
6726                }))
6727                .await;
6728                if let Some(instance) = track.clap_plugins.last()
6729                    && let Some(stderr) = instance.processor.lock().take_stderr()
6730                {
6731                    let source = format!("clap:{plugin_path}");
6732                    self.spawn_plugin_host_stderr_reader(stderr, source);
6733                    self.notify_clients(Ok(Action::Log {
6734                        source: "engine".to_string(),
6735                        message: format!(
6736                            "Attached stderr reader for CLAP plugin on track '{track_name}'"
6737                        ),
6738                    }))
6739                    .await;
6740                }
6741            }
6742            Action::TrackUnloadClapPlugin {
6743                ref track_name,
6744                ref plugin_path,
6745            } => {
6746                if self
6747                    .reject_if_track_frozen(track_name, "CLAP plugin unloading")
6748                    .await
6749                {
6750                    return;
6751                }
6752                let track = match self.track_handle_or_err(track_name) {
6753                    Ok(track) => track,
6754                    Err(e) => {
6755                        self.notify_clients(Err(e)).await;
6756                        return;
6757                    }
6758                };
6759                let track = track.lock();
6760                if track.audio.processing {
6761                    self.notify_clients(Err(format!(
6762                        "Track '{}' is currently processing audio; stop playback before unloading CLAP plugins",
6763                        track_name
6764                    )))
6765                    .await;
6766                    return;
6767                }
6768                if let Err(e) = track.unload_clap_plugin(plugin_path) {
6769                    self.notify_clients(Err(e)).await;
6770                    return;
6771                }
6772            }
6773            Action::TrackUnloadClapPluginInstance {
6774                ref track_name,
6775                instance_id,
6776            } => {
6777                if self
6778                    .reject_if_track_frozen(track_name, "CLAP plugin unloading")
6779                    .await
6780                {
6781                    return;
6782                }
6783                let track = match self.track_handle_or_err(track_name) {
6784                    Ok(track) => track,
6785                    Err(e) => {
6786                        self.notify_clients(Err(e)).await;
6787                        return;
6788                    }
6789                };
6790                let track = track.lock();
6791                if track.audio.processing {
6792                    self.notify_clients(Err(format!(
6793                        "Track '{}' is currently processing audio; stop playback before unloading CLAP plugins",
6794                        track_name
6795                    )))
6796                    .await;
6797                    return;
6798                }
6799                if let Err(e) = track.unload_clap_plugin_instance(instance_id) {
6800                    self.notify_clients(Err(e)).await;
6801                    return;
6802                }
6803            }
6804            Action::TrackShowClapGui {
6805                ref track_name,
6806                instance_id,
6807            } => {
6808                let track = match self.track_handle_or_err(track_name) {
6809                    Ok(track) => track,
6810                    Err(e) => {
6811                        self.notify_clients(Err(e)).await;
6812                        return;
6813                    }
6814                };
6815                if let Err(e) = track.lock().show_clap_gui(instance_id) {
6816                    self.notify_clients(Err(e)).await;
6817                    return;
6818                }
6819            }
6820            Action::TrackLoadVst3Plugin {
6821                ref track_name,
6822                ref plugin_path,
6823                instance_id,
6824            } => {
6825                if self
6826                    .reject_if_track_frozen(track_name, "VST3 plugin loading")
6827                    .await
6828                {
6829                    return;
6830                }
6831                let track = match self.track_handle_or_err(track_name) {
6832                    Ok(track) => track,
6833                    Err(e) => {
6834                        self.notify_clients(Err(e)).await;
6835                        return;
6836                    }
6837                };
6838                let track = track.lock();
6839                if track.audio.processing {
6840                    self.notify_clients(Err(format!(
6841                        "Track '{}' is currently processing audio; stop playback before loading VST3 plugins",
6842                        track_name
6843                    )))
6844                    .await;
6845                    return;
6846                }
6847                if let Err(e) = track.load_vst3_plugin(plugin_path, instance_id) {
6848                    self.notify_clients(Err(e)).await;
6849                    return;
6850                }
6851                if let Some(instance) = track.vst3_plugins.last()
6852                    && let Some(stderr) = instance.processor.lock().take_stderr()
6853                {
6854                    let source = format!("vst3:{plugin_path}");
6855                    self.spawn_plugin_host_stderr_reader(stderr, source);
6856                }
6857            }
6858            Action::TrackUnloadVst3Plugin {
6859                ref track_name,
6860                ref plugin_path,
6861            } => {
6862                if self
6863                    .reject_if_track_frozen(track_name, "VST3 plugin unloading")
6864                    .await
6865                {
6866                    return;
6867                }
6868                let track = match self.track_handle_or_err(track_name) {
6869                    Ok(track) => track,
6870                    Err(e) => {
6871                        self.notify_clients(Err(e)).await;
6872                        return;
6873                    }
6874                };
6875                let track = track.lock();
6876                if track.audio.processing {
6877                    self.notify_clients(Err(format!(
6878                        "Track '{}' is currently processing audio; stop playback before unloading VST3 plugins",
6879                        track_name
6880                    )))
6881                    .await;
6882                    return;
6883                }
6884                if let Err(e) = track.unload_vst3_plugin(plugin_path) {
6885                    self.notify_clients(Err(e)).await;
6886                    return;
6887                }
6888            }
6889            Action::TrackUnloadVst3PluginInstance {
6890                ref track_name,
6891                instance_id,
6892            } => {
6893                if self
6894                    .reject_if_track_frozen(track_name, "VST3 plugin unloading")
6895                    .await
6896                {
6897                    return;
6898                }
6899                let track = match self.track_handle_or_err(track_name) {
6900                    Ok(track) => track,
6901                    Err(e) => {
6902                        self.notify_clients(Err(e)).await;
6903                        return;
6904                    }
6905                };
6906                let track = track.lock();
6907                if track.audio.processing {
6908                    self.notify_clients(Err(format!(
6909                        "Track '{}' is currently processing audio; stop playback before unloading VST3 plugins",
6910                        track_name
6911                    )))
6912                    .await;
6913                    return;
6914                }
6915                if let Err(e) = track.unload_vst3_plugin_instance(instance_id) {
6916                    self.notify_clients(Err(e)).await;
6917                    return;
6918                }
6919            }
6920            Action::TrackShowVst3Gui {
6921                ref track_name,
6922                instance_id,
6923            } => {
6924                let track = match self.track_handle_or_err(track_name) {
6925                    Ok(track) => track,
6926                    Err(e) => {
6927                        self.notify_clients(Err(e)).await;
6928                        return;
6929                    }
6930                };
6931                if let Err(e) = track.lock().show_vst3_gui(instance_id) {
6932                    self.notify_clients(Err(e)).await;
6933                    return;
6934                }
6935            }
6936            #[cfg(all(unix, not(target_os = "macos")))]
6937            Action::TrackLoadLv2Plugin {
6938                ref track_name,
6939                ref plugin_uri,
6940                instance_id,
6941            } => {
6942                if self
6943                    .reject_if_track_frozen(track_name, "LV2 plugin loading")
6944                    .await
6945                {
6946                    return;
6947                }
6948                let track = match self.track_handle_or_err(track_name) {
6949                    Ok(track) => track,
6950                    Err(e) => {
6951                        self.notify_clients(Err(e)).await;
6952                        return;
6953                    }
6954                };
6955                let track = track.lock();
6956                if track.audio.processing {
6957                    self.notify_clients(Err(format!(
6958                        "Track '{}' is currently processing audio; stop playback before loading LV2 plugins",
6959                        track_name
6960                    )))
6961                    .await;
6962                    return;
6963                }
6964                if let Err(e) = track.load_lv2_plugin(plugin_uri, instance_id) {
6965                    self.notify_clients(Err(e)).await;
6966                    return;
6967                }
6968                if let Some(instance) = track.lv2_plugins.last()
6969                    && let Some(stderr) = instance.processor.lock().take_stderr()
6970                {
6971                    let source = format!("lv2:{plugin_uri}");
6972                    self.spawn_plugin_host_stderr_reader(stderr, source);
6973                }
6974            }
6975            #[cfg(all(unix, not(target_os = "macos")))]
6976            Action::TrackUnloadLv2Plugin {
6977                ref track_name,
6978                ref plugin_uri,
6979            } => {
6980                if self
6981                    .reject_if_track_frozen(track_name, "LV2 plugin unloading")
6982                    .await
6983                {
6984                    return;
6985                }
6986                let track = match self.track_handle_or_err(track_name) {
6987                    Ok(track) => track,
6988                    Err(e) => {
6989                        self.notify_clients(Err(e)).await;
6990                        return;
6991                    }
6992                };
6993                let track = track.lock();
6994                if track.audio.processing {
6995                    self.notify_clients(Err(format!(
6996                        "Track '{}' is currently processing audio; stop playback before unloading LV2 plugins",
6997                        track_name
6998                    )))
6999                    .await;
7000                    return;
7001                }
7002                if let Err(e) = track.unload_lv2_plugin(plugin_uri) {
7003                    self.notify_clients(Err(e)).await;
7004                    return;
7005                }
7006            }
7007            #[cfg(all(unix, not(target_os = "macos")))]
7008            Action::TrackUnloadLv2PluginInstance {
7009                ref track_name,
7010                instance_id,
7011            } => {
7012                if self
7013                    .reject_if_track_frozen(track_name, "LV2 plugin unloading")
7014                    .await
7015                {
7016                    return;
7017                }
7018                let track = match self.track_handle_or_err(track_name) {
7019                    Ok(track) => track,
7020                    Err(e) => {
7021                        self.notify_clients(Err(e)).await;
7022                        return;
7023                    }
7024                };
7025                let track = track.lock();
7026                if track.audio.processing {
7027                    self.notify_clients(Err(format!(
7028                        "Track '{}' is currently processing audio; stop playback before unloading LV2 plugins",
7029                        track_name
7030                    )))
7031                    .await;
7032                    return;
7033                }
7034                if let Err(e) = track.unload_lv2_plugin_instance(instance_id) {
7035                    self.notify_clients(Err(e)).await;
7036                    return;
7037                }
7038            }
7039            #[cfg(all(unix, not(target_os = "macos")))]
7040            Action::TrackShowLv2Gui {
7041                ref track_name,
7042                instance_id,
7043            } => {
7044                let track = match self.track_handle_or_err(track_name) {
7045                    Ok(track) => track,
7046                    Err(e) => {
7047                        self.notify_clients(Err(e)).await;
7048                        return;
7049                    }
7050                };
7051                if let Err(e) = track.lock().show_lv2_gui(instance_id) {
7052                    self.notify_clients(Err(e)).await;
7053                    return;
7054                }
7055            }
7056            Action::TrackSetPluginResourceDir {
7057                ref track_name,
7058                instance_id,
7059                ref format,
7060                ref directory,
7061            } => {
7062                let track = match self.track_handle_or_err(track_name) {
7063                    Ok(track) => track,
7064                    Err(e) => {
7065                        self.notify_clients(Err(e)).await;
7066                        return;
7067                    }
7068                };
7069                let dir = std::path::Path::new(directory);
7070                let result = if format.eq_ignore_ascii_case("CLAP") {
7071                    track.lock().set_clap_plugin_resource_dir(instance_id, dir)
7072                } else if format.eq_ignore_ascii_case("LV2") {
7073                    #[cfg(all(unix, not(target_os = "macos")))]
7074                    {
7075                        track.lock().set_lv2_plugin_resource_dir(instance_id, dir)
7076                    }
7077                    #[cfg(not(all(unix, not(target_os = "macos"))))]
7078                    Err("LV2 is not supported on this platform".to_string())
7079                } else {
7080                    Err(format!(
7081                        "Unsupported plugin format for resource dir: {format}"
7082                    ))
7083                };
7084                if let Err(e) = result {
7085                    self.notify_clients(Err(e)).await;
7086                    return;
7087                }
7088            }
7089            Action::TrackClapFileReferences {
7090                ref track_name,
7091                instance_id,
7092                refs: _,
7093            } => match self.track_handle_or_err(track_name) {
7094                Ok(track) => {
7095                    let refs = track.lock().clap_file_references(instance_id).unwrap_or_else(|e| {
7096                        tracing::warn!(track_name = %track_name, instance_id, error = %e, "Failed to enumerate CLAP file references");
7097                        Vec::new()
7098                    });
7099                    self.notify_clients(Ok(Action::TrackClapFileReferences {
7100                        track_name: track_name.clone(),
7101                        instance_id,
7102                        refs,
7103                    }))
7104                    .await;
7105                }
7106                Err(e) => {
7107                    self.notify_clients(Err(e)).await;
7108                }
7109            },
7110            Action::TrackUpdateClapFileReference {
7111                ref track_name,
7112                instance_id,
7113                index,
7114                ref path,
7115            } => {
7116                let track = match self.track_handle_or_err(track_name) {
7117                    Ok(track) => track,
7118                    Err(e) => {
7119                        self.notify_clients(Err(e)).await;
7120                        return;
7121                    }
7122                };
7123                if let Err(e) = track
7124                    .lock()
7125                    .update_clap_file_reference(instance_id, index, path)
7126                {
7127                    self.notify_clients(Err(e)).await;
7128                    return;
7129                }
7130            }
7131            Action::ClipSetPluginResourceDir {
7132                ref track_name,
7133                clip_idx,
7134                instance_id,
7135                ref format,
7136                ref directory,
7137            } => {
7138                let track = match self.track_handle_or_err(track_name) {
7139                    Ok(track) => track,
7140                    Err(e) => {
7141                        self.notify_clients(Err(e)).await;
7142                        return;
7143                    }
7144                };
7145                let dir = std::path::Path::new(directory);
7146                let track = track.lock();
7147                let result = if format.eq_ignore_ascii_case("CLAP") {
7148                    track.clip_set_clap_plugin_resource_dir(clip_idx, instance_id, dir)
7149                } else if format.eq_ignore_ascii_case("LV2") {
7150                    #[cfg(all(unix, not(target_os = "macos")))]
7151                    {
7152                        track.clip_set_lv2_plugin_resource_dir(clip_idx, instance_id, dir)
7153                    }
7154                    #[cfg(not(all(unix, not(target_os = "macos"))))]
7155                    Err("LV2 is not supported on this platform".to_string())
7156                } else {
7157                    Err(format!(
7158                        "Unsupported plugin format for resource dir: {format}"
7159                    ))
7160                };
7161                if let Err(e) = result {
7162                    self.notify_clients(Err(e)).await;
7163                    return;
7164                }
7165            }
7166            Action::ClipClapFileReferences {
7167                ref track_name,
7168                clip_idx,
7169                instance_id,
7170                refs: _,
7171            } => match self.track_handle_or_err(track_name) {
7172                Ok(track) => {
7173                    let track = track.lock();
7174                    let refs = track
7175                        .clip_clap_file_references(clip_idx, instance_id)
7176                        .unwrap_or_else(|e| {
7177                            tracing::warn!(
7178                                track_name = %track_name,
7179                                clip_idx,
7180                                instance_id,
7181                                error = %e,
7182                                "Failed to enumerate clip CLAP file references"
7183                            );
7184                            Vec::new()
7185                        });
7186                    self.notify_clients(Ok(Action::ClipClapFileReferences {
7187                        track_name: track_name.clone(),
7188                        clip_idx,
7189                        instance_id,
7190                        refs,
7191                    }))
7192                    .await;
7193                }
7194                Err(e) => {
7195                    self.notify_clients(Err(e)).await;
7196                }
7197            },
7198            Action::ClipUpdateClapFileReference {
7199                ref track_name,
7200                clip_idx,
7201                instance_id,
7202                index,
7203                ref path,
7204            } => {
7205                let track = match self.track_handle_or_err(track_name) {
7206                    Ok(track) => track,
7207                    Err(e) => {
7208                        self.notify_clients(Err(e)).await;
7209                        return;
7210                    }
7211                };
7212                if let Err(e) =
7213                    track
7214                        .lock()
7215                        .clip_update_clap_file_reference(clip_idx, instance_id, index, path)
7216                {
7217                    self.notify_clients(Err(e)).await;
7218                    return;
7219                }
7220            }
7221            Action::TrackSetClapParameter {
7222                ref track_name,
7223                instance_id,
7224                param_id,
7225                value,
7226            } => {
7227                if self
7228                    .reject_if_track_frozen(track_name, "CLAP parameter changes")
7229                    .await
7230                {
7231                    return;
7232                }
7233                match self.track_handle_or_err(track_name) {
7234                    Ok(track) => {
7235                        if let Err(e) =
7236                            track
7237                                .lock()
7238                                .set_clap_parameter(instance_id, param_id, value)
7239                        {
7240                            self.notify_clients(Err(e)).await;
7241                            return;
7242                        }
7243                        self.notify_clients(Ok(a.clone())).await;
7244                    }
7245                    Err(e) => {
7246                        self.notify_clients(Err(e)).await;
7247                    }
7248                }
7249            }
7250            Action::ClipSetClapParameter {
7251                ref track_name,
7252                clip_idx,
7253                instance_id,
7254                param_id,
7255                value,
7256            } => {
7257                if self
7258                    .reject_if_track_frozen(track_name, "CLAP parameter changes")
7259                    .await
7260                {
7261                    return;
7262                }
7263                match self.track_handle_or_err(track_name) {
7264                    Ok(track) => {
7265                        if let Err(e) = track.lock().clip_set_clap_parameter(
7266                            clip_idx,
7267                            instance_id,
7268                            param_id,
7269                            value,
7270                        ) {
7271                            self.notify_clients(Err(e)).await;
7272                            return;
7273                        }
7274                        self.notify_clients(Ok(a.clone())).await;
7275                    }
7276                    Err(e) => {
7277                        self.notify_clients(Err(e)).await;
7278                    }
7279                }
7280            }
7281            Action::TrackSetClapParameterAt {
7282                ref track_name,
7283                instance_id,
7284                param_id,
7285                value,
7286                frame,
7287            } => {
7288                if self
7289                    .reject_if_track_frozen(track_name, "CLAP parameter changes")
7290                    .await
7291                {
7292                    return;
7293                }
7294                match self.track_handle_or_err(track_name) {
7295                    Ok(track) => {
7296                        if let Err(e) =
7297                            track
7298                                .lock()
7299                                .set_clap_parameter_at(instance_id, param_id, value, frame)
7300                        {
7301                            self.notify_clients(Err(e)).await;
7302                            return;
7303                        }
7304                        self.notify_clients(Ok(a.clone())).await;
7305                    }
7306                    Err(e) => {
7307                        self.notify_clients(Err(e)).await;
7308                    }
7309                }
7310            }
7311            Action::TrackBeginClapParameterEdit {
7312                ref track_name,
7313                instance_id,
7314                param_id,
7315                frame,
7316            } => {
7317                if self
7318                    .reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
7319                    .await
7320                {
7321                    return;
7322                }
7323                match self.track_handle_or_err(track_name) {
7324                    Ok(track) => {
7325                        if let Err(e) =
7326                            track
7327                                .lock()
7328                                .begin_clap_parameter_edit(instance_id, param_id, frame)
7329                        {
7330                            self.notify_clients(Err(e)).await;
7331                            return;
7332                        }
7333                        self.notify_clients(Ok(a.clone())).await;
7334                    }
7335                    Err(e) => {
7336                        self.notify_clients(Err(e)).await;
7337                    }
7338                }
7339            }
7340            Action::TrackEndClapParameterEdit {
7341                ref track_name,
7342                instance_id,
7343                param_id,
7344                frame,
7345            } => {
7346                if self
7347                    .reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
7348                    .await
7349                {
7350                    return;
7351                }
7352                match self.track_handle_or_err(track_name) {
7353                    Ok(track) => {
7354                        if let Err(e) =
7355                            track
7356                                .lock()
7357                                .end_clap_parameter_edit(instance_id, param_id, frame)
7358                        {
7359                            self.notify_clients(Err(e)).await;
7360                            return;
7361                        }
7362                        self.notify_clients(Ok(a.clone())).await;
7363                    }
7364                    Err(e) => {
7365                        self.notify_clients(Err(e)).await;
7366                    }
7367                }
7368            }
7369            Action::TrackGetClapParameters {
7370                ref track_name,
7371                instance_id,
7372            } => match self.track_handle_or_err(track_name) {
7373                Ok(track) => match track.lock().get_clap_parameters(instance_id) {
7374                    Ok(parameters) => {
7375                        self.notify_clients(Ok(Action::TrackClapParameters {
7376                            track_name: track_name.clone(),
7377                            instance_id,
7378                            parameters,
7379                        }))
7380                        .await;
7381                    }
7382                    Err(e) => {
7383                        self.notify_clients(Err(e)).await;
7384                    }
7385                },
7386                Err(e) => {
7387                    self.notify_clients(Err(e)).await;
7388                }
7389            },
7390            Action::TrackClapParameters { .. } => {}
7391            Action::TrackClapSnapshotState {
7392                ref track_name,
7393                instance_id,
7394            } => match self.track_handle_or_err(track_name) {
7395                Ok(track) => {
7396                    let plugin_path = track
7397                        .lock()
7398                        .clap_plugins
7399                        .iter()
7400                        .find(|instance| instance.id == instance_id)
7401                        .map(|instance| instance.processor.lock().path().to_string())
7402                        .unwrap_or_default();
7403                    match track.lock().clap_snapshot_state(instance_id) {
7404                        Ok(state) => {
7405                            self.notify_clients(Ok(Action::TrackClapStateSnapshot {
7406                                track_name: track_name.clone(),
7407                                instance_id,
7408                                plugin_path,
7409                                state,
7410                            }))
7411                            .await;
7412                        }
7413                        Err(e) => {
7414                            self.notify_clients(Err(e)).await;
7415                        }
7416                    }
7417                }
7418                Err(e) => {
7419                    self.notify_clients(Err(e)).await;
7420                }
7421            },
7422            Action::ClipClapSnapshotState {
7423                ref track_name,
7424                clip_idx,
7425                instance_id,
7426            } => match self.track_handle_or_err(track_name) {
7427                Ok(track) => match track.lock().clip_clap_snapshot_state(clip_idx, instance_id) {
7428                    Ok((plugin_path, state)) => {
7429                        self.notify_clients(Ok(Action::ClipClapStateSnapshot {
7430                            track_name: track_name.clone(),
7431                            clip_idx,
7432                            instance_id,
7433                            plugin_path,
7434                            state,
7435                        }))
7436                        .await;
7437                    }
7438                    Err(e) => {
7439                        self.notify_clients(Err(e)).await;
7440                    }
7441                },
7442                Err(e) => {
7443                    self.notify_clients(Err(e)).await;
7444                }
7445            },
7446            Action::TrackClapStateSnapshot { .. } => {}
7447            Action::ClipClapStateSnapshot { .. } => {}
7448            Action::TrackClapStateDirty { .. } => {}
7449            Action::ClipClapStateDirty { .. } => {}
7450            Action::TrackClapRestoreState {
7451                ref track_name,
7452                instance_id,
7453                ref state,
7454            } => {
7455                if self
7456                    .reject_if_track_frozen(track_name, "CLAP state restore")
7457                    .await
7458                {
7459                    return;
7460                }
7461                let track = match self.track_handle_or_err(track_name) {
7462                    Ok(track) => track,
7463                    Err(e) => {
7464                        self.notify_clients(Err(e)).await;
7465                        return;
7466                    }
7467                };
7468                let track = track.lock();
7469                if track.audio.processing {
7470                    self.notify_clients(Err(format!(
7471                        "Track '{}' is currently processing audio; stop playback before restoring CLAP state",
7472                        track_name
7473                    )))
7474                    .await;
7475                    return;
7476                }
7477                if let Err(e) = track.clap_restore_state(instance_id, state) {
7478                    self.notify_clients(Err(e)).await;
7479                    return;
7480                }
7481            }
7482            Action::ClipClapRestoreState {
7483                ref track_name,
7484                clip_idx,
7485                instance_id,
7486                ref state,
7487            } => {
7488                if self
7489                    .reject_if_track_frozen(track_name, "CLAP state restore")
7490                    .await
7491                {
7492                    return;
7493                }
7494                let track = match self.track_handle_or_err(track_name) {
7495                    Ok(track) => track,
7496                    Err(e) => {
7497                        self.notify_clients(Err(e)).await;
7498                        return;
7499                    }
7500                };
7501                let track = track.lock();
7502                if track.audio.processing {
7503                    self.notify_clients(Err(format!(
7504                        "Track '{}' is currently processing audio; stop playback before restoring CLAP state",
7505                        track_name
7506                    )))
7507                    .await;
7508                    return;
7509                }
7510                if let Err(e) = track.clip_clap_restore_state(clip_idx, instance_id, state) {
7511                    self.notify_clients(Err(e)).await;
7512                    return;
7513                }
7514            }
7515            Action::TrackSnapshotAllClapStates { ref track_name } => {
7516                let track = match self.track_handle_or_err(track_name) {
7517                    Ok(track) => track,
7518                    Err(e) => {
7519                        self.notify_clients(Err(e)).await;
7520                        return;
7521                    }
7522                };
7523                let instances: Vec<_> = {
7524                    let locked = track.lock();
7525                    locked
7526                        .clap_plugins
7527                        .iter()
7528                        .map(|i| (i.id, i.processor.lock().path().to_string()))
7529                        .collect()
7530                };
7531                for (instance_id, plugin_path) in instances {
7532                    match track.lock().clap_snapshot_state(instance_id) {
7533                        Ok(state) => {
7534                            self.notify_clients(Ok(Action::TrackClapStateSnapshot {
7535                                track_name: track_name.clone(),
7536                                instance_id,
7537                                plugin_path,
7538                                state,
7539                            }))
7540                            .await;
7541                        }
7542                        Err(_e) => {}
7543                    }
7544                }
7545                self.notify_clients(Ok(Action::TrackSnapshotAllClapStatesDone {
7546                    track_name: track_name.clone(),
7547                }))
7548                .await;
7549            }
7550            Action::TrackSnapshotAllClapStatesDone { .. } => {}
7551            Action::TrackGetVst3Graph { ref track_name } => {
7552                match self.track_handle_or_err(track_name) {
7553                    Ok(track) => {
7554                        let t = track.lock();
7555                        let plugins = t.vst3_graph_plugins();
7556                        let connections = t.vst3_graph_connections();
7557                        self.notify_clients(Ok(Action::TrackVst3Graph {
7558                            track_name: track_name.clone(),
7559                            plugins,
7560                            connections,
7561                        }))
7562                        .await;
7563                    }
7564                    Err(e) => {
7565                        self.notify_clients(Err(e)).await;
7566                    }
7567                }
7568            }
7569            Action::TrackVst3Graph { .. } => {}
7570            Action::TrackSetVst3Parameter {
7571                ref track_name,
7572                instance_id,
7573                param_id,
7574                value,
7575            } => {
7576                if self
7577                    .reject_if_track_frozen(track_name, "VST3 parameter changes")
7578                    .await
7579                {
7580                    return;
7581                }
7582                match self.track_handle_or_err(track_name) {
7583                    Ok(track) => {
7584                        if let Err(e) =
7585                            track
7586                                .lock()
7587                                .set_vst3_parameter(instance_id, param_id, value)
7588                        {
7589                            self.notify_clients(Err(e)).await;
7590                            return;
7591                        }
7592                        self.notify_clients(Ok(a.clone())).await;
7593                    }
7594                    Err(e) => {
7595                        self.notify_clients(Err(e)).await;
7596                    }
7597                }
7598            }
7599            Action::TrackSetPluginBypassed {
7600                ref track_name,
7601                instance_id,
7602                ref format,
7603                bypassed,
7604            } => match self.track_handle_or_err(track_name) {
7605                Ok(track) => {
7606                    let result = match format.as_str() {
7607                        "CLAP" => track.lock().set_clap_plugin_bypassed(instance_id, bypassed),
7608                        "VST3" => track.lock().set_vst3_plugin_bypassed(instance_id, bypassed),
7609                        #[cfg(all(unix, not(target_os = "macos")))]
7610                        "LV2" => track.lock().set_lv2_plugin_bypassed(instance_id, bypassed),
7611                        _ => Err(format!("Unknown plugin format for bypass: {format}")),
7612                    };
7613                    if let Err(e) = result {
7614                        self.notify_clients(Err(e)).await;
7615                        return;
7616                    }
7617                    self.notify_clients(Ok(a.clone())).await;
7618                }
7619                Err(e) => {
7620                    self.notify_clients(Err(e)).await;
7621                }
7622            },
7623            Action::TrackGetVst3Parameters {
7624                ref track_name,
7625                instance_id,
7626            } => match self.track_handle_or_err(track_name) {
7627                Ok(track) => match track.lock().get_vst3_parameters(instance_id) {
7628                    Ok(parameters) => {
7629                        self.notify_clients(Ok(Action::TrackVst3Parameters {
7630                            track_name: track_name.clone(),
7631                            instance_id,
7632                            parameters,
7633                        }))
7634                        .await;
7635                    }
7636                    Err(e) => {
7637                        self.notify_clients(Err(e)).await;
7638                    }
7639                },
7640                Err(e) => {
7641                    self.notify_clients(Err(e)).await;
7642                }
7643            },
7644            Action::TrackVst3Parameters { .. } => {}
7645            Action::TrackVst3SnapshotState {
7646                ref track_name,
7647                instance_id,
7648            } => match self.track_handle_or_err(track_name) {
7649                Ok(track) => match track.lock().vst3_snapshot_state(instance_id) {
7650                    Ok(state) => {
7651                        self.notify_clients(Ok(Action::TrackVst3StateSnapshot {
7652                            track_name: track_name.clone(),
7653                            instance_id,
7654                            state,
7655                        }))
7656                        .await;
7657                    }
7658                    Err(e) => {
7659                        self.notify_clients(Err(e)).await;
7660                    }
7661                },
7662                Err(e) => {
7663                    self.notify_clients(Err(e)).await;
7664                }
7665            },
7666            Action::ClipVst3SnapshotState {
7667                ref track_name,
7668                clip_idx,
7669                instance_id,
7670            } => match self.track_handle_or_err(track_name) {
7671                Ok(track) => match track.lock().clip_vst3_snapshot_state(clip_idx, instance_id) {
7672                    Ok(state) => {
7673                        self.notify_clients(Ok(Action::ClipVst3StateSnapshot {
7674                            track_name: track_name.clone(),
7675                            clip_idx,
7676                            instance_id,
7677                            state,
7678                        }))
7679                        .await;
7680                    }
7681                    Err(e) => {
7682                        self.notify_clients(Err(e)).await;
7683                    }
7684                },
7685                Err(e) => {
7686                    self.notify_clients(Err(e)).await;
7687                }
7688            },
7689            Action::TrackVst3StateSnapshot { .. } => {}
7690            Action::ClipVst3StateSnapshot { .. } => {}
7691            Action::TrackVst3RestoreState {
7692                ref track_name,
7693                instance_id,
7694                ref state,
7695            } => match self.track_handle_or_err(track_name) {
7696                Ok(track) => {
7697                    if let Err(e) = track.lock().vst3_restore_state(instance_id, state) {
7698                        self.notify_clients(Err(e)).await;
7699                        return;
7700                    }
7701                    self.notify_clients(Ok(a.clone())).await;
7702                }
7703                Err(e) => {
7704                    self.notify_clients(Err(e)).await;
7705                }
7706            },
7707            Action::TrackConnectVst3Audio {
7708                ref track_name,
7709                ref from_node,
7710                from_port,
7711                ref to_node,
7712                to_port,
7713            } => {
7714                if self
7715                    .reject_if_track_frozen(track_name, "VST3 routing changes")
7716                    .await
7717                {
7718                    return;
7719                }
7720                match self.track_handle_or_err(track_name) {
7721                    Ok(track) => {
7722                        if let Err(e) = track
7723                            .lock()
7724                            .connect_vst3_audio(from_node, from_port, to_node, to_port)
7725                        {
7726                            self.notify_clients(Err(e)).await;
7727                            return;
7728                        }
7729                        self.notify_clients(Ok(a.clone())).await;
7730                    }
7731                    Err(e) => {
7732                        self.notify_clients(Err(e)).await;
7733                    }
7734                }
7735            }
7736            Action::TrackDisconnectVst3Audio {
7737                ref track_name,
7738                ref from_node,
7739                from_port,
7740                ref to_node,
7741                to_port,
7742            } => {
7743                if self
7744                    .reject_if_track_frozen(track_name, "VST3 routing changes")
7745                    .await
7746                {
7747                    return;
7748                }
7749                match self.track_handle_or_err(track_name) {
7750                    Ok(track) => {
7751                        if let Err(e) = track
7752                            .lock()
7753                            .disconnect_vst3_audio(from_node, from_port, to_node, to_port)
7754                        {
7755                            self.notify_clients(Err(e)).await;
7756                            return;
7757                        }
7758                        self.notify_clients(Ok(a.clone())).await;
7759                    }
7760                    Err(e) => {
7761                        self.notify_clients(Err(e)).await;
7762                    }
7763                }
7764            }
7765            Action::ClipMove {
7766                ref kind,
7767                ref from,
7768                ref to,
7769                copy,
7770            } => {
7771                if let Some(from_track_handle) = self.state.lock().tracks.get(&from.track_name)
7772                    && let Some(to_track_handle) = self.state.lock().tracks.get(&to.track_name)
7773                {
7774                    let from_track = from_track_handle.lock();
7775                    let to_track = to_track_handle.lock();
7776                    match kind {
7777                        Kind::Audio => {
7778                            if from.clip_index >= from_track.audio.clips.len() {
7779                                self.notify_clients(Err(format!(
7780                                    "Clip index {} is too high, as track {} has only {} clips!",
7781                                    from.clip_index,
7782                                    from_track.name.clone(),
7783                                    from_track.audio.clips.len(),
7784                                )))
7785                                .await;
7786                                return;
7787                            }
7788                            if from_track.audio.ins.len() != to_track.audio.ins.len() {
7789                                self.notify_clients(Err(format!(
7790                                    "Cannot move/copy audio clip from '{}' ({} inputs) to '{}' ({} inputs)",
7791                                    from_track.name,
7792                                    from_track.audio.ins.len(),
7793                                    to_track.name,
7794                                    to_track.audio.ins.len()
7795                                )))
7796                                .await;
7797                                return;
7798                            }
7799                            let clip_copy = from_track.audio.clips[from.clip_index].clone();
7800                            if !copy {
7801                                from_track.audio.clips.remove(from.clip_index);
7802                            }
7803                            let mut clip_copy = clip_copy;
7804                            clip_copy.start = to.sample_offset;
7805                            let max_lane = to_track.audio.ins.len().saturating_sub(1);
7806                            clip_copy.input_channel = to.input_channel.min(max_lane);
7807                            to_track.audio.clips.push(clip_copy);
7808                        }
7809                        Kind::MIDI => {
7810                            if from.clip_index >= from_track.midi.clips.len() {
7811                                self.notify_clients(Err(format!(
7812                                    "Clip index {} is too high, as track {} has only {} clips!",
7813                                    from.clip_index,
7814                                    from_track.name.clone(),
7815                                    from_track.midi.clips.len(),
7816                                )))
7817                                .await;
7818                                return;
7819                            }
7820                            let clip_copy = from_track.midi.clips[from.clip_index].clone();
7821                            if !copy {
7822                                from_track.midi.clips.remove(from.clip_index);
7823                            }
7824                            let mut clip_copy = clip_copy;
7825                            clip_copy.start = to.sample_offset;
7826                            let max_lane = to_track.midi.ins.len().saturating_sub(1);
7827                            clip_copy.input_channel = to.input_channel.min(max_lane);
7828                            to_track.midi.clips.push(clip_copy);
7829                        }
7830                    }
7831                }
7832            }
7833            Action::AddClip {
7834                ref name,
7835                ref track_name,
7836                start,
7837                length,
7838                offset,
7839                input_channel,
7840                muted,
7841                ref peaks_file,
7842                kind,
7843                fade_enabled,
7844                fade_in_samples,
7845                fade_out_samples,
7846                ref source_name,
7847                source_offset,
7848                source_length,
7849                ref preview_name,
7850                ref pitch_correction_points,
7851                pitch_correction_frame_likeness,
7852                pitch_correction_inertia_ms,
7853                pitch_correction_formant_compensation,
7854                ref plugin_graph_json,
7855            } => {
7856                self.add_clip_to_track(ClipAddRequest {
7857                    name,
7858                    track_name,
7859                    start,
7860                    length,
7861                    offset,
7862                    input_channel,
7863                    muted,
7864                    peaks_file: peaks_file.clone(),
7865                    kind,
7866                    fade_enabled,
7867                    fade_in_samples,
7868                    fade_out_samples,
7869                    source_name: source_name.clone(),
7870                    source_offset,
7871                    source_length,
7872                    preview_name: preview_name.clone(),
7873                    pitch_correction_points: pitch_correction_points.clone(),
7874                    pitch_correction_frame_likeness,
7875                    pitch_correction_inertia_ms,
7876                    pitch_correction_formant_compensation,
7877                    plugin_graph_json: plugin_graph_json.clone(),
7878                });
7879                if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
7880                    let track_name = track_name.clone();
7881                    tokio::task::spawn_blocking(move || {
7882                        track.lock().preload_clips();
7883                        tracing::debug!("Preloaded clips for track '{}' after AddClip", track_name);
7884                    });
7885                }
7886            }
7887            Action::AddGroupedClip {
7888                ref track_name,
7889                kind,
7890                ref audio_clip,
7891                ref midi_clip,
7892            } => {
7893                self.add_grouped_clip_to_track(
7894                    track_name,
7895                    kind,
7896                    audio_clip.clone(),
7897                    midi_clip.clone(),
7898                );
7899                if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
7900                    let track_name = track_name.clone();
7901                    tokio::task::spawn_blocking(move || {
7902                        track.lock().preload_clips();
7903                        tracing::debug!(
7904                            "Preloaded clips for track '{}' after AddGroupedClip",
7905                            track_name
7906                        );
7907                    });
7908                }
7909            }
7910            Action::RemoveClip {
7911                ref track_name,
7912                kind,
7913                ref clip_indices,
7914            } => {
7915                self.remove_clips_from_track(track_name, kind, clip_indices);
7916            }
7917            Action::RenameClip {
7918                ref track_name,
7919                kind,
7920                clip_index,
7921                ref new_name,
7922            } => {
7923                self.rename_clip_references(track_name, kind, clip_index, new_name);
7924            }
7925            Action::SetClipSourceName {
7926                ref track_name,
7927                kind,
7928                clip_index,
7929                ref name,
7930            } => {
7931                self.set_clip_source_name(track_name, clip_index, kind, name.clone());
7932            }
7933            Action::SetClipFade {
7934                ref track_name,
7935                clip_index,
7936                kind,
7937                fade_enabled,
7938                fade_in_samples,
7939                fade_out_samples,
7940            } => {
7941                self.set_clip_fade(
7942                    track_name,
7943                    clip_index,
7944                    kind,
7945                    fade_enabled,
7946                    fade_in_samples,
7947                    fade_out_samples,
7948                );
7949            }
7950            Action::SetClipBounds {
7951                ref track_name,
7952                clip_index,
7953                kind,
7954                start,
7955                length,
7956                offset,
7957            } => {
7958                self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
7959            }
7960            Action::SyncClipBounds {
7961                ref track_name,
7962                clip_index,
7963                kind,
7964                start,
7965                length,
7966                offset,
7967            } => {
7968                self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
7969            }
7970            Action::SetClipMuted {
7971                ref track_name,
7972                clip_index,
7973                kind,
7974                muted,
7975            } => {
7976                self.set_clip_muted(track_name, clip_index, kind, muted);
7977            }
7978            Action::SetClipPluginGraphJson {
7979                ref track_name,
7980                clip_index,
7981                ref plugin_graph_json,
7982            } => {
7983                self.set_clip_plugin_graph_json(track_name, clip_index, plugin_graph_json.clone());
7984            }
7985            Action::SetClipPitchCorrection {
7986                ref track_name,
7987                clip_index,
7988                ref preview_name,
7989                ref source_name,
7990                source_offset,
7991                source_length,
7992                ref pitch_correction_points,
7993                pitch_correction_frame_likeness,
7994                pitch_correction_inertia_ms,
7995                pitch_correction_formant_compensation,
7996            } => {
7997                self.set_clip_pitch_correction(
7998                    track_name,
7999                    clip_index,
8000                    preview_name.clone(),
8001                    source_name.clone(),
8002                    source_offset,
8003                    source_length,
8004                    pitch_correction_points.clone(),
8005                    pitch_correction_frame_likeness,
8006                    pitch_correction_inertia_ms,
8007                    pitch_correction_formant_compensation,
8008                );
8009            }
8010            Action::Connect {
8011                ref from_track,
8012                from_port,
8013                ref to_track,
8014                to_port,
8015                kind,
8016            } => {
8017                match kind {
8018                    Kind::Audio => {
8019                        let from_audio_io = if from_track == "hw:in" {
8020                            self.hw_input_audio_port(from_port)
8021                        } else {
8022                            self.state
8023                                .lock()
8024                                .tracks
8025                                .get(from_track)
8026                                .and_then(|t| t.lock().audio.outs.get(from_port).cloned())
8027                        };
8028                        let to_audio_io = if to_track == "hw:out" {
8029                            self.hw_output_audio_port(to_port)
8030                        } else {
8031                            self.state
8032                                .lock()
8033                                .tracks
8034                                .get(to_track)
8035                                .and_then(|t| t.lock().audio.ins.get(to_port).cloned())
8036                        };
8037                        match (from_audio_io, to_audio_io) {
8038                            (Some(source), Some(target)) => {
8039                                if from_track != "hw:in"
8040                                    && to_track != "hw:out"
8041                                    && self.check_if_leads_to_kind(
8042                                        Kind::Audio,
8043                                        to_track,
8044                                        from_track,
8045                                    )
8046                                {
8047                                    self.notify_clients(Err(
8048                                        "Circular routing is not allowed!".into()
8049                                    ))
8050                                    .await;
8051                                    return;
8052                                }
8053                                crate::audio::io::AudioIO::connect(&source, &target);
8054                            }
8055                            (None, _) => {
8056                                self.notify_clients(Err(format!(
8057                                    "Source track '{}' not found",
8058                                    from_track
8059                                )))
8060                                .await;
8061                                return;
8062                            }
8063                            (_, None) => {
8064                                self.notify_clients(Err(format!(
8065                                    "Destination track '{}' not found",
8066                                    to_track
8067                                )))
8068                                .await;
8069                                return;
8070                            }
8071                        }
8072                    }
8073                    Kind::MIDI => {
8074                        let from_hw_in_device = Self::midi_hw_in_device(from_track);
8075                        let to_hw_out_device = Self::midi_hw_out_device(to_track);
8076                        let from_is_invalid_hw = Self::midi_hw_out_device(from_track).is_some();
8077                        let to_is_invalid_hw = Self::midi_hw_in_device(to_track).is_some();
8078
8079                        if from_is_invalid_hw || to_is_invalid_hw {
8080                            self.notify_clients(Err(
8081                                "Invalid MIDI hardware connection direction".to_string()
8082                            ))
8083                            .await;
8084                            return;
8085                        }
8086
8087                        if from_hw_in_device.is_none()
8088                            && to_hw_out_device.is_none()
8089                            && self.check_if_leads_to_kind(Kind::MIDI, to_track, from_track)
8090                        {
8091                            self.notify_clients(Err("Circular routing is not allowed!".into()))
8092                                .await;
8093                            return;
8094                        }
8095
8096                        let state = self.state.lock();
8097                        let from_track_handle = state.tracks.get(from_track);
8098                        let to_track_handle = state.tracks.get(to_track);
8099
8100                        if let (Some(from_device), Some(to_device)) =
8101                            (from_hw_in_device, to_hw_out_device)
8102                        {
8103                            let route = MidiHwThruRoute {
8104                                from_device: from_device.to_string(),
8105                                to_device: to_device.to_string(),
8106                            };
8107                            if !self.midi_hw_thru_routes.iter().any(|r| r == &route) {
8108                                self.midi_hw_thru_routes.push(route);
8109                            }
8110                        } else if let Some(device) = from_hw_in_device {
8111                            if let Some(t_t) = to_track_handle {
8112                                if t_t.lock().midi.ins.get(to_port).is_none() {
8113                                    self.notify_clients(Err(format!(
8114                                        "MIDI input port {} not found on track '{}'",
8115                                        to_port, to_track
8116                                    )))
8117                                    .await;
8118                                    return;
8119                                }
8120                                let route = MidiHwInRoute {
8121                                    device: device.to_string(),
8122                                    to_track: to_track.to_string(),
8123                                    to_port,
8124                                };
8125                                if !self.midi_hw_in_routes.iter().any(|r| r == &route) {
8126                                    self.midi_hw_in_routes.push(route);
8127                                }
8128                            } else {
8129                                self.notify_clients(Err(format!(
8130                                    "MIDI destination track not found: {}",
8131                                    to_track
8132                                )))
8133                                .await;
8134                                return;
8135                            }
8136                        } else if let Some(device) = to_hw_out_device {
8137                            if let Some(f_t) = from_track_handle {
8138                                if f_t.lock().midi.outs.get(from_port).is_none() {
8139                                    self.notify_clients(Err(format!(
8140                                        "MIDI output port {} not found on track '{}'",
8141                                        from_port, from_track
8142                                    )))
8143                                    .await;
8144                                    return;
8145                                }
8146                                let route = MidiHwOutRoute {
8147                                    from_track: from_track.to_string(),
8148                                    from_port,
8149                                    device: device.to_string(),
8150                                };
8151                                if !self.midi_hw_out_routes.iter().any(|r| r == &route) {
8152                                    self.midi_hw_out_routes.push(route);
8153                                }
8154                            } else {
8155                                self.notify_clients(Err(format!(
8156                                    "MIDI source track not found: {}",
8157                                    from_track
8158                                )))
8159                                .await;
8160                                return;
8161                            }
8162                        } else {
8163                            match (from_track_handle, to_track_handle) {
8164                                (Some(f_t), Some(t_t)) => {
8165                                    let to_in_res = t_t.lock().midi.ins.get(to_port).cloned();
8166                                    if let Some(to_in) = to_in_res {
8167                                        let from_track = f_t.lock();
8168                                        if let Err(e) =
8169                                            from_track.midi.connect_out(from_port, to_in)
8170                                        {
8171                                            self.notify_clients(Err(e)).await;
8172                                            return;
8173                                        }
8174                                        from_track.invalidate_midi_route_cache();
8175                                    } else {
8176                                        self.notify_clients(Err(format!(
8177                                            "MIDI input port {} not found on track '{}'",
8178                                            to_port, to_track
8179                                        )))
8180                                        .await;
8181                                        return;
8182                                    }
8183                                }
8184                                _ => {
8185                                    self.notify_clients(Err(format!(
8186                                        "MIDI tracks not found: {} or {}",
8187                                        from_track, to_track
8188                                    )))
8189                                    .await;
8190                                    return;
8191                                }
8192                            }
8193                        }
8194                    }
8195                };
8196            }
8197            Action::Disconnect {
8198                ref from_track,
8199                from_port,
8200                ref to_track,
8201                to_port,
8202                kind,
8203            } => {
8204                if kind == Kind::Audio {
8205                    if let Err(e) = self.disconnect_audio_route_and_notify(a.clone()).await {
8206                        self.notify_clients(Err(e)).await;
8207                    }
8208                } else if kind == Kind::MIDI {
8209                    let from_hw_in_device = Self::midi_hw_in_device(from_track);
8210                    let to_hw_out_device = Self::midi_hw_out_device(to_track);
8211
8212                    if let (Some(from_device), Some(to_device)) =
8213                        (from_hw_in_device, to_hw_out_device)
8214                    {
8215                        let before = self.midi_hw_thru_routes.len();
8216                        self.midi_hw_thru_routes.retain(|r| {
8217                            !(r.from_device == from_device && r.to_device == to_device)
8218                        });
8219                        if self.midi_hw_thru_routes.len() < before {
8220                            self.notify_clients(Ok(a.clone())).await;
8221                        } else {
8222                            self.notify_clients(Err(format!(
8223                                "Disconnect failed: MIDI route not found ({} -> {})",
8224                                from_track, to_track
8225                            )))
8226                            .await;
8227                        }
8228                        return;
8229                    }
8230
8231                    if let Some(device) = from_hw_in_device {
8232                        let before = self.midi_hw_in_routes.len();
8233                        self.midi_hw_in_routes.retain(|r| {
8234                            !(r.device == device && r.to_track == *to_track && r.to_port == to_port)
8235                        });
8236                        if self.midi_hw_in_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) = to_hw_out_device {
8249                        let before = self.midi_hw_out_routes.len();
8250                        self.midi_hw_out_routes.retain(|r| {
8251                            !(r.from_track == *from_track
8252                                && r.from_port == from_port
8253                                && r.device == device)
8254                        });
8255                        if self.midi_hw_out_routes.len() < before {
8256                            self.notify_clients(Ok(a.clone())).await;
8257                        } else {
8258                            self.notify_clients(Err(format!(
8259                                "Disconnect failed: MIDI route not found ({} -> {})",
8260                                from_track, to_track
8261                            )))
8262                            .await;
8263                        }
8264                        return;
8265                    }
8266
8267                    let state = self.state.lock();
8268                    if let (Some(f_t), Some(t_t)) =
8269                        (state.tracks.get(from_track), state.tracks.get(to_track))
8270                        && let Some(to_in) = t_t.lock().midi.ins.get(to_port).cloned()
8271                    {
8272                        let from_track = f_t.lock();
8273                        if let Err(e) = from_track.midi.disconnect_out(from_port, &to_in) {
8274                            self.notify_clients(Err(e)).await;
8275                        } else {
8276                            from_track.invalidate_midi_route_cache();
8277                            self.notify_clients(Ok(a.clone())).await;
8278                        }
8279                    } else {
8280                        self.notify_clients(Err(format!(
8281                            "Disconnect failed: MIDI ports not found ({} -> {})",
8282                            from_track, to_track
8283                        )))
8284                        .await;
8285                    }
8286                }
8287            }
8288
8289            Action::OpenAudioDevice {
8290                ref device,
8291                ref input_device,
8292                sample_rate_hz,
8293                bits,
8294                exclusive,
8295                period_frames,
8296                nperiods,
8297                sync_mode,
8298                ..
8299            } => {
8300                #[cfg(unix)]
8301                {
8302                    let request = AudioOpenRequest {
8303                        device,
8304                        input_device: input_device.as_deref(),
8305                        sample_rate_hz,
8306                        bits,
8307                        exclusive,
8308                        period_frames,
8309                        nperiods,
8310                        sync_mode,
8311                    };
8312                    if self.maybe_open_jack_runtime(request).await.is_some() {
8313                        return;
8314                    }
8315                }
8316                let hw_opts = Self::build_hw_options(exclusive, period_frames, nperiods, sync_mode);
8317                let open_result = self
8318                    .open_non_jack_audio_device(
8319                        device,
8320                        input_device.as_deref(),
8321                        sample_rate_hz,
8322                        bits,
8323                        hw_opts,
8324                    )
8325                    .await;
8326                match open_result {
8327                    Ok(()) => {}
8328                    Err(e) => {
8329                        error!("Failed to open audio device: {e}");
8330                        self.notify_clients(Err(e)).await;
8331                        return;
8332                    }
8333                }
8334                self.finalize_open_audio_device().await;
8335                if let Some(hw) = &self.hw_driver {
8336                    let effective_action = {
8337                        let hw = hw.lock();
8338                        Action::OpenAudioDevice {
8339                            device: device.clone(),
8340                            input_device: input_device.clone(),
8341                            sample_rate_hz: hw.sample_rate(),
8342                            bits: hw.sample_bits(),
8343                            exclusive,
8344                            period_frames,
8345                            nperiods,
8346                            sync_mode,
8347                            actual_period_frames: hw.cycle_samples(),
8348                            input_channels: hw.input_channels(),
8349                            output_channels: hw.output_channels(),
8350                            bytes_per_frame: hw.frame_size_bytes(),
8351                        }
8352                    };
8353                    action_to_process = effective_action;
8354                }
8355            }
8356            Action::JackAddAudioInputPort => {
8357                #[cfg(unix)]
8358                {
8359                    if let Some(jack) = self.jack_runtime.clone() {
8360                        let (input_channels, output_channels, rate) = {
8361                            let jack = jack.lock();
8362                            if let Err(e) = jack.add_audio_input_port() {
8363                                self.notify_clients(Err(e)).await;
8364                                return;
8365                            }
8366                            (
8367                                jack.input_channels(),
8368                                jack.output_channels(),
8369                                jack.sample_rate,
8370                            )
8371                        };
8372                        self.publish_hw_infos(input_channels, output_channels, rate)
8373                            .await;
8374                        self.notify_clients(Ok(a.clone())).await;
8375                    } else {
8376                        self.notify_clients(Err(
8377                            "JACK runtime is not active; open the JACK backend first".to_string(),
8378                        ))
8379                        .await;
8380                    }
8381                }
8382                #[cfg(not(unix))]
8383                {
8384                    self.notify_clients(Err(
8385                        "JACK backend is not available on this platform build".to_string(),
8386                    ))
8387                    .await;
8388                }
8389            }
8390            Action::JackRemoveAudioInputPort(_removed_port) => {
8391                #[cfg(unix)]
8392                {
8393                    let removed_port = _removed_port;
8394                    if let Some(jack) = self.jack_runtime.clone() {
8395                        let (removed_port, removed_io) = {
8396                            let jack = jack.lock();
8397                            let removed_port = Some(removed_port);
8398                            let removed_io =
8399                                removed_port.and_then(|port| jack.input_audio_port(port));
8400                            match (removed_port, removed_io) {
8401                                (Some(port), Some(io)) => (port, io),
8402                                _ => {
8403                                    self.notify_clients(Err(
8404                                        "JACK audio input port index is out of range".to_string(),
8405                                    ))
8406                                    .await;
8407                                    return;
8408                                }
8409                            }
8410                        };
8411                        let reindex_notifications =
8412                            self.reindex_notifications_for_removed_hw_input(removed_port);
8413                        for disconnect in
8414                            self.disconnect_actions_for_removed_hw_input(removed_port, &removed_io)
8415                        {
8416                            if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await
8417                            {
8418                                self.notify_clients(Err(e)).await;
8419                                return;
8420                            }
8421                        }
8422                        let (input_channels, output_channels, rate) = {
8423                            let jack = jack.lock();
8424                            if let Err(e) = jack.remove_audio_input_port(removed_port) {
8425                                self.notify_clients(Err(e)).await;
8426                                return;
8427                            }
8428                            (
8429                                jack.input_channels(),
8430                                jack.output_channels(),
8431                                jack.sample_rate,
8432                            )
8433                        };
8434                        for action in reindex_notifications {
8435                            self.notify_clients(Ok(action)).await;
8436                        }
8437                        self.publish_hw_infos(input_channels, output_channels, rate)
8438                            .await;
8439                        self.notify_clients(Ok(a.clone())).await;
8440                    } else {
8441                        self.notify_clients(Err(
8442                            "JACK runtime is not active; open the JACK backend first".to_string(),
8443                        ))
8444                        .await;
8445                    }
8446                }
8447                #[cfg(not(unix))]
8448                {
8449                    self.notify_clients(Err(
8450                        "JACK backend is not available on this platform build".to_string(),
8451                    ))
8452                    .await;
8453                }
8454            }
8455            Action::JackAddAudioOutputPort => {
8456                #[cfg(unix)]
8457                {
8458                    if let Some(jack) = self.jack_runtime.clone() {
8459                        let (input_channels, output_channels, rate) = {
8460                            let jack = jack.lock();
8461                            if let Err(e) = jack.add_audio_output_port() {
8462                                self.notify_clients(Err(e)).await;
8463                                return;
8464                            }
8465                            (
8466                                jack.input_channels(),
8467                                jack.output_channels(),
8468                                jack.sample_rate,
8469                            )
8470                        };
8471                        self.publish_hw_infos(input_channels, output_channels, rate)
8472                            .await;
8473                        self.notify_clients(Ok(a.clone())).await;
8474                    } else {
8475                        self.notify_clients(Err(
8476                            "JACK runtime is not active; open the JACK backend first".to_string(),
8477                        ))
8478                        .await;
8479                    }
8480                }
8481                #[cfg(not(unix))]
8482                {
8483                    self.notify_clients(Err(
8484                        "JACK backend is not available on this platform build".to_string(),
8485                    ))
8486                    .await;
8487                }
8488            }
8489            Action::JackRemoveAudioOutputPort(_removed_port) => {
8490                #[cfg(unix)]
8491                {
8492                    let removed_port = _removed_port;
8493                    if let Some(jack) = self.jack_runtime.clone() {
8494                        let (removed_port, removed_io) = {
8495                            let jack = jack.lock();
8496                            let removed_port = Some(removed_port);
8497                            let removed_io =
8498                                removed_port.and_then(|port| jack.output_audio_port(port));
8499                            match (removed_port, removed_io) {
8500                                (Some(port), Some(io)) => (port, io),
8501                                _ => {
8502                                    self.notify_clients(Err(
8503                                        "JACK audio output port index is out of range".to_string(),
8504                                    ))
8505                                    .await;
8506                                    return;
8507                                }
8508                            }
8509                        };
8510                        let reindex_notifications =
8511                            self.reindex_notifications_for_removed_hw_output(removed_port);
8512                        for disconnect in
8513                            self.disconnect_actions_for_removed_hw_output(removed_port, &removed_io)
8514                        {
8515                            if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await
8516                            {
8517                                self.notify_clients(Err(e)).await;
8518                                return;
8519                            }
8520                        }
8521                        let (input_channels, output_channels, rate) = {
8522                            let jack = jack.lock();
8523                            if let Err(e) = jack.remove_audio_output_port(removed_port) {
8524                                self.notify_clients(Err(e)).await;
8525                                return;
8526                            }
8527                            (
8528                                jack.input_channels(),
8529                                jack.output_channels(),
8530                                jack.sample_rate,
8531                            )
8532                        };
8533                        for action in reindex_notifications {
8534                            self.notify_clients(Ok(action)).await;
8535                        }
8536                        self.publish_hw_infos(input_channels, output_channels, rate)
8537                            .await;
8538                        self.notify_clients(Ok(a.clone())).await;
8539                    } else {
8540                        self.notify_clients(Err(
8541                            "JACK runtime is not active; open the JACK backend first".to_string(),
8542                        ))
8543                        .await;
8544                    }
8545                }
8546                #[cfg(not(unix))]
8547                {
8548                    self.notify_clients(Err(
8549                        "JACK backend is not available on this platform build".to_string(),
8550                    ))
8551                    .await;
8552                }
8553            }
8554            Action::OpenMidiInputDevice(ref device) => {
8555                let midi_hub = self.midi_hub.lock();
8556                if let Err(e) = midi_hub.open_input(device) {
8557                    self.notify_clients(Err(e)).await;
8558                    return;
8559                }
8560            }
8561            Action::OpenMidiOutputDevice(ref device) => {
8562                let midi_hub = self.midi_hub.lock();
8563                if let Err(e) = midi_hub.open_output(device) {
8564                    self.notify_clients(Err(e)).await;
8565                    return;
8566                }
8567            }
8568            Action::RequestSessionDiagnostics => {
8569                let (
8570                    track_count,
8571                    frozen_track_count,
8572                    audio_clip_count,
8573                    midi_clip_count,
8574                    lv2_instance_count,
8575                    vst3_instance_count,
8576                    clap_instance_count,
8577                ) = {
8578                    let tracks = &self.state.lock().tracks;
8579                    let mut track_count = 0usize;
8580                    let mut frozen_track_count = 0usize;
8581                    let mut audio_clip_count = 0usize;
8582                    let mut midi_clip_count = 0usize;
8583                    #[cfg(all(unix, not(target_os = "macos")))]
8584                    let mut lv2_instance_count = 0usize;
8585                    #[cfg(not(all(unix, not(target_os = "macos"))))]
8586                    let lv2_instance_count = 0usize;
8587                    let mut vst3_instance_count = 0usize;
8588                    let mut clap_instance_count = 0usize;
8589                    for track in tracks.values() {
8590                        let t = track.lock();
8591                        track_count += 1;
8592                        if t.frozen {
8593                            frozen_track_count += 1;
8594                        }
8595                        audio_clip_count += t.audio.clips.len();
8596                        midi_clip_count += t.midi.clips.len();
8597                        #[cfg(all(unix, not(target_os = "macos")))]
8598                        {
8599                            lv2_instance_count += t.lv2_plugins.len();
8600                        }
8601                        vst3_instance_count += t.vst3_plugins.len();
8602                        clap_instance_count += t.clap_plugins.len();
8603                    }
8604                    (
8605                        track_count,
8606                        frozen_track_count,
8607                        audio_clip_count,
8608                        midi_clip_count,
8609                        lv2_instance_count,
8610                        vst3_instance_count,
8611                        clap_instance_count,
8612                    )
8613                };
8614                #[cfg(not(all(unix, not(target_os = "macos"))))]
8615                let _lv2_instance_count = lv2_instance_count;
8616                let pending_hw_midi_events = self.pending_hw_midi_events.len()
8617                    + self
8618                        .pending_hw_midi_events_by_device
8619                        .values()
8620                        .map(std::vec::Vec::len)
8621                        .sum::<usize>();
8622                let sample_rate_hz = if let Some(hw) = &self.hw_driver {
8623                    hw.lock().sample_rate() as usize
8624                } else {
8625                    #[cfg(unix)]
8626                    {
8627                        self.jack_runtime
8628                            .as_ref()
8629                            .map(|j| j.lock().sample_rate)
8630                            .unwrap_or(0)
8631                    }
8632                    #[cfg(not(unix))]
8633                    0
8634                };
8635                let cycle_samples = self.current_cycle_samples();
8636                self.notify_clients(Ok(Action::SessionDiagnosticsReport {
8637                    track_count,
8638                    frozen_track_count,
8639                    audio_clip_count,
8640                    midi_clip_count,
8641                    #[cfg(all(unix, not(target_os = "macos")))]
8642                    lv2_instance_count,
8643                    vst3_instance_count,
8644                    clap_instance_count,
8645                    pending_requests: self.pending_requests.len(),
8646                    workers_total: self.workers.len(),
8647                    workers_ready: self.ready_workers.len(),
8648                    pending_hw_midi_events,
8649                    playing: self.playing,
8650                    transport_sample: self.transport_sample,
8651                    tempo_bpm: self.tempo_bpm,
8652                    sample_rate_hz,
8653                    cycle_samples,
8654                }))
8655                .await;
8656            }
8657            Action::RequestMidiLearnMappingsReport => {
8658                let mut lines = Vec::<String>::new();
8659                let fmt_binding = |b: &crate::message::MidiLearnBinding| {
8660                    let device = b.device.as_deref().unwrap_or("*");
8661                    format!("{device} CH{} CC{}", b.channel + 1, b.cc)
8662                };
8663                if let Some(b) = self.global_midi_learn_play_pause.as_ref() {
8664                    lines.push(format!("Global PlayPause: {}", fmt_binding(b)));
8665                }
8666                if let Some(b) = self.global_midi_learn_stop.as_ref() {
8667                    lines.push(format!("Global Stop: {}", fmt_binding(b)));
8668                }
8669                if let Some(b) = self.global_midi_learn_record_toggle.as_ref() {
8670                    lines.push(format!("Global RecordToggle: {}", fmt_binding(b)));
8671                }
8672                for (track_name, track) in self.state.lock().tracks.iter() {
8673                    let t = track.lock();
8674                    if let Some(b) = t.midi_learn_volume.as_ref() {
8675                        lines.push(format!("{} Volume: {}", track_name, fmt_binding(b)));
8676                    }
8677                    if let Some(b) = t.midi_learn_balance.as_ref() {
8678                        lines.push(format!("{} Balance: {}", track_name, fmt_binding(b)));
8679                    }
8680                    if let Some(b) = t.midi_learn_mute.as_ref() {
8681                        lines.push(format!("{} Mute: {}", track_name, fmt_binding(b)));
8682                    }
8683                    if let Some(b) = t.midi_learn_solo.as_ref() {
8684                        lines.push(format!("{} Solo: {}", track_name, fmt_binding(b)));
8685                    }
8686                    if let Some(b) = t.midi_learn_arm.as_ref() {
8687                        lines.push(format!("{} Arm: {}", track_name, fmt_binding(b)));
8688                    }
8689                    if let Some(b) = t.midi_learn_input_monitor.as_ref() {
8690                        lines.push(format!("{} InputMonitor: {}", track_name, fmt_binding(b)));
8691                    }
8692                    if let Some(b) = t.midi_learn_disk_monitor.as_ref() {
8693                        lines.push(format!("{} DiskMonitor: {}", track_name, fmt_binding(b)));
8694                    }
8695                }
8696                if lines.is_empty() {
8697                    lines.push("No MIDI learn mappings configured".to_string());
8698                }
8699                self.notify_clients(Ok(Action::MidiLearnMappingsReport { lines }))
8700                    .await;
8701            }
8702            Action::ClearAllMidiLearnBindings => {
8703                self.pending_midi_learn = None;
8704                self.pending_global_midi_learn = None;
8705                self.global_midi_learn_play_pause = None;
8706                self.global_midi_learn_stop = None;
8707                self.global_midi_learn_record_toggle = None;
8708                self.midi_cc_gate.clear();
8709                for track in self.state.lock().tracks.values() {
8710                    let t = track.lock();
8711                    t.midi_learn_volume = None;
8712                    t.midi_learn_balance = None;
8713                    t.midi_learn_mute = None;
8714                    t.midi_learn_solo = None;
8715                    t.midi_learn_arm = None;
8716                    t.midi_learn_input_monitor = None;
8717                    t.midi_learn_disk_monitor = None;
8718                }
8719            }
8720            #[cfg(all(unix, not(target_os = "macos")))]
8721            Action::TrackLv2PluginControls { .. } => {}
8722            #[cfg(all(unix, not(target_os = "macos")))]
8723            Action::ClipLv2PluginControls { .. } => {}
8724            #[cfg(all(unix, not(target_os = "macos")))]
8725            Action::TrackLv2Midnam { .. } => {}
8726            Action::TrackClapNoteNames { .. } => {}
8727            Action::SessionDiagnosticsReport { .. } => {}
8728            Action::MidiLearnMappingsReport { .. } => {}
8729            Action::HWInfo { .. } => {}
8730            Action::HistoryState { .. } => {}
8731            Action::Undo => {}
8732            Action::Redo => {}
8733            Action::ApplyGroupedActions(_) => {}
8734            _ => {}
8735        }
8736
8737        if let Some(inverse) = inverse_actions {
8738            if let Some(group) = self.history_group.as_mut() {
8739                group.forward_actions.push(action_to_process.clone());
8740                group.inverse_actions.splice(0..0, inverse);
8741            } else {
8742                self.history.record(UndoEntry {
8743                    forward_actions: vec![action_to_process.clone()],
8744                    inverse_actions: inverse,
8745                });
8746            }
8747        }
8748
8749        self.notify_clients(Ok(action_to_process)).await;
8750    }
8751
8752    pub async fn work(&mut self) {
8753        while let Some(message) = self.rx.recv().await {
8754            match message {
8755                Message::Ready(id) => self.push_ready_worker(id),
8756                Message::Finished {
8757                    worker_id,
8758                    task,
8759                    output_linear,
8760                    process_epoch,
8761                    parameter_updates,
8762                } => {
8763                    tracing::debug!(
8764                        "engine received Finished from worker {} for task {:?} (epoch {} vs {})",
8765                        worker_id,
8766                        task,
8767                        process_epoch,
8768                        self.track_process_epoch
8769                    );
8770                    self.push_ready_worker(worker_id);
8771                    let task_key = Self::task_key(&task);
8772                    self.task_processing_started_at.remove(&task_key);
8773                    if process_epoch != self.track_process_epoch {
8774                        if let Some(track) = self
8775                            .state
8776                            .lock()
8777                            .tracks
8778                            .get(&Self::task_track_name(&task))
8779                            .cloned()
8780                        {
8781                            let t = track.lock();
8782                            t.audio.finished = false;
8783                            t.audio.processing = false;
8784                        }
8785                        continue;
8786                    }
8787                    self.cycle_tasks_running
8788                        .retain(|t| Self::task_key(t) != task_key);
8789                    self.cycle_tasks_finished.push(task.clone());
8790                    let track_name = Self::task_track_name(&task);
8791                    self.track_meter_linear_by_track
8792                        .insert(track_name.clone(), output_linear);
8793                    for action in parameter_updates {
8794                        self.notify_clients(Ok(action)).await;
8795                    }
8796                    self.force_stalled_task_completions();
8797                    let all_finished = self.send_tasks().await;
8798                    tracing::debug!(
8799                        "engine after Finished for {}: all_finished={}",
8800                        track_name,
8801                        all_finished
8802                    );
8803                    if all_finished {
8804                        self.on_all_tracks_finished().await;
8805                    }
8806                }
8807                Message::Channel(s) => {
8808                    self.clients.push(s);
8809                }
8810
8811                Message::Request(a) => {
8812                    match a {
8813                        Action::TrackOfflineBounceCancel { track_name } => {
8814                            if let Some(job) = self.offline_bounce_jobs.get(&track_name) {
8815                                job.cancel.store(true, Ordering::Relaxed);
8816                            }
8817                        }
8818                        Action::TrackOfflineBounceCancelAll => {
8819                            for job in self.offline_bounce_jobs.values() {
8820                                job.cancel.store(true, Ordering::Relaxed);
8821                            }
8822                        }
8823                        _ if !self.offline_bounce_jobs.is_empty() => {
8824                            self.pending_requests.push_back(a);
8825                        }
8826                        Action::OpenAudioDevice { .. }
8827                        | Action::OpenMidiInputDevice(_)
8828                        | Action::OpenMidiOutputDevice(_)
8829                        | Action::RequestMeterSnapshot
8830                        | Action::Quit
8831                        | Action::Log { .. }
8832                        | Action::Play
8833                        | Action::Pause
8834                        | Action::Stop
8835                        | Action::TransportPosition(_)
8836                        | Action::JumpToEnd
8837                        | Action::SetLoopEnabled(_)
8838                        | Action::SetLoopRange(_)
8839                        | Action::SetPunchEnabled(_)
8840                        | Action::SetPunchRange(_)
8841                        | Action::SetMetronomeEnabled(_)
8842                        | Action::SetTempo(_)
8843                        | Action::SetTimeSignature { .. }
8844                        | Action::SetOscEnabled(_)
8845                        | Action::SetClipPlaybackEnabled(_)
8846                        | Action::SetRecordEnabled(_)
8847                        | Action::SetStepRecording(_)
8848                        | Action::StepRecordMidiNote { .. }
8849                        | Action::SetSessionPath(_)
8850                        | Action::ClearHistory
8851                        | Action::BeginSessionRestore
8852                        | Action::PianoKey { .. }
8853                        | Action::ModifyMidiNotes { .. }
8854                        | Action::ModifyMidiControllers { .. }
8855                        | Action::DeleteMidiControllers { .. }
8856                        | Action::InsertMidiControllers { .. }
8857                        | Action::DeleteMidiNotes { .. }
8858                        | Action::InsertMidiNotes { .. }
8859                        | Action::SetMidiSysExEvents { .. } => {
8860                            self.handle_request(a).await;
8861                        }
8862                        #[cfg(all(unix, not(target_os = "macos")))]
8863                        Action::ListLv2Plugins => {
8864                            self.handle_request(a).await;
8865                        }
8866                        Action::ListVst3Plugins => {
8867                            self.handle_request(a).await;
8868                        }
8869                        Action::ListClapPlugins => {
8870                            self.handle_request(a).await;
8871                        }
8872                        Action::ListClapPluginsWithCapabilities => {
8873                            self.handle_request(a).await;
8874                        }
8875                        _ => {
8876                            self.pending_requests.push_back(a);
8877                            if self.can_schedule_hw_cycle() {
8878                                self.request_hw_cycle().await;
8879                            } else {
8880                                while let Some(next) = self.pending_requests.pop_front() {
8881                                    self.handle_request(next).await;
8882                                }
8883                            }
8884                        }
8885                    };
8886                    self.publish_clap_state_dirty().await;
8887                }
8888                Message::OfflineBounceFinished { result } => {
8889                    if let Ok(Action::TrackOfflineBounce { track_name, .. }) = &result {
8890                        self.offline_bounce_jobs.remove(track_name);
8891                    }
8892                    self.notify_clients(result).await;
8893                    if self.offline_bounce_jobs.is_empty() {
8894                        while let Some(next) = self.pending_requests.pop_front() {
8895                            self.handle_request(next).await;
8896                        }
8897                    }
8898                }
8899                Message::HWFinished => {
8900                    if !self.awaiting_hwfinished {
8901                        tracing::debug!("HWFinished ignored (not awaiting)");
8902                        continue;
8903                    }
8904                    tracing::debug!("HWFinished handling; playing={}", self.playing);
8905                    self.handling_hwfinished = true;
8906                    self.awaiting_hwfinished = false;
8907                    #[cfg(unix)]
8908                    {
8909                        if let Some(jack) = &self.jack_runtime {
8910                            if !self.pending_hw_midi_out_events.is_empty() {
8911                                let out_events =
8912                                    std::mem::take(&mut self.pending_hw_midi_out_events);
8913                                jack.lock().write_events(&out_events);
8914                            }
8915                            let mut in_events = vec![];
8916                            jack.lock().read_events_into(&mut in_events);
8917                            if !in_events.is_empty() {
8918                                self.pending_hw_midi_events.extend(in_events);
8919                            }
8920                        }
8921                    }
8922                    #[cfg(unix)]
8923                    if self.jack_runtime.is_some() {
8924                        self.sync_from_jack_transport().await;
8925                    }
8926                    while let Some(a) = self.pending_requests.pop_front() {
8927                        self.handle_request(a).await;
8928                    }
8929                    self.apply_mute_solo_policy();
8930                    self.append_recorded_cycle();
8931                    self.flush_completed_recordings().await;
8932                    let hw_in_routes = self.midi_hw_in_routes.clone();
8933                    let pending_hw_in_by_device = self.pending_hw_midi_events_by_device.clone();
8934                    let mut reconfigured_tracks = Vec::new();
8935                    for (track_name, track) in self.state.lock().tracks.iter() {
8936                        let track_lock = track.lock();
8937                        if self.jack_runtime_is_some() {
8938                            if !self.pending_hw_midi_events.is_empty() {
8939                                track_lock.push_hw_midi_events(&self.pending_hw_midi_events);
8940                            }
8941                        } else {
8942                            for route in hw_in_routes.iter().filter(|r| &r.to_track == track_name) {
8943                                if let Some(events) = pending_hw_in_by_device.get(&route.device) {
8944                                    track_lock.push_hw_midi_events_to_port(route.to_port, events);
8945                                }
8946                            }
8947                        }
8948                        if track_lock.setup() {
8949                            reconfigured_tracks.push(track_name.clone());
8950                        }
8951                    }
8952                    self.publish_track_meters().await;
8953                    self.publish_clap_state_dirty().await;
8954                    for track_name in reconfigured_tracks {
8955                        let track = self.state.lock().tracks.get(&track_name).cloned();
8956                        if let Some(track) = track {
8957                            let (plugins, connections, connectable_connections) = {
8958                                let track_lock = track.lock();
8959                                (
8960                                    track_lock.plugin_graph_plugins(),
8961                                    track_lock.plugin_graph_connections(),
8962                                    track_lock.connectable_connections(),
8963                                )
8964                            };
8965                            self.notify_clients(Ok(Action::TrackPluginGraph {
8966                                track_name: track_name.clone(),
8967                                plugins,
8968                                connections,
8969                                connectable_connections,
8970                            }))
8971                            .await;
8972                        }
8973                    }
8974                    self.pending_hw_midi_events.clear();
8975                    self.pending_hw_midi_events_by_device.clear();
8976                    if self.playing {
8977                        if self.transport_panic_flush_pending {
8978                            self.transport_panic_flush_pending = false;
8979                        } else if self.transport_restart_pending {
8980                            self.transport_restart_pending = false;
8981                        } else {
8982                            let next = self
8983                                .transport_sample
8984                                .saturating_add(self.current_cycle_samples());
8985                            let normalized = self.normalize_transport_sample(next);
8986                            let wrapped = normalized != next;
8987                            self.transport_sample = normalized;
8988                            if wrapped {
8989                                if self.notified_loop_wrap_sample == Some(self.transport_sample) {
8990                                    self.notified_loop_wrap_sample = None;
8991                                } else {
8992                                    self.notify_clients(Ok(Action::TransportPosition(
8993                                        self.transport_sample,
8994                                    )))
8995                                    .await;
8996                                }
8997                            }
8998                        }
8999                    }
9000                    {
9001                        let echoes = self.apply_modulators(self.transport_sample);
9002                        for action in echoes {
9003                            self.notify_clients(Ok(action)).await;
9004                        }
9005                    }
9006                    self.invalidate_track_cycle_state();
9007                    let all_finished = self.send_tasks().await;
9008                    tracing::debug!(
9009                        "HWFinished send_tasks finished={} hw_worker={}",
9010                        all_finished,
9011                        self.hw_worker.is_some()
9012                    );
9013                    if all_finished && self.hw_worker.is_some() {
9014                        self.request_hw_cycle().await;
9015                    }
9016                    #[cfg(unix)]
9017                    {
9018                        if self.jack_runtime.is_some() {
9019                            self.awaiting_hwfinished = true;
9020                        }
9021                    }
9022                    self.handling_hwfinished = false;
9023                }
9024                Message::HWMidiEvents(events) => {
9025                    for hw_event in events {
9026                        let thru_targets: Vec<String> = self
9027                            .midi_hw_thru_routes
9028                            .iter()
9029                            .filter(|route| route.from_device == hw_event.device)
9030                            .map(|route| route.to_device.clone())
9031                            .collect();
9032                        for device in thru_targets {
9033                            self.pending_hw_midi_out_events_by_device.push(HwMidiEvent {
9034                                device,
9035                                event: hw_event.event.clone(),
9036                            });
9037                        }
9038                        if hw_event.event.data.len() >= 3 {
9039                            let status = hw_event.event.data[0];
9040                            if status & 0xF0 == 0xB0 {
9041                                let channel = status & 0x0F;
9042                                let cc = hw_event.event.data[1];
9043                                let value = hw_event.event.data[2];
9044                                self.handle_incoming_hw_cc(&hw_event.device, channel, cc, value)
9045                                    .await;
9046                            }
9047                            if self.step_recording_enabled && status & 0xF0 == 0x90 {
9048                                let channel = status & 0x0F;
9049                                let pitch = hw_event.event.data[1];
9050                                let velocity = hw_event.event.data[2];
9051                                if velocity > 0 {
9052                                    self.notify_clients(Ok(Action::StepRecordMidiNote {
9053                                        device: hw_event.device.clone(),
9054                                        channel,
9055                                        pitch,
9056                                        velocity,
9057                                    }))
9058                                    .await;
9059                                }
9060                            }
9061                        }
9062                        self.pending_hw_midi_events_by_device
9063                            .entry(hw_event.device)
9064                            .or_default()
9065                            .push(hw_event.event);
9066                    }
9067                }
9068                _ => {}
9069            }
9070        }
9071    }
9072
9073    fn collect_hw_midi_output_events(&self) -> Vec<MidiEvent> {
9074        let mut events = vec![];
9075        for track in self.state.lock().tracks.values() {
9076            events.extend(
9077                track
9078                    .lock()
9079                    .take_hw_midi_out_events()
9080                    .into_iter()
9081                    .map(|evt| evt.event),
9082            );
9083        }
9084        events.sort_by_key(|a| a.frame);
9085        events
9086    }
9087
9088    fn collect_hw_midi_output_events_by_device(&mut self) -> Vec<HwMidiEvent> {
9089        let mut events = Vec::<HwMidiEvent>::new();
9090        let routes = self.midi_hw_out_routes.clone();
9091        let mut events_by_track = HashMap::<String, Vec<crate::track::HwMidiOutEvent>>::new();
9092        {
9093            let state = self.state.lock();
9094            for route in &routes {
9095                if events_by_track.contains_key(&route.from_track) {
9096                    continue;
9097                }
9098                let Some(track) = state.tracks.get(&route.from_track) else {
9099                    continue;
9100                };
9101                events_by_track.insert(
9102                    route.from_track.clone(),
9103                    track.lock().take_hw_midi_out_events(),
9104                );
9105            }
9106        }
9107
9108        for route in routes {
9109            let Some(track_events) = events_by_track.get(&route.from_track) else {
9110                continue;
9111            };
9112            for hw_event in track_events
9113                .iter()
9114                .filter(|evt| evt.port == route.from_port)
9115            {
9116                self.update_active_hw_notes_for_track(
9117                    &route.from_track,
9118                    &route.device,
9119                    &hw_event.event.data,
9120                );
9121                events.push(HwMidiEvent {
9122                    device: route.device.clone(),
9123                    event: hw_event.event.clone(),
9124                });
9125            }
9126        }
9127        events.sort_by(|a, b| {
9128            a.event
9129                .frame
9130                .cmp(&b.event.frame)
9131                .then_with(|| a.device.cmp(&b.device))
9132        });
9133        events
9134    }
9135}
9136
9137#[cfg(test)]
9138mod tests {
9139    use super::*;
9140    use crate::mutex::UnsafeMutex;
9141    use tokio::sync::mpsc::channel;
9142    use tokio::time::{Duration as TokioDuration, timeout};
9143
9144    #[test]
9145    #[cfg(unix)]
9146    fn jack_transport_sync_decision_starts_and_syncs_position_on_external_play() {
9147        let decision = Engine::jack_transport_sync_decision(false, 128, true, 256, 64);
9148
9149        assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Start));
9150        assert_eq!(decision.position_sync, Some(256));
9151    }
9152
9153    #[test]
9154    #[cfg(unix)]
9155    fn jack_transport_sync_decision_stops_and_syncs_position_on_external_stop() {
9156        let decision = Engine::jack_transport_sync_decision(true, 512, false, 96, 64);
9157
9158        assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Stop));
9159        assert_eq!(decision.position_sync, Some(96));
9160    }
9161
9162    #[test]
9163    #[cfg(unix)]
9164    fn jack_transport_sync_decision_ignores_small_rolling_drift() {
9165        let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1040, 64);
9166
9167        assert_eq!(decision.play_sync, None);
9168        assert_eq!(decision.position_sync, None);
9169    }
9170
9171    #[test]
9172    #[cfg(unix)]
9173    fn jack_transport_sync_decision_syncs_large_rolling_jump() {
9174        let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1200, 64);
9175
9176        assert_eq!(decision.play_sync, None);
9177        assert_eq!(decision.position_sync, Some(1200));
9178    }
9179
9180    #[test]
9181    #[cfg(unix)]
9182    fn jack_transport_sync_decision_syncs_locate_while_stopped() {
9183        let decision = Engine::jack_transport_sync_decision(false, 400, false, 900, 64);
9184
9185        assert_eq!(decision.play_sync, None);
9186        assert_eq!(decision.position_sync, Some(900));
9187    }
9188
9189    fn make_engine_with_client() -> (Engine, tokio::sync::mpsc::Receiver<Message>) {
9190        let (engine_tx, engine_rx) = channel(16);
9191        let mut engine = Engine::new(engine_rx, engine_tx);
9192        let (client_tx, client_rx) = channel(16);
9193        engine.clients.push(client_tx);
9194        (engine, client_rx)
9195    }
9196
9197    fn insert_track(engine: &mut Engine, track: Track) {
9198        engine.state.lock().tracks.insert(
9199            track.name.clone(),
9200            Arc::new(UnsafeMutex::new(Box::new(track))),
9201        );
9202    }
9203
9204    fn osc_packet(address: &str) -> Vec<u8> {
9205        fn push_padded_osc_string(packet: &mut Vec<u8>, value: &str) {
9206            packet.extend_from_slice(value.as_bytes());
9207            packet.push(0);
9208            while !packet.len().is_multiple_of(4) {
9209                packet.push(0);
9210            }
9211        }
9212
9213        let mut packet = Vec::new();
9214        push_padded_osc_string(&mut packet, address);
9215        push_padded_osc_string(&mut packet, ",");
9216        packet
9217    }
9218
9219    #[tokio::test]
9220    async fn set_osc_enabled_starts_and_stops_server() {
9221        let (mut engine, _client_rx) = make_engine_with_client();
9222
9223        engine
9224            .set_osc_enabled_with(true, |tx| OscServer::start_on_addr(tx, "127.0.0.1:0"))
9225            .expect("start osc server on ephemeral port");
9226        assert!(engine.osc_server.is_some());
9227
9228        engine
9229            .set_osc_enabled_with(false, OscServer::start)
9230            .expect("stop osc server");
9231        assert!(engine.osc_server.is_none());
9232    }
9233
9234    #[tokio::test]
9235    async fn osc_server_forwards_transport_packets_to_engine_channel() {
9236        let (tx, mut rx) = channel(4);
9237        let mut server =
9238            OscServer::start_on_addr(tx, "127.0.0.1:0").expect("start osc test server");
9239        let socket = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind sender socket");
9240        let packet = osc_packet("/transport/play");
9241        socket
9242            .send_to(&packet, server.listen_addr())
9243            .expect("send osc packet");
9244
9245        let message = timeout(TokioDuration::from_secs(1), rx.recv())
9246            .await
9247            .expect("packet delivery timeout")
9248            .expect("osc message");
9249        match message {
9250            Message::Request(Action::Play) => {}
9251            other => panic!("unexpected osc message: {other:?}"),
9252        }
9253
9254        server.stop();
9255    }
9256
9257    #[tokio::test]
9258    async fn track_offline_bounce_rejects_zero_length_requests() {
9259        let (mut engine, mut client_rx) = make_engine_with_client();
9260        insert_track(
9261            &mut engine,
9262            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
9263        );
9264
9265        engine
9266            .handle_request(Action::TrackOfflineBounce {
9267                track_name: "track".to_string(),
9268                output_path: "/tmp/out.wav".to_string(),
9269                start_sample: 0,
9270                length_samples: 0,
9271                automation_lanes: vec![],
9272                apply_fader: false,
9273            })
9274            .await;
9275
9276        match client_rx.recv().await.expect("response") {
9277            Message::Response(Err(err)) => {
9278                assert!(err.contains("has no renderable content for offline bounce"));
9279            }
9280            other => panic!("unexpected message: {other:?}"),
9281        }
9282    }
9283
9284    #[tokio::test]
9285    async fn track_offline_bounce_rejects_when_same_track_is_active() {
9286        let (mut engine, mut client_rx) = make_engine_with_client();
9287        engine.offline_bounce_jobs.insert(
9288            "other".to_string(),
9289            OfflineBounceJob {
9290                cancel: Arc::new(AtomicBool::new(false)),
9291            },
9292        );
9293
9294        engine
9295            .handle_request(Action::TrackOfflineBounce {
9296                track_name: "other".to_string(),
9297                output_path: "/tmp/out.wav".to_string(),
9298                start_sample: 0,
9299                length_samples: 128,
9300                automation_lanes: vec![],
9301                apply_fader: false,
9302            })
9303            .await;
9304
9305        match client_rx.recv().await.expect("response") {
9306            Message::Response(Err(err)) => {
9307                assert!(err.contains("already in progress"));
9308            }
9309            other => panic!("unexpected message: {other:?}"),
9310        }
9311    }
9312
9313    #[tokio::test]
9314    async fn track_offline_bounce_allows_different_track_concurrently() {
9315        let (mut engine, _client_rx) = make_engine_with_client();
9316        insert_track(
9317            &mut engine,
9318            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
9319        );
9320        engine.offline_bounce_jobs.insert(
9321            "other".to_string(),
9322            OfflineBounceJob {
9323                cancel: Arc::new(AtomicBool::new(false)),
9324            },
9325        );
9326
9327        engine
9328            .handle_request(Action::TrackOfflineBounce {
9329                track_name: "track".to_string(),
9330                output_path: "/tmp/out.wav".to_string(),
9331                start_sample: 0,
9332                length_samples: 128,
9333                automation_lanes: vec![],
9334                apply_fader: false,
9335            })
9336            .await;
9337
9338        assert!(engine.offline_bounce_jobs.contains_key("other"));
9339        assert_eq!(engine.pending_requests.len(), 1);
9340    }
9341
9342    #[tokio::test]
9343    async fn reject_if_track_frozen_sends_error_and_blocks_operation() {
9344        let (mut engine, mut client_rx) = make_engine_with_client();
9345        let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
9346        track.set_frozen(true);
9347        insert_track(&mut engine, track);
9348
9349        let rejected = engine
9350            .reject_if_track_frozen("track", "arming/disarming")
9351            .await;
9352
9353        assert!(rejected);
9354        match client_rx.recv().await.expect("response") {
9355            Message::Response(Err(err)) => {
9356                assert_eq!(err, "Track 'track' is frozen; arming/disarming is blocked");
9357            }
9358            other => panic!("unexpected message: {other:?}"),
9359        }
9360    }
9361
9362    #[tokio::test]
9363    async fn undo_restores_original_clip_bounds_after_stretch_style_group() {
9364        let (mut engine, _client_rx) = make_engine_with_client();
9365        let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
9366        let mut clip = AudioClip::new("audio/original.wav".to_string(), 100, 220);
9367        clip.offset = 12;
9368        clip.fade_in_samples = 20;
9369        clip.fade_out_samples = 30;
9370        track.audio.clips.push(clip);
9371        insert_track(&mut engine, track);
9372
9373        engine.handle_request(Action::BeginHistoryGroup).await;
9374        engine
9375            .handle_request(Action::SetClipBounds {
9376                track_name: "track".to_string(),
9377                clip_index: 0,
9378                kind: Kind::Audio,
9379                start: 120,
9380                length: 180,
9381                offset: 0,
9382            })
9383            .await;
9384        engine
9385            .handle_request(Action::SetClipSourceName {
9386                track_name: "track".to_string(),
9387                clip_index: 0,
9388                kind: Kind::Audio,
9389                name: "audio/stretched.wav".to_string(),
9390            })
9391            .await;
9392        engine
9393            .handle_request(Action::SetClipFade {
9394                track_name: "track".to_string(),
9395                clip_index: 0,
9396                kind: Kind::Audio,
9397                fade_enabled: true,
9398                fade_in_samples: 12,
9399                fade_out_samples: 12,
9400            })
9401            .await;
9402        engine.handle_request(Action::EndHistoryGroup).await;
9403
9404        engine.handle_request(Action::Undo).await;
9405
9406        let state = engine.state.lock();
9407        let track = state.tracks.get("track").expect("track exists").lock();
9408        let clip = track.audio.clips.first().expect("clip exists");
9409        assert_eq!(clip.name, "audio/original.wav");
9410        assert_eq!(clip.start, 100);
9411        assert_eq!(clip.end, 220);
9412        assert_eq!(clip.end.saturating_sub(clip.start), 120);
9413        assert_eq!(clip.offset, 12);
9414    }
9415
9416    #[tokio::test]
9417    async fn track_offline_bounce_queues_when_no_worker_is_ready() {
9418        let (mut engine, _client_rx) = make_engine_with_client();
9419        insert_track(
9420            &mut engine,
9421            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
9422        );
9423
9424        engine
9425            .handle_request(Action::TrackOfflineBounce {
9426                track_name: "track".to_string(),
9427                output_path: "/tmp/out.wav".to_string(),
9428                start_sample: 0,
9429                length_samples: 128,
9430                automation_lanes: vec![],
9431                apply_fader: false,
9432            })
9433            .await;
9434
9435        assert!(engine.offline_bounce_jobs.is_empty());
9436        assert_eq!(engine.pending_requests.len(), 1);
9437        assert!(matches!(
9438            engine.pending_requests.front(),
9439            Some(Action::TrackOfflineBounce { track_name, length_samples, .. })
9440                if track_name == "track" && *length_samples == 128
9441        ));
9442    }
9443
9444    #[tokio::test]
9445    async fn track_offline_bounce_returns_missing_track_error() {
9446        let (mut engine, mut client_rx) = make_engine_with_client();
9447
9448        engine
9449            .handle_request(Action::TrackOfflineBounce {
9450                track_name: "missing".to_string(),
9451                output_path: "/tmp/out.wav".to_string(),
9452                start_sample: 0,
9453                length_samples: 128,
9454                automation_lanes: vec![],
9455                apply_fader: false,
9456            })
9457            .await;
9458
9459        match client_rx.recv().await.expect("response") {
9460            Message::Response(Err(err)) => {
9461                assert_eq!(err, "Track not found: missing");
9462            }
9463            other => panic!("unexpected message: {other:?}"),
9464        }
9465    }
9466
9467    #[tokio::test]
9468    async fn track_offline_bounce_clears_job_when_worker_send_fails() {
9469        let (mut engine, mut client_rx) = make_engine_with_client();
9470        insert_track(
9471            &mut engine,
9472            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
9473        );
9474        let (worker_tx, worker_rx) = channel(1);
9475        drop(worker_rx);
9476        engine
9477            .workers
9478            .push(WorkerData::new(worker_tx, tokio::spawn(async {})));
9479        engine.ready_workers.push(0);
9480
9481        engine
9482            .handle_request(Action::TrackOfflineBounce {
9483                track_name: "track".to_string(),
9484                output_path: "/tmp/out.wav".to_string(),
9485                start_sample: 0,
9486                length_samples: 128,
9487                automation_lanes: vec![],
9488                apply_fader: false,
9489            })
9490            .await;
9491
9492        assert!(engine.offline_bounce_jobs.is_empty());
9493        match client_rx.recv().await.expect("response") {
9494            Message::Response(Err(err)) => {
9495                assert!(err.contains("Failed to schedule offline bounce"));
9496            }
9497            other => panic!("unexpected message: {other:?}"),
9498        }
9499    }
9500
9501    #[tokio::test]
9502    async fn play_stop_play_keeps_clip_output_audible() {
9503        use crate::audio::clip::AudioClip;
9504        use crate::audio_codec::write_wav_f32;
9505
9506        let (engine_tx, engine_rx) = channel(16);
9507        let mut engine = Engine::new(engine_rx, engine_tx);
9508        let state = engine.state();
9509        let (client_tx, mut client_rx) = channel(16);
9510        engine.clients.push(client_tx);
9511        engine.init().await;
9512
9513        let tmp_dir = std::env::temp_dir().join("maolan_play_stop_play_test");
9514        let _ = std::fs::create_dir_all(&tmp_dir);
9515        let wav_path = tmp_dir.join("tone.wav");
9516        let sample_rate = 48_000u32;
9517        let clip_samples = sample_rate as usize;
9518        let mut samples = Vec::with_capacity(clip_samples);
9519        for i in 0..clip_samples {
9520            let phase = i as f32 / sample_rate as f32 * 2.0 * std::f32::consts::PI * 440.0;
9521            samples.push(phase.sin() * 0.5);
9522        }
9523        write_wav_f32(&wav_path, &samples, 1, sample_rate).expect("write wav");
9524
9525        let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 1024, sample_rate as f64);
9526        let mut clip = AudioClip::new(wav_path.to_string_lossy().to_string(), 0, clip_samples);
9527        clip.fade_enabled = false;
9528        track.audio.clips.push(clip);
9529        track.session_base_dir = Some(tmp_dir.clone());
9530        insert_track(&mut engine, track);
9531
9532        let tx = engine.tx.clone();
9533        let work_handle = tokio::spawn(async move {
9534            engine.work().await;
9535        });
9536
9537        // Wait for worker tasks to start up and send Ready messages.
9538        tokio::time::sleep(TokioDuration::from_millis(100)).await;
9539
9540        async fn drain_responses(
9541            client_rx: &mut tokio::sync::mpsc::Receiver<Message>,
9542            count: usize,
9543        ) {
9544            for _ in 0..count {
9545                let _ = tokio::time::timeout(TokioDuration::from_secs(2), client_rx.recv()).await;
9546            }
9547        }
9548
9549        async fn wait_for_track_processed(
9550            client_rx: &mut tokio::sync::mpsc::Receiver<Message>,
9551            state: &Arc<UnsafeMutex<State>>,
9552        ) -> bool {
9553            let deadline = Instant::now() + Duration::from_secs(5);
9554            while Instant::now() < deadline {
9555                let msg =
9556                    tokio::time::timeout(TokioDuration::from_millis(100), client_rx.recv()).await;
9557                if let Ok(Some(Message::Response(Ok(Action::TransportPosition(_)))))
9558                | Ok(Some(Message::Response(Ok(Action::Play)))) = msg
9559                {
9560                    let track_deadline = Instant::now() + Duration::from_secs(5);
9561                    while Instant::now() < track_deadline {
9562                        if state
9563                            .lock()
9564                            .tracks
9565                            .get("track")
9566                            .map(|t| t.lock().audio.finished)
9567                            .unwrap_or(false)
9568                        {
9569                            return true;
9570                        }
9571                        tokio::time::sleep(TokioDuration::from_millis(10)).await;
9572                    }
9573                }
9574            }
9575            false
9576        }
9577
9578        tx.send(Message::Request(Action::SetClipPlaybackEnabled(true)))
9579            .await
9580            .unwrap();
9581        tx.send(Message::Request(Action::Play)).await.unwrap();
9582        assert!(
9583            wait_for_track_processed(&mut client_rx, &state).await,
9584            "track did not process on first play"
9585        );
9586        let first_peak = {
9587            let state = state.lock();
9588            let track = state.tracks.get("track").expect("track").lock();
9589            let input = track.audio.ins[0].buffer.lock();
9590            crate::simd::peak_abs(input)
9591        };
9592        assert!(
9593            first_peak > 0.001,
9594            "expected audible input on first play, got {first_peak}"
9595        );
9596
9597        tx.send(Message::Request(Action::SetClipPlaybackEnabled(true)))
9598            .await
9599            .unwrap();
9600        tx.send(Message::Request(Action::Stop)).await.unwrap();
9601        drain_responses(&mut client_rx, 2).await;
9602
9603        tx.send(Message::Request(Action::SetClipPlaybackEnabled(true)))
9604            .await
9605            .unwrap();
9606        tx.send(Message::Request(Action::Play)).await.unwrap();
9607        assert!(
9608            wait_for_track_processed(&mut client_rx, &state).await,
9609            "track did not process on second play"
9610        );
9611        let second_peak = {
9612            let state = state.lock();
9613            let track = state.tracks.get("track").expect("track").lock();
9614            let input = track.audio.ins[0].buffer.lock();
9615            crate::simd::peak_abs(input)
9616        };
9617        assert!(
9618            second_peak > 0.001,
9619            "expected audible input on second play after stop, got {second_peak}"
9620        );
9621
9622        let _ = tx.send(Message::Request(Action::Quit)).await;
9623        tokio::time::sleep(TokioDuration::from_millis(200)).await;
9624        work_handle.abort();
9625        let _ = std::fs::remove_dir_all(&tmp_dir);
9626    }
9627
9628    #[test]
9629    fn modulator_sets_track_volume() {
9630        let (mut engine, _client_rx) = make_engine_with_client();
9631        let track = Track::new("vol-track".to_string(), 0, 2, 0, 0, 128, 48_000.0);
9632        insert_track(&mut engine, track);
9633
9634        engine.modulators = vec![crate::modulator::Modulator {
9635            id: 1,
9636            name: "LFO".to_string(),
9637            shape: crate::modulator::ModulatorShape::Sine,
9638            rate_hz: 1.0,
9639            phase: 0.0,
9640            enabled: true,
9641            targets: vec![crate::modulator::ModulatorTarget::TrackVolume {
9642                track_name: "vol-track".to_string(),
9643                min: -90.0,
9644                max: 20.0,
9645            }],
9646        }];
9647
9648        // At sample 12000 (1/4 period at 48kHz/1Hz), sine value maps to 1.0 -> max 20 dB.
9649        let echoes = engine.apply_modulators(12_000);
9650        let track = engine.state.lock().tracks["vol-track"].lock();
9651        assert!(
9652            (track.level() - 20.0).abs() < 0.01,
9653            "expected 20 dB, got {}",
9654            track.level()
9655        );
9656        assert!(
9657            echoes
9658                .iter()
9659                .any(|a| matches!(a, Action::TrackAutomationLevel(name, _) if name == "vol-track"))
9660        );
9661    }
9662
9663    #[test]
9664    fn modulator_sets_track_balance() {
9665        let (mut engine, _client_rx) = make_engine_with_client();
9666        let track = Track::new("pan-track".to_string(), 0, 2, 0, 0, 128, 48_000.0);
9667        insert_track(&mut engine, track);
9668
9669        engine.modulators = vec![crate::modulator::Modulator {
9670            id: 1,
9671            name: "LFO".to_string(),
9672            shape: crate::modulator::ModulatorShape::Sine,
9673            rate_hz: 1.0,
9674            phase: 0.0,
9675            enabled: true,
9676            targets: vec![crate::modulator::ModulatorTarget::TrackBalance {
9677                track_name: "pan-track".to_string(),
9678                min: -1.0,
9679                max: 1.0,
9680            }],
9681        }];
9682
9683        // At sample 12000 (1/4 period), sine value maps to 1.0 -> max balance 1.0.
9684        let echoes = engine.apply_modulators(12_000);
9685        let track = engine.state.lock().tracks["pan-track"].lock();
9686        assert!(
9687            (track.balance - 1.0).abs() < 0.01,
9688            "expected balance 1.0, got {}",
9689            track.balance
9690        );
9691        assert!(
9692            echoes.iter().any(
9693                |a| matches!(a, Action::TrackAutomationBalance(name, _) if name == "pan-track")
9694            )
9695        );
9696    }
9697
9698    #[tokio::test]
9699    async fn track_set_parent_wires_folder_input_to_child_input_and_child_output_to_folder_output()
9700    {
9701        let (mut engine, mut client_rx) = make_engine_with_client();
9702        let folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9703        let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9704        insert_track(&mut engine, folder);
9705        insert_track(&mut engine, child);
9706
9707        engine
9708            .handle_request_inner(
9709                Action::TrackSetParent {
9710                    track_name: "child".to_string(),
9711                    parent_name: Some("folder".to_string()),
9712                },
9713                false,
9714            )
9715            .await;
9716
9717        // Drain client messages so the channel does not block later drops.
9718        while let Ok(Some(_)) =
9719            tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
9720        {}
9721
9722        let state = engine.state.lock();
9723        let folder = state.tracks.get("folder").unwrap().lock();
9724        let child = state.tracks.get("child").unwrap().lock();
9725
9726        assert!(folder.child_tracks.iter().any(|c| c.lock().name == "child"));
9727        assert_eq!(child.parent_track.as_deref(), Some("folder"));
9728
9729        // Folder input -> child input.
9730        for (i, (parent_in, child_in)) in folder.audio.ins.iter().zip(&child.audio.ins).enumerate()
9731        {
9732            assert!(
9733                child_in
9734                    .connections
9735                    .lock()
9736                    .iter()
9737                    .any(|c| Arc::ptr_eq(c, parent_in)),
9738                "folder input {i} is not routed to child input {i}"
9739            );
9740            assert!(
9741                !parent_in
9742                    .connections
9743                    .lock()
9744                    .iter()
9745                    .any(|c| Arc::ptr_eq(c, child_in)),
9746                "folder input {i} should not read from child input {i}"
9747            );
9748        }
9749
9750        // Child output -> folder output.
9751        for (i, (child_out, parent_out)) in
9752            child.audio.outs.iter().zip(&folder.audio.outs).enumerate()
9753        {
9754            assert!(
9755                parent_out
9756                    .connections
9757                    .lock()
9758                    .iter()
9759                    .any(|c| Arc::ptr_eq(c, child_out)),
9760                "child output {i} is not routed to folder output {i}"
9761            );
9762        }
9763
9764        // Child passthrough is restored so audio can flow through.
9765        for (i, child_out) in child.audio.outs.iter().enumerate() {
9766            assert!(
9767                child_out.connections.lock().iter().any(|c| {
9768                    child
9769                        .audio
9770                        .ins
9771                        .get(i)
9772                        .is_some_and(|inp| Arc::ptr_eq(c, inp))
9773                }),
9774                "child output {i} is not connected to child input {i}"
9775            );
9776        }
9777    }
9778
9779    #[tokio::test]
9780    async fn track_set_parent_to_none_restores_root_passthrough() {
9781        let (mut engine, mut client_rx) = make_engine_with_client();
9782        let folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9783        let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9784        insert_track(&mut engine, folder);
9785        insert_track(&mut engine, child);
9786
9787        engine
9788            .handle_request_inner(
9789                Action::TrackSetParent {
9790                    track_name: "child".to_string(),
9791                    parent_name: Some("folder".to_string()),
9792                },
9793                false,
9794            )
9795            .await;
9796        engine
9797            .handle_request_inner(
9798                Action::TrackSetParent {
9799                    track_name: "child".to_string(),
9800                    parent_name: None,
9801                },
9802                false,
9803            )
9804            .await;
9805
9806        while let Ok(Some(_)) =
9807            tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
9808        {}
9809
9810        let state = engine.state.lock();
9811        let folder = state.tracks.get("folder").unwrap().lock();
9812        let child = state.tracks.get("child").unwrap().lock();
9813
9814        assert!(folder.child_tracks.is_empty());
9815        assert!(child.parent_track.is_none());
9816
9817        for (i, child_out) in child.audio.outs.iter().enumerate() {
9818            assert!(
9819                child_out.connections.lock().iter().any(|c| {
9820                    child
9821                        .audio
9822                        .ins
9823                        .get(i)
9824                        .is_some_and(|inp| Arc::ptr_eq(c, inp))
9825                }),
9826                "child output {i} should be connected to child input {i} after moving to root"
9827            );
9828        }
9829    }
9830
9831    #[tokio::test]
9832    async fn track_set_parent_wires_folder_midi_to_child_midi() {
9833        let (mut engine, mut client_rx) = make_engine_with_client();
9834        let folder = Track::new_folder("folder".to_string(), 0, 0, 1, 1, 64, 48_000.0);
9835        let child = Track::new("child".to_string(), 0, 0, 1, 1, 64, 48_000.0);
9836        insert_track(&mut engine, folder);
9837        insert_track(&mut engine, child);
9838
9839        engine
9840            .handle_request_inner(
9841                Action::TrackSetParent {
9842                    track_name: "child".to_string(),
9843                    parent_name: Some("folder".to_string()),
9844                },
9845                false,
9846            )
9847            .await;
9848
9849        while let Ok(Some(_)) =
9850            tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
9851        {}
9852
9853        let state = engine.state.lock();
9854        let folder = state.tracks.get("folder").unwrap().lock();
9855        let child = state.tracks.get("child").unwrap().lock();
9856
9857        let folder_midi_in = &folder.midi.ins[0];
9858        let child_midi_in = &child.midi.ins[0];
9859        assert!(
9860            child_midi_in
9861                .lock()
9862                .connections
9863                .iter()
9864                .any(|c| Arc::ptr_eq(c, folder_midi_in)),
9865            "folder MIDI input should be routed to child MIDI input"
9866        );
9867
9868        let child_midi_out = &child.midi.outs[0];
9869        let folder_midi_out = &folder.midi.outs[0];
9870        assert!(
9871            child_midi_out
9872                .lock()
9873                .connections
9874                .iter()
9875                .any(|c| Arc::ptr_eq(c, folder_midi_out)),
9876            "child MIDI output should be routed to folder MIDI output"
9877        );
9878    }
9879
9880    #[test]
9881    fn nested_folder_expands_in_task_graph() {
9882        let (mut engine, _client_rx) = make_engine_with_client();
9883        let outer = Track::new_folder("outer".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9884        let inner = Track::new_folder("inner".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9885        let leaf = Track::new("leaf".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9886        insert_track(&mut engine, outer);
9887        insert_track(&mut engine, inner);
9888        insert_track(&mut engine, leaf);
9889
9890        {
9891            let state = engine.state.lock();
9892            let outer = state.tracks.get("outer").unwrap().clone();
9893            let inner = state.tracks.get("inner").unwrap().clone();
9894            let leaf = state.tracks.get("leaf").unwrap().clone();
9895            outer.lock().child_tracks.push(inner.clone());
9896            inner.lock().child_tracks.push(leaf.clone());
9897            inner.lock().parent_track = Some("outer".to_string());
9898            leaf.lock().parent_track = Some("inner".to_string());
9899        }
9900
9901        let (tasks, deps) = engine.build_task_graph();
9902        let names: Vec<String> = tasks
9903            .iter()
9904            .map(|t| match t {
9905                ProcessTask::Track(t) => format!("track:{}", t.lock().name.clone()),
9906                ProcessTask::FolderInput(t) => format!("in:{}", t.lock().name.clone()),
9907                ProcessTask::FolderOutput(t) => format!("out:{}", t.lock().name.clone()),
9908                ProcessTask::Plugin { track, .. } => {
9909                    format!("plugin:{}", track.lock().name.clone())
9910                }
9911            })
9912            .collect();
9913
9914        let expected = vec![
9915            "in:outer",
9916            "in:inner",
9917            "track:leaf",
9918            "out:inner",
9919            "out:outer",
9920        ];
9921        assert_eq!(names, expected, "task graph should expand nested folders");
9922
9923        // Each task should depend on the previous one.
9924        for window in tasks.windows(2) {
9925            let prev = &window[0];
9926            let next = &window[1];
9927            let prev_key = Engine::task_key(prev);
9928            let next_key = Engine::task_key(next);
9929            assert!(
9930                deps.get(&next_key).is_some_and(|d| d.contains(&prev_key)),
9931                "{:?} should depend on {:?}",
9932                next,
9933                prev
9934            );
9935        }
9936    }
9937
9938    #[test]
9939    fn child_to_plugin_to_folder_output_task_graph_has_no_cycle() {
9940        use crate::message::ConnectableRef;
9941
9942        let plugin_path = Path::new(env!("CARGO_MANIFEST_DIR"))
9943            .parent()
9944            .unwrap()
9945            .join("daw")
9946            .join("plugin-host")
9947            .join("tests")
9948            .join("test_passthrough.clap");
9949        if !plugin_path.exists() {
9950            return;
9951        }
9952        if crate::plugins::ipc::find_plugin_host_binary().is_none() {
9953            return;
9954        }
9955
9956        let (mut engine, _client_rx) = make_engine_with_client();
9957        let mut folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9958        let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9959
9960        folder
9961            .load_clap_plugin(
9962                &format!("{}::com.maolan.test.passthrough", plugin_path.display()),
9963                None,
9964            )
9965            .expect("should load CLAP plugin on folder");
9966        folder.clap_plugins[0].processor.lock().setup_audio_ports();
9967        let plugin_id = folder.clap_plugins[0].id;
9968
9969        insert_track(&mut engine, folder);
9970        insert_track(&mut engine, child);
9971
9972        {
9973            let state = engine.state.lock();
9974            let folder = state.tracks.get("folder").unwrap().clone();
9975            let child = state.tracks.get("child").unwrap().clone();
9976            folder.lock().child_tracks.push(child.clone());
9977            child.lock().parent_track = Some("folder".to_string());
9978
9979            folder
9980                .lock()
9981                .connect_audio_connectable(
9982                    ConnectableRef::ChildTrack("child".to_string()),
9983                    0,
9984                    ConnectableRef::ClapPlugin(plugin_id),
9985                    0,
9986                )
9987                .expect("connect child L to plugin L");
9988            folder
9989                .lock()
9990                .connect_audio_connectable(
9991                    ConnectableRef::ChildTrack("child".to_string()),
9992                    1,
9993                    ConnectableRef::ClapPlugin(plugin_id),
9994                    1,
9995                )
9996                .expect("connect child R to plugin R");
9997            folder
9998                .lock()
9999                .connect_audio_connectable(
10000                    ConnectableRef::ClapPlugin(plugin_id),
10001                    0,
10002                    ConnectableRef::TrackOutput,
10003                    0,
10004                )
10005                .expect("connect plugin L to folder output L");
10006            folder
10007                .lock()
10008                .connect_audio_connectable(
10009                    ConnectableRef::ClapPlugin(plugin_id),
10010                    1,
10011                    ConnectableRef::TrackOutput,
10012                    1,
10013                )
10014                .expect("connect plugin R to folder output R");
10015        }
10016
10017        let (tasks, deps) = engine.build_task_graph();
10018
10019        let folder_in_key = tasks
10020            .iter()
10021            .find(|t| matches!(t, ProcessTask::FolderInput(t) if t.lock().name == "folder"))
10022            .map(Engine::task_key)
10023            .expect("folder input task");
10024        let child_key = tasks
10025            .iter()
10026            .find(|t| matches!(t, ProcessTask::Track(t) if t.lock().name == "child"))
10027            .map(Engine::task_key)
10028            .expect("child task");
10029        let plugin_key = tasks
10030            .iter()
10031            .find(|t| {
10032                matches!(
10033                    t,
10034                    ProcessTask::Plugin {
10035                        track,
10036                        kind: PluginKind::Clap,
10037                        index: 0,
10038                    } if track.lock().name == "folder"
10039                )
10040            })
10041            .map(Engine::task_key)
10042            .expect("plugin task");
10043        let folder_out_key = tasks
10044            .iter()
10045            .find(|t| matches!(t, ProcessTask::FolderOutput(t) if t.lock().name == "folder"))
10046            .map(Engine::task_key)
10047            .expect("folder output task");
10048
10049        assert!(
10050            deps.get(&child_key)
10051                .is_some_and(|d| d.contains(&folder_in_key)),
10052            "child task should depend on folder input"
10053        );
10054        assert!(
10055            deps.get(&plugin_key)
10056                .is_some_and(|d| d.contains(&folder_in_key) && d.contains(&child_key)),
10057            "plugin task should depend on folder input and child"
10058        );
10059        assert!(
10060            deps.get(&folder_out_key).is_some_and(|d| {
10061                d.contains(&folder_in_key) && d.contains(&plugin_key) && d.contains(&child_key)
10062            }),
10063            "folder output should depend on folder input, plugin, and child"
10064        );
10065
10066        fn has_cycle(deps: &HashMap<String, Vec<String>>) -> bool {
10067            let mut state: HashMap<String, u8> = HashMap::new();
10068            fn visit(
10069                node: &str,
10070                deps: &HashMap<String, Vec<String>>,
10071                state: &mut HashMap<String, u8>,
10072            ) -> bool {
10073                match state.get(node).copied() {
10074                    Some(1) => return true,
10075                    Some(2) => return false,
10076                    _ => {}
10077                }
10078                state.insert(node.to_string(), 1);
10079                for next in deps.get(node).into_iter().flatten() {
10080                    if visit(next, deps, state) {
10081                        return true;
10082                    }
10083                }
10084                state.insert(node.to_string(), 2);
10085                false
10086            }
10087            for node in deps.keys() {
10088                if visit(node, deps, &mut state) {
10089                    return true;
10090                }
10091            }
10092            false
10093        }
10094
10095        assert!(
10096            !has_cycle(&deps),
10097            "task graph should not contain a cycle when a plugin reads from a child track"
10098        );
10099    }
10100
10101    #[tokio::test]
10102    async fn track_set_parent_wires_child_io_to_folder_even_after_addtrack() {
10103        let (mut engine, mut client_rx) = make_engine_with_client();
10104        let folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
10105        let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
10106        insert_track(&mut engine, folder);
10107        insert_track(&mut engine, child);
10108
10109        engine
10110            .handle_request_inner(
10111                Action::TrackSetParent {
10112                    track_name: "child".to_string(),
10113                    parent_name: Some("folder".to_string()),
10114                },
10115                false,
10116            )
10117            .await;
10118
10119        while let Ok(Some(_)) =
10120            tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
10121        {}
10122
10123        let state = engine.state.lock();
10124        let folder = state.tracks.get("folder").unwrap().lock();
10125        let child = state.tracks.get("child").unwrap().lock();
10126
10127        // Folder input -> child input.
10128        for (i, (parent_in, child_in)) in folder.audio.ins.iter().zip(&child.audio.ins).enumerate()
10129        {
10130            assert!(
10131                child_in
10132                    .connections
10133                    .lock()
10134                    .iter()
10135                    .any(|c| Arc::ptr_eq(c, parent_in)),
10136                "folder input {i} is not routed to child input {i}"
10137            );
10138        }
10139
10140        // Child output -> folder output.
10141        for (i, (child_out, parent_out)) in
10142            child.audio.outs.iter().zip(&folder.audio.outs).enumerate()
10143        {
10144            assert!(
10145                parent_out
10146                    .connections
10147                    .lock()
10148                    .iter()
10149                    .any(|c| Arc::ptr_eq(c, child_out)),
10150                "child output {i} is not routed to folder output {i}"
10151            );
10152        }
10153    }
10154
10155    #[tokio::test]
10156    async fn folder_child_audio_passes_through() {
10157        let (mut engine, mut client_rx) = make_engine_with_client();
10158        let folder = Track::new_folder("folder".to_string(), 1, 1, 0, 0, 64, 48_000.0);
10159        let child = Track::new("child".to_string(), 1, 1, 0, 0, 64, 48_000.0);
10160        insert_track(&mut engine, folder);
10161        insert_track(&mut engine, child);
10162
10163        engine
10164            .handle_request_inner(
10165                Action::TrackSetParent {
10166                    track_name: "child".to_string(),
10167                    parent_name: Some("folder".to_string()),
10168                },
10169                false,
10170            )
10171            .await;
10172        while let Ok(Some(_)) =
10173            tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
10174        {}
10175
10176        {
10177            let state = engine.state.lock();
10178            let folder = state.tracks.get("folder").unwrap().clone();
10179            let child = state.tracks.get("child").unwrap().clone();
10180
10181            folder.lock().input_monitor = vec![true];
10182            child.lock().input_monitor = vec![true];
10183
10184            // Feed a signal into the folder input from an external source.
10185            let source = Arc::new(crate::audio::io::AudioIO::new(64));
10186            for sample in source.buffer.lock().iter_mut() {
10187                *sample = 0.75;
10188            }
10189            crate::audio::io::AudioIO::connect(&source, &folder.lock().audio.ins[0]);
10190
10191            folder.lock().process_folder_input();
10192            child.lock().process();
10193            folder.lock().process_folder_output();
10194
10195            let output = folder.lock().audio.outs[0].buffer.lock();
10196            assert!(
10197                output.iter().any(|s| (*s - 0.75).abs() < 1e-5),
10198                "folder output should contain the child-processed folder input signal, got {:?}",
10199                output.iter().take(8).collect::<Vec<_>>()
10200            );
10201        }
10202    }
10203
10204    #[tokio::test]
10205    async fn remove_folder_track_deletes_descendants_recursively() {
10206        let (mut engine, mut client_rx) = make_engine_with_client();
10207        let folder = Track::new_folder("folder".to_string(), 1, 1, 0, 0, 64, 48_000.0);
10208        let child = Track::new_folder("child".to_string(), 1, 1, 0, 0, 64, 48_000.0);
10209        let grandchild = Track::new("grandchild".to_string(), 1, 1, 0, 0, 64, 48_000.0);
10210        insert_track(&mut engine, folder);
10211        insert_track(&mut engine, child);
10212        insert_track(&mut engine, grandchild);
10213
10214        engine
10215            .handle_request(Action::TrackSetParent {
10216                track_name: "child".to_string(),
10217                parent_name: Some("folder".to_string()),
10218            })
10219            .await;
10220        engine
10221            .handle_request(Action::TrackSetParent {
10222                track_name: "grandchild".to_string(),
10223                parent_name: Some("child".to_string()),
10224            })
10225            .await;
10226
10227        // Drain TrackSetParent notifications so we can inspect the removal notifications.
10228        while let Ok(Some(_)) =
10229            tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
10230        {}
10231
10232        engine
10233            .handle_request(Action::RemoveTrack("folder".to_string()))
10234            .await;
10235
10236        {
10237            let state = engine.state.lock();
10238            assert!(
10239                !state.tracks.contains_key("folder"),
10240                "folder should have been removed"
10241            );
10242            assert!(
10243                !state.tracks.contains_key("child"),
10244                "child should have been removed"
10245            );
10246            assert!(
10247                !state.tracks.contains_key("grandchild"),
10248                "grandchild should have been removed"
10249            );
10250        }
10251
10252        let mut removed_names = Vec::new();
10253        for _ in 0..3 {
10254            let msg = tokio::time::timeout(TokioDuration::from_millis(100), client_rx.recv()).await;
10255            if let Ok(Some(Message::Response(Ok(Action::RemoveTrack(name))))) = msg {
10256                removed_names.push(name);
10257            }
10258        }
10259        assert_eq!(
10260            removed_names,
10261            vec!["grandchild", "child", "folder"],
10262            "descendants should be removed before the folder and clients notified"
10263        );
10264    }
10265}