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                        self.notify_clients(Err(e)).await;
6636                    }
6637                }
6638                return;
6639            }
6640            #[cfg(all(unix, not(target_os = "macos")))]
6641            Action::Lv2Plugins(_) => {}
6642            Action::ListVst3Plugins => {
6643                match crate::plugins::scan_plugins::<crate::plugins::types::Vst3PluginInfo>("vst3")
6644                {
6645                    Ok(plugins) => {
6646                        self.notify_clients(Ok(Action::Vst3Plugins(plugins))).await;
6647                    }
6648                    Err(e) => {
6649                        self.notify_clients(Err(e)).await;
6650                    }
6651                }
6652                return;
6653            }
6654            Action::Vst3Plugins(_) => {}
6655            Action::ListClapPlugins => {
6656                match crate::plugins::scan_plugins::<crate::plugins::types::ClapPluginInfo>("clap")
6657                {
6658                    Ok(plugins) => {
6659                        self.notify_clients(Ok(Action::ClapPlugins(plugins))).await;
6660                    }
6661                    Err(e) => {
6662                        self.notify_clients(Err(e)).await;
6663                    }
6664                }
6665                return;
6666            }
6667            Action::ListClapPluginsWithCapabilities => {
6668                match crate::plugins::scan_plugins::<crate::plugins::types::ClapPluginInfo>("clap")
6669                {
6670                    Ok(plugins) => {
6671                        self.notify_clients(Ok(Action::ClapPlugins(plugins))).await;
6672                    }
6673                    Err(e) => {
6674                        self.notify_clients(Err(e)).await;
6675                    }
6676                }
6677                return;
6678            }
6679            Action::ClapPlugins(_) => {}
6680            Action::TrackLoadClapPlugin {
6681                ref track_name,
6682                ref plugin_path,
6683                instance_id,
6684            } => {
6685                if self
6686                    .reject_if_track_frozen(track_name, "CLAP plugin loading")
6687                    .await
6688                {
6689                    return;
6690                }
6691                let track = match self.track_handle_or_err(track_name) {
6692                    Ok(track) => track,
6693                    Err(e) => {
6694                        self.notify_clients(Err(e)).await;
6695                        return;
6696                    }
6697                };
6698                let track = track.lock();
6699                if track.audio.processing {
6700                    self.notify_clients(Err(format!(
6701                        "Track '{}' is currently processing audio; stop playback before loading CLAP plugins",
6702                        track_name
6703                    )))
6704                    .await;
6705                    return;
6706                }
6707                if let Err(e) = track.load_clap_plugin(plugin_path, instance_id) {
6708                    self.notify_clients(Err(e)).await;
6709                    return;
6710                }
6711                self.notify_clients(Ok(Action::Log {
6712                    source: "engine".to_string(),
6713                    message: format!("CLAP plugin loaded on track '{track_name}': {plugin_path}"),
6714                }))
6715                .await;
6716                if let Some(instance) = track.clap_plugins.last()
6717                    && let Some(stderr) = instance.processor.lock().take_stderr()
6718                {
6719                    let source = format!("clap:{plugin_path}");
6720                    self.spawn_plugin_host_stderr_reader(stderr, source);
6721                    self.notify_clients(Ok(Action::Log {
6722                        source: "engine".to_string(),
6723                        message: format!(
6724                            "Attached stderr reader for CLAP plugin on track '{track_name}'"
6725                        ),
6726                    }))
6727                    .await;
6728                }
6729            }
6730            Action::TrackUnloadClapPlugin {
6731                ref track_name,
6732                ref plugin_path,
6733            } => {
6734                if self
6735                    .reject_if_track_frozen(track_name, "CLAP plugin unloading")
6736                    .await
6737                {
6738                    return;
6739                }
6740                let track = match self.track_handle_or_err(track_name) {
6741                    Ok(track) => track,
6742                    Err(e) => {
6743                        self.notify_clients(Err(e)).await;
6744                        return;
6745                    }
6746                };
6747                let track = track.lock();
6748                if track.audio.processing {
6749                    self.notify_clients(Err(format!(
6750                        "Track '{}' is currently processing audio; stop playback before unloading CLAP plugins",
6751                        track_name
6752                    )))
6753                    .await;
6754                    return;
6755                }
6756                if let Err(e) = track.unload_clap_plugin(plugin_path) {
6757                    self.notify_clients(Err(e)).await;
6758                    return;
6759                }
6760            }
6761            Action::TrackUnloadClapPluginInstance {
6762                ref track_name,
6763                instance_id,
6764            } => {
6765                if self
6766                    .reject_if_track_frozen(track_name, "CLAP plugin unloading")
6767                    .await
6768                {
6769                    return;
6770                }
6771                let track = match self.track_handle_or_err(track_name) {
6772                    Ok(track) => track,
6773                    Err(e) => {
6774                        self.notify_clients(Err(e)).await;
6775                        return;
6776                    }
6777                };
6778                let track = track.lock();
6779                if track.audio.processing {
6780                    self.notify_clients(Err(format!(
6781                        "Track '{}' is currently processing audio; stop playback before unloading CLAP plugins",
6782                        track_name
6783                    )))
6784                    .await;
6785                    return;
6786                }
6787                if let Err(e) = track.unload_clap_plugin_instance(instance_id) {
6788                    self.notify_clients(Err(e)).await;
6789                    return;
6790                }
6791            }
6792            Action::TrackShowClapGui {
6793                ref track_name,
6794                instance_id,
6795            } => {
6796                let track = match self.track_handle_or_err(track_name) {
6797                    Ok(track) => track,
6798                    Err(e) => {
6799                        self.notify_clients(Err(e)).await;
6800                        return;
6801                    }
6802                };
6803                if let Err(e) = track.lock().show_clap_gui(instance_id) {
6804                    self.notify_clients(Err(e)).await;
6805                    return;
6806                }
6807            }
6808            Action::TrackLoadVst3Plugin {
6809                ref track_name,
6810                ref plugin_path,
6811                instance_id,
6812            } => {
6813                if self
6814                    .reject_if_track_frozen(track_name, "VST3 plugin loading")
6815                    .await
6816                {
6817                    return;
6818                }
6819                let track = match self.track_handle_or_err(track_name) {
6820                    Ok(track) => track,
6821                    Err(e) => {
6822                        self.notify_clients(Err(e)).await;
6823                        return;
6824                    }
6825                };
6826                let track = track.lock();
6827                if track.audio.processing {
6828                    self.notify_clients(Err(format!(
6829                        "Track '{}' is currently processing audio; stop playback before loading VST3 plugins",
6830                        track_name
6831                    )))
6832                    .await;
6833                    return;
6834                }
6835                if let Err(e) = track.load_vst3_plugin(plugin_path, instance_id) {
6836                    self.notify_clients(Err(e)).await;
6837                    return;
6838                }
6839                if let Some(instance) = track.vst3_plugins.last()
6840                    && let Some(stderr) = instance.processor.lock().take_stderr()
6841                {
6842                    let source = format!("vst3:{plugin_path}");
6843                    self.spawn_plugin_host_stderr_reader(stderr, source);
6844                }
6845            }
6846            Action::TrackUnloadVst3Plugin {
6847                ref track_name,
6848                ref plugin_path,
6849            } => {
6850                if self
6851                    .reject_if_track_frozen(track_name, "VST3 plugin unloading")
6852                    .await
6853                {
6854                    return;
6855                }
6856                let track = match self.track_handle_or_err(track_name) {
6857                    Ok(track) => track,
6858                    Err(e) => {
6859                        self.notify_clients(Err(e)).await;
6860                        return;
6861                    }
6862                };
6863                let track = track.lock();
6864                if track.audio.processing {
6865                    self.notify_clients(Err(format!(
6866                        "Track '{}' is currently processing audio; stop playback before unloading VST3 plugins",
6867                        track_name
6868                    )))
6869                    .await;
6870                    return;
6871                }
6872                if let Err(e) = track.unload_vst3_plugin(plugin_path) {
6873                    self.notify_clients(Err(e)).await;
6874                    return;
6875                }
6876            }
6877            Action::TrackUnloadVst3PluginInstance {
6878                ref track_name,
6879                instance_id,
6880            } => {
6881                if self
6882                    .reject_if_track_frozen(track_name, "VST3 plugin unloading")
6883                    .await
6884                {
6885                    return;
6886                }
6887                let track = match self.track_handle_or_err(track_name) {
6888                    Ok(track) => track,
6889                    Err(e) => {
6890                        self.notify_clients(Err(e)).await;
6891                        return;
6892                    }
6893                };
6894                let track = track.lock();
6895                if track.audio.processing {
6896                    self.notify_clients(Err(format!(
6897                        "Track '{}' is currently processing audio; stop playback before unloading VST3 plugins",
6898                        track_name
6899                    )))
6900                    .await;
6901                    return;
6902                }
6903                if let Err(e) = track.unload_vst3_plugin_instance(instance_id) {
6904                    self.notify_clients(Err(e)).await;
6905                    return;
6906                }
6907            }
6908            Action::TrackShowVst3Gui {
6909                ref track_name,
6910                instance_id,
6911            } => {
6912                let track = match self.track_handle_or_err(track_name) {
6913                    Ok(track) => track,
6914                    Err(e) => {
6915                        self.notify_clients(Err(e)).await;
6916                        return;
6917                    }
6918                };
6919                if let Err(e) = track.lock().show_vst3_gui(instance_id) {
6920                    self.notify_clients(Err(e)).await;
6921                    return;
6922                }
6923            }
6924            #[cfg(all(unix, not(target_os = "macos")))]
6925            Action::TrackLoadLv2Plugin {
6926                ref track_name,
6927                ref plugin_uri,
6928                instance_id,
6929            } => {
6930                if self
6931                    .reject_if_track_frozen(track_name, "LV2 plugin loading")
6932                    .await
6933                {
6934                    return;
6935                }
6936                let track = match self.track_handle_or_err(track_name) {
6937                    Ok(track) => track,
6938                    Err(e) => {
6939                        self.notify_clients(Err(e)).await;
6940                        return;
6941                    }
6942                };
6943                let track = track.lock();
6944                if track.audio.processing {
6945                    self.notify_clients(Err(format!(
6946                        "Track '{}' is currently processing audio; stop playback before loading LV2 plugins",
6947                        track_name
6948                    )))
6949                    .await;
6950                    return;
6951                }
6952                if let Err(e) = track.load_lv2_plugin(plugin_uri, instance_id) {
6953                    self.notify_clients(Err(e)).await;
6954                    return;
6955                }
6956                if let Some(instance) = track.lv2_plugins.last()
6957                    && let Some(stderr) = instance.processor.lock().take_stderr()
6958                {
6959                    let source = format!("lv2:{plugin_uri}");
6960                    self.spawn_plugin_host_stderr_reader(stderr, source);
6961                }
6962            }
6963            #[cfg(all(unix, not(target_os = "macos")))]
6964            Action::TrackUnloadLv2Plugin {
6965                ref track_name,
6966                ref plugin_uri,
6967            } => {
6968                if self
6969                    .reject_if_track_frozen(track_name, "LV2 plugin unloading")
6970                    .await
6971                {
6972                    return;
6973                }
6974                let track = match self.track_handle_or_err(track_name) {
6975                    Ok(track) => track,
6976                    Err(e) => {
6977                        self.notify_clients(Err(e)).await;
6978                        return;
6979                    }
6980                };
6981                let track = track.lock();
6982                if track.audio.processing {
6983                    self.notify_clients(Err(format!(
6984                        "Track '{}' is currently processing audio; stop playback before unloading LV2 plugins",
6985                        track_name
6986                    )))
6987                    .await;
6988                    return;
6989                }
6990                if let Err(e) = track.unload_lv2_plugin(plugin_uri) {
6991                    self.notify_clients(Err(e)).await;
6992                    return;
6993                }
6994            }
6995            #[cfg(all(unix, not(target_os = "macos")))]
6996            Action::TrackUnloadLv2PluginInstance {
6997                ref track_name,
6998                instance_id,
6999            } => {
7000                if self
7001                    .reject_if_track_frozen(track_name, "LV2 plugin unloading")
7002                    .await
7003                {
7004                    return;
7005                }
7006                let track = match self.track_handle_or_err(track_name) {
7007                    Ok(track) => track,
7008                    Err(e) => {
7009                        self.notify_clients(Err(e)).await;
7010                        return;
7011                    }
7012                };
7013                let track = track.lock();
7014                if track.audio.processing {
7015                    self.notify_clients(Err(format!(
7016                        "Track '{}' is currently processing audio; stop playback before unloading LV2 plugins",
7017                        track_name
7018                    )))
7019                    .await;
7020                    return;
7021                }
7022                if let Err(e) = track.unload_lv2_plugin_instance(instance_id) {
7023                    self.notify_clients(Err(e)).await;
7024                    return;
7025                }
7026            }
7027            #[cfg(all(unix, not(target_os = "macos")))]
7028            Action::TrackShowLv2Gui {
7029                ref track_name,
7030                instance_id,
7031            } => {
7032                let track = match self.track_handle_or_err(track_name) {
7033                    Ok(track) => track,
7034                    Err(e) => {
7035                        self.notify_clients(Err(e)).await;
7036                        return;
7037                    }
7038                };
7039                if let Err(e) = track.lock().show_lv2_gui(instance_id) {
7040                    self.notify_clients(Err(e)).await;
7041                    return;
7042                }
7043            }
7044            Action::TrackSetPluginResourceDir {
7045                ref track_name,
7046                instance_id,
7047                ref format,
7048                ref directory,
7049            } => {
7050                let track = match self.track_handle_or_err(track_name) {
7051                    Ok(track) => track,
7052                    Err(e) => {
7053                        self.notify_clients(Err(e)).await;
7054                        return;
7055                    }
7056                };
7057                let dir = std::path::Path::new(directory);
7058                let result = if format.eq_ignore_ascii_case("CLAP") {
7059                    track.lock().set_clap_plugin_resource_dir(instance_id, dir)
7060                } else if format.eq_ignore_ascii_case("LV2") {
7061                    #[cfg(all(unix, not(target_os = "macos")))]
7062                    {
7063                        track.lock().set_lv2_plugin_resource_dir(instance_id, dir)
7064                    }
7065                    #[cfg(not(all(unix, not(target_os = "macos"))))]
7066                    Err("LV2 is not supported on this platform".to_string())
7067                } else {
7068                    Err(format!(
7069                        "Unsupported plugin format for resource dir: {format}"
7070                    ))
7071                };
7072                if let Err(e) = result {
7073                    self.notify_clients(Err(e)).await;
7074                    return;
7075                }
7076            }
7077            Action::TrackClapFileReferences {
7078                ref track_name,
7079                instance_id,
7080                refs: _,
7081            } => match self.track_handle_or_err(track_name) {
7082                Ok(track) => {
7083                    let refs = track.lock().clap_file_references(instance_id).unwrap_or_else(|e| {
7084                        tracing::warn!(track_name = %track_name, instance_id, error = %e, "Failed to enumerate CLAP file references");
7085                        Vec::new()
7086                    });
7087                    self.notify_clients(Ok(Action::TrackClapFileReferences {
7088                        track_name: track_name.clone(),
7089                        instance_id,
7090                        refs,
7091                    }))
7092                    .await;
7093                }
7094                Err(e) => {
7095                    self.notify_clients(Err(e)).await;
7096                }
7097            },
7098            Action::TrackUpdateClapFileReference {
7099                ref track_name,
7100                instance_id,
7101                index,
7102                ref path,
7103            } => {
7104                let track = match self.track_handle_or_err(track_name) {
7105                    Ok(track) => track,
7106                    Err(e) => {
7107                        self.notify_clients(Err(e)).await;
7108                        return;
7109                    }
7110                };
7111                if let Err(e) = track
7112                    .lock()
7113                    .update_clap_file_reference(instance_id, index, path)
7114                {
7115                    self.notify_clients(Err(e)).await;
7116                    return;
7117                }
7118            }
7119            Action::ClipSetPluginResourceDir {
7120                ref track_name,
7121                clip_idx,
7122                instance_id,
7123                ref format,
7124                ref directory,
7125            } => {
7126                let track = match self.track_handle_or_err(track_name) {
7127                    Ok(track) => track,
7128                    Err(e) => {
7129                        self.notify_clients(Err(e)).await;
7130                        return;
7131                    }
7132                };
7133                let dir = std::path::Path::new(directory);
7134                let track = track.lock();
7135                let result = if format.eq_ignore_ascii_case("CLAP") {
7136                    track.clip_set_clap_plugin_resource_dir(clip_idx, instance_id, dir)
7137                } else if format.eq_ignore_ascii_case("LV2") {
7138                    #[cfg(all(unix, not(target_os = "macos")))]
7139                    {
7140                        track.clip_set_lv2_plugin_resource_dir(clip_idx, instance_id, dir)
7141                    }
7142                    #[cfg(not(all(unix, not(target_os = "macos"))))]
7143                    Err("LV2 is not supported on this platform".to_string())
7144                } else {
7145                    Err(format!(
7146                        "Unsupported plugin format for resource dir: {format}"
7147                    ))
7148                };
7149                if let Err(e) = result {
7150                    self.notify_clients(Err(e)).await;
7151                    return;
7152                }
7153            }
7154            Action::ClipClapFileReferences {
7155                ref track_name,
7156                clip_idx,
7157                instance_id,
7158                refs: _,
7159            } => match self.track_handle_or_err(track_name) {
7160                Ok(track) => {
7161                    let track = track.lock();
7162                    let refs = track
7163                        .clip_clap_file_references(clip_idx, instance_id)
7164                        .unwrap_or_else(|e| {
7165                            tracing::warn!(
7166                                track_name = %track_name,
7167                                clip_idx,
7168                                instance_id,
7169                                error = %e,
7170                                "Failed to enumerate clip CLAP file references"
7171                            );
7172                            Vec::new()
7173                        });
7174                    self.notify_clients(Ok(Action::ClipClapFileReferences {
7175                        track_name: track_name.clone(),
7176                        clip_idx,
7177                        instance_id,
7178                        refs,
7179                    }))
7180                    .await;
7181                }
7182                Err(e) => {
7183                    self.notify_clients(Err(e)).await;
7184                }
7185            },
7186            Action::ClipUpdateClapFileReference {
7187                ref track_name,
7188                clip_idx,
7189                instance_id,
7190                index,
7191                ref path,
7192            } => {
7193                let track = match self.track_handle_or_err(track_name) {
7194                    Ok(track) => track,
7195                    Err(e) => {
7196                        self.notify_clients(Err(e)).await;
7197                        return;
7198                    }
7199                };
7200                if let Err(e) =
7201                    track
7202                        .lock()
7203                        .clip_update_clap_file_reference(clip_idx, instance_id, index, path)
7204                {
7205                    self.notify_clients(Err(e)).await;
7206                    return;
7207                }
7208            }
7209            Action::TrackSetClapParameter {
7210                ref track_name,
7211                instance_id,
7212                param_id,
7213                value,
7214            } => {
7215                if self
7216                    .reject_if_track_frozen(track_name, "CLAP parameter changes")
7217                    .await
7218                {
7219                    return;
7220                }
7221                match self.track_handle_or_err(track_name) {
7222                    Ok(track) => {
7223                        if let Err(e) =
7224                            track
7225                                .lock()
7226                                .set_clap_parameter(instance_id, param_id, value)
7227                        {
7228                            self.notify_clients(Err(e)).await;
7229                            return;
7230                        }
7231                        self.notify_clients(Ok(a.clone())).await;
7232                    }
7233                    Err(e) => {
7234                        self.notify_clients(Err(e)).await;
7235                    }
7236                }
7237            }
7238            Action::ClipSetClapParameter {
7239                ref track_name,
7240                clip_idx,
7241                instance_id,
7242                param_id,
7243                value,
7244            } => {
7245                if self
7246                    .reject_if_track_frozen(track_name, "CLAP parameter changes")
7247                    .await
7248                {
7249                    return;
7250                }
7251                match self.track_handle_or_err(track_name) {
7252                    Ok(track) => {
7253                        if let Err(e) = track.lock().clip_set_clap_parameter(
7254                            clip_idx,
7255                            instance_id,
7256                            param_id,
7257                            value,
7258                        ) {
7259                            self.notify_clients(Err(e)).await;
7260                            return;
7261                        }
7262                        self.notify_clients(Ok(a.clone())).await;
7263                    }
7264                    Err(e) => {
7265                        self.notify_clients(Err(e)).await;
7266                    }
7267                }
7268            }
7269            Action::TrackSetClapParameterAt {
7270                ref track_name,
7271                instance_id,
7272                param_id,
7273                value,
7274                frame,
7275            } => {
7276                if self
7277                    .reject_if_track_frozen(track_name, "CLAP parameter changes")
7278                    .await
7279                {
7280                    return;
7281                }
7282                match self.track_handle_or_err(track_name) {
7283                    Ok(track) => {
7284                        if let Err(e) =
7285                            track
7286                                .lock()
7287                                .set_clap_parameter_at(instance_id, param_id, value, frame)
7288                        {
7289                            self.notify_clients(Err(e)).await;
7290                            return;
7291                        }
7292                        self.notify_clients(Ok(a.clone())).await;
7293                    }
7294                    Err(e) => {
7295                        self.notify_clients(Err(e)).await;
7296                    }
7297                }
7298            }
7299            Action::TrackBeginClapParameterEdit {
7300                ref track_name,
7301                instance_id,
7302                param_id,
7303                frame,
7304            } => {
7305                if self
7306                    .reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
7307                    .await
7308                {
7309                    return;
7310                }
7311                match self.track_handle_or_err(track_name) {
7312                    Ok(track) => {
7313                        if let Err(e) =
7314                            track
7315                                .lock()
7316                                .begin_clap_parameter_edit(instance_id, param_id, frame)
7317                        {
7318                            self.notify_clients(Err(e)).await;
7319                            return;
7320                        }
7321                        self.notify_clients(Ok(a.clone())).await;
7322                    }
7323                    Err(e) => {
7324                        self.notify_clients(Err(e)).await;
7325                    }
7326                }
7327            }
7328            Action::TrackEndClapParameterEdit {
7329                ref track_name,
7330                instance_id,
7331                param_id,
7332                frame,
7333            } => {
7334                if self
7335                    .reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
7336                    .await
7337                {
7338                    return;
7339                }
7340                match self.track_handle_or_err(track_name) {
7341                    Ok(track) => {
7342                        if let Err(e) =
7343                            track
7344                                .lock()
7345                                .end_clap_parameter_edit(instance_id, param_id, frame)
7346                        {
7347                            self.notify_clients(Err(e)).await;
7348                            return;
7349                        }
7350                        self.notify_clients(Ok(a.clone())).await;
7351                    }
7352                    Err(e) => {
7353                        self.notify_clients(Err(e)).await;
7354                    }
7355                }
7356            }
7357            Action::TrackGetClapParameters {
7358                ref track_name,
7359                instance_id,
7360            } => match self.track_handle_or_err(track_name) {
7361                Ok(track) => match track.lock().get_clap_parameters(instance_id) {
7362                    Ok(parameters) => {
7363                        self.notify_clients(Ok(Action::TrackClapParameters {
7364                            track_name: track_name.clone(),
7365                            instance_id,
7366                            parameters,
7367                        }))
7368                        .await;
7369                    }
7370                    Err(e) => {
7371                        self.notify_clients(Err(e)).await;
7372                    }
7373                },
7374                Err(e) => {
7375                    self.notify_clients(Err(e)).await;
7376                }
7377            },
7378            Action::TrackClapParameters { .. } => {}
7379            Action::TrackClapSnapshotState {
7380                ref track_name,
7381                instance_id,
7382            } => match self.track_handle_or_err(track_name) {
7383                Ok(track) => {
7384                    let plugin_path = track
7385                        .lock()
7386                        .clap_plugins
7387                        .iter()
7388                        .find(|instance| instance.id == instance_id)
7389                        .map(|instance| instance.processor.lock().path().to_string())
7390                        .unwrap_or_default();
7391                    match track.lock().clap_snapshot_state(instance_id) {
7392                        Ok(state) => {
7393                            self.notify_clients(Ok(Action::TrackClapStateSnapshot {
7394                                track_name: track_name.clone(),
7395                                instance_id,
7396                                plugin_path,
7397                                state,
7398                            }))
7399                            .await;
7400                        }
7401                        Err(e) => {
7402                            self.notify_clients(Err(e)).await;
7403                        }
7404                    }
7405                }
7406                Err(e) => {
7407                    self.notify_clients(Err(e)).await;
7408                }
7409            },
7410            Action::ClipClapSnapshotState {
7411                ref track_name,
7412                clip_idx,
7413                instance_id,
7414            } => match self.track_handle_or_err(track_name) {
7415                Ok(track) => match track.lock().clip_clap_snapshot_state(clip_idx, instance_id) {
7416                    Ok((plugin_path, state)) => {
7417                        self.notify_clients(Ok(Action::ClipClapStateSnapshot {
7418                            track_name: track_name.clone(),
7419                            clip_idx,
7420                            instance_id,
7421                            plugin_path,
7422                            state,
7423                        }))
7424                        .await;
7425                    }
7426                    Err(e) => {
7427                        self.notify_clients(Err(e)).await;
7428                    }
7429                },
7430                Err(e) => {
7431                    self.notify_clients(Err(e)).await;
7432                }
7433            },
7434            Action::TrackClapStateSnapshot { .. } => {}
7435            Action::ClipClapStateSnapshot { .. } => {}
7436            Action::TrackClapStateDirty { .. } => {}
7437            Action::ClipClapStateDirty { .. } => {}
7438            Action::TrackClapRestoreState {
7439                ref track_name,
7440                instance_id,
7441                ref state,
7442            } => {
7443                if self
7444                    .reject_if_track_frozen(track_name, "CLAP state restore")
7445                    .await
7446                {
7447                    return;
7448                }
7449                let track = match self.track_handle_or_err(track_name) {
7450                    Ok(track) => track,
7451                    Err(e) => {
7452                        self.notify_clients(Err(e)).await;
7453                        return;
7454                    }
7455                };
7456                let track = track.lock();
7457                if track.audio.processing {
7458                    self.notify_clients(Err(format!(
7459                        "Track '{}' is currently processing audio; stop playback before restoring CLAP state",
7460                        track_name
7461                    )))
7462                    .await;
7463                    return;
7464                }
7465                if let Err(e) = track.clap_restore_state(instance_id, state) {
7466                    self.notify_clients(Err(e)).await;
7467                    return;
7468                }
7469            }
7470            Action::ClipClapRestoreState {
7471                ref track_name,
7472                clip_idx,
7473                instance_id,
7474                ref state,
7475            } => {
7476                if self
7477                    .reject_if_track_frozen(track_name, "CLAP state restore")
7478                    .await
7479                {
7480                    return;
7481                }
7482                let track = match self.track_handle_or_err(track_name) {
7483                    Ok(track) => track,
7484                    Err(e) => {
7485                        self.notify_clients(Err(e)).await;
7486                        return;
7487                    }
7488                };
7489                let track = track.lock();
7490                if track.audio.processing {
7491                    self.notify_clients(Err(format!(
7492                        "Track '{}' is currently processing audio; stop playback before restoring CLAP state",
7493                        track_name
7494                    )))
7495                    .await;
7496                    return;
7497                }
7498                if let Err(e) = track.clip_clap_restore_state(clip_idx, instance_id, state) {
7499                    self.notify_clients(Err(e)).await;
7500                    return;
7501                }
7502            }
7503            Action::TrackSnapshotAllClapStates { ref track_name } => {
7504                let track = match self.track_handle_or_err(track_name) {
7505                    Ok(track) => track,
7506                    Err(e) => {
7507                        self.notify_clients(Err(e)).await;
7508                        return;
7509                    }
7510                };
7511                let instances: Vec<_> = {
7512                    let locked = track.lock();
7513                    locked
7514                        .clap_plugins
7515                        .iter()
7516                        .map(|i| (i.id, i.processor.lock().path().to_string()))
7517                        .collect()
7518                };
7519                for (instance_id, plugin_path) in instances {
7520                    match track.lock().clap_snapshot_state(instance_id) {
7521                        Ok(state) => {
7522                            self.notify_clients(Ok(Action::TrackClapStateSnapshot {
7523                                track_name: track_name.clone(),
7524                                instance_id,
7525                                plugin_path,
7526                                state,
7527                            }))
7528                            .await;
7529                        }
7530                        Err(_e) => {}
7531                    }
7532                }
7533                self.notify_clients(Ok(Action::TrackSnapshotAllClapStatesDone {
7534                    track_name: track_name.clone(),
7535                }))
7536                .await;
7537            }
7538            Action::TrackSnapshotAllClapStatesDone { .. } => {}
7539            Action::TrackGetVst3Graph { ref track_name } => {
7540                match self.track_handle_or_err(track_name) {
7541                    Ok(track) => {
7542                        let t = track.lock();
7543                        let plugins = t.vst3_graph_plugins();
7544                        let connections = t.vst3_graph_connections();
7545                        self.notify_clients(Ok(Action::TrackVst3Graph {
7546                            track_name: track_name.clone(),
7547                            plugins,
7548                            connections,
7549                        }))
7550                        .await;
7551                    }
7552                    Err(e) => {
7553                        self.notify_clients(Err(e)).await;
7554                    }
7555                }
7556            }
7557            Action::TrackVst3Graph { .. } => {}
7558            Action::TrackSetVst3Parameter {
7559                ref track_name,
7560                instance_id,
7561                param_id,
7562                value,
7563            } => {
7564                if self
7565                    .reject_if_track_frozen(track_name, "VST3 parameter changes")
7566                    .await
7567                {
7568                    return;
7569                }
7570                match self.track_handle_or_err(track_name) {
7571                    Ok(track) => {
7572                        if let Err(e) =
7573                            track
7574                                .lock()
7575                                .set_vst3_parameter(instance_id, param_id, value)
7576                        {
7577                            self.notify_clients(Err(e)).await;
7578                            return;
7579                        }
7580                        self.notify_clients(Ok(a.clone())).await;
7581                    }
7582                    Err(e) => {
7583                        self.notify_clients(Err(e)).await;
7584                    }
7585                }
7586            }
7587            Action::TrackSetPluginBypassed {
7588                ref track_name,
7589                instance_id,
7590                ref format,
7591                bypassed,
7592            } => match self.track_handle_or_err(track_name) {
7593                Ok(track) => {
7594                    let result = match format.as_str() {
7595                        "CLAP" => track.lock().set_clap_plugin_bypassed(instance_id, bypassed),
7596                        "VST3" => track.lock().set_vst3_plugin_bypassed(instance_id, bypassed),
7597                        #[cfg(all(unix, not(target_os = "macos")))]
7598                        "LV2" => track.lock().set_lv2_plugin_bypassed(instance_id, bypassed),
7599                        _ => Err(format!("Unknown plugin format for bypass: {format}")),
7600                    };
7601                    if let Err(e) = result {
7602                        self.notify_clients(Err(e)).await;
7603                        return;
7604                    }
7605                    self.notify_clients(Ok(a.clone())).await;
7606                }
7607                Err(e) => {
7608                    self.notify_clients(Err(e)).await;
7609                }
7610            },
7611            Action::TrackGetVst3Parameters {
7612                ref track_name,
7613                instance_id,
7614            } => match self.track_handle_or_err(track_name) {
7615                Ok(track) => match track.lock().get_vst3_parameters(instance_id) {
7616                    Ok(parameters) => {
7617                        self.notify_clients(Ok(Action::TrackVst3Parameters {
7618                            track_name: track_name.clone(),
7619                            instance_id,
7620                            parameters,
7621                        }))
7622                        .await;
7623                    }
7624                    Err(e) => {
7625                        self.notify_clients(Err(e)).await;
7626                    }
7627                },
7628                Err(e) => {
7629                    self.notify_clients(Err(e)).await;
7630                }
7631            },
7632            Action::TrackVst3Parameters { .. } => {}
7633            Action::TrackVst3SnapshotState {
7634                ref track_name,
7635                instance_id,
7636            } => match self.track_handle_or_err(track_name) {
7637                Ok(track) => match track.lock().vst3_snapshot_state(instance_id) {
7638                    Ok(state) => {
7639                        self.notify_clients(Ok(Action::TrackVst3StateSnapshot {
7640                            track_name: track_name.clone(),
7641                            instance_id,
7642                            state,
7643                        }))
7644                        .await;
7645                    }
7646                    Err(e) => {
7647                        self.notify_clients(Err(e)).await;
7648                    }
7649                },
7650                Err(e) => {
7651                    self.notify_clients(Err(e)).await;
7652                }
7653            },
7654            Action::ClipVst3SnapshotState {
7655                ref track_name,
7656                clip_idx,
7657                instance_id,
7658            } => match self.track_handle_or_err(track_name) {
7659                Ok(track) => match track.lock().clip_vst3_snapshot_state(clip_idx, instance_id) {
7660                    Ok(state) => {
7661                        self.notify_clients(Ok(Action::ClipVst3StateSnapshot {
7662                            track_name: track_name.clone(),
7663                            clip_idx,
7664                            instance_id,
7665                            state,
7666                        }))
7667                        .await;
7668                    }
7669                    Err(e) => {
7670                        self.notify_clients(Err(e)).await;
7671                    }
7672                },
7673                Err(e) => {
7674                    self.notify_clients(Err(e)).await;
7675                }
7676            },
7677            Action::TrackVst3StateSnapshot { .. } => {}
7678            Action::ClipVst3StateSnapshot { .. } => {}
7679            Action::TrackVst3RestoreState {
7680                ref track_name,
7681                instance_id,
7682                ref state,
7683            } => match self.track_handle_or_err(track_name) {
7684                Ok(track) => {
7685                    if let Err(e) = track.lock().vst3_restore_state(instance_id, state) {
7686                        self.notify_clients(Err(e)).await;
7687                        return;
7688                    }
7689                    self.notify_clients(Ok(a.clone())).await;
7690                }
7691                Err(e) => {
7692                    self.notify_clients(Err(e)).await;
7693                }
7694            },
7695            Action::TrackConnectVst3Audio {
7696                ref track_name,
7697                ref from_node,
7698                from_port,
7699                ref to_node,
7700                to_port,
7701            } => {
7702                if self
7703                    .reject_if_track_frozen(track_name, "VST3 routing changes")
7704                    .await
7705                {
7706                    return;
7707                }
7708                match self.track_handle_or_err(track_name) {
7709                    Ok(track) => {
7710                        if let Err(e) = track
7711                            .lock()
7712                            .connect_vst3_audio(from_node, from_port, to_node, to_port)
7713                        {
7714                            self.notify_clients(Err(e)).await;
7715                            return;
7716                        }
7717                        self.notify_clients(Ok(a.clone())).await;
7718                    }
7719                    Err(e) => {
7720                        self.notify_clients(Err(e)).await;
7721                    }
7722                }
7723            }
7724            Action::TrackDisconnectVst3Audio {
7725                ref track_name,
7726                ref from_node,
7727                from_port,
7728                ref to_node,
7729                to_port,
7730            } => {
7731                if self
7732                    .reject_if_track_frozen(track_name, "VST3 routing changes")
7733                    .await
7734                {
7735                    return;
7736                }
7737                match self.track_handle_or_err(track_name) {
7738                    Ok(track) => {
7739                        if let Err(e) = track
7740                            .lock()
7741                            .disconnect_vst3_audio(from_node, from_port, to_node, to_port)
7742                        {
7743                            self.notify_clients(Err(e)).await;
7744                            return;
7745                        }
7746                        self.notify_clients(Ok(a.clone())).await;
7747                    }
7748                    Err(e) => {
7749                        self.notify_clients(Err(e)).await;
7750                    }
7751                }
7752            }
7753            Action::ClipMove {
7754                ref kind,
7755                ref from,
7756                ref to,
7757                copy,
7758            } => {
7759                if let Some(from_track_handle) = self.state.lock().tracks.get(&from.track_name)
7760                    && let Some(to_track_handle) = self.state.lock().tracks.get(&to.track_name)
7761                {
7762                    let from_track = from_track_handle.lock();
7763                    let to_track = to_track_handle.lock();
7764                    match kind {
7765                        Kind::Audio => {
7766                            if from.clip_index >= from_track.audio.clips.len() {
7767                                self.notify_clients(Err(format!(
7768                                    "Clip index {} is too high, as track {} has only {} clips!",
7769                                    from.clip_index,
7770                                    from_track.name.clone(),
7771                                    from_track.audio.clips.len(),
7772                                )))
7773                                .await;
7774                                return;
7775                            }
7776                            if from_track.audio.ins.len() != to_track.audio.ins.len() {
7777                                self.notify_clients(Err(format!(
7778                                    "Cannot move/copy audio clip from '{}' ({} inputs) to '{}' ({} inputs)",
7779                                    from_track.name,
7780                                    from_track.audio.ins.len(),
7781                                    to_track.name,
7782                                    to_track.audio.ins.len()
7783                                )))
7784                                .await;
7785                                return;
7786                            }
7787                            let clip_copy = from_track.audio.clips[from.clip_index].clone();
7788                            if !copy {
7789                                from_track.audio.clips.remove(from.clip_index);
7790                            }
7791                            let mut clip_copy = clip_copy;
7792                            clip_copy.start = to.sample_offset;
7793                            let max_lane = to_track.audio.ins.len().saturating_sub(1);
7794                            clip_copy.input_channel = to.input_channel.min(max_lane);
7795                            to_track.audio.clips.push(clip_copy);
7796                        }
7797                        Kind::MIDI => {
7798                            if from.clip_index >= from_track.midi.clips.len() {
7799                                self.notify_clients(Err(format!(
7800                                    "Clip index {} is too high, as track {} has only {} clips!",
7801                                    from.clip_index,
7802                                    from_track.name.clone(),
7803                                    from_track.midi.clips.len(),
7804                                )))
7805                                .await;
7806                                return;
7807                            }
7808                            let clip_copy = from_track.midi.clips[from.clip_index].clone();
7809                            if !copy {
7810                                from_track.midi.clips.remove(from.clip_index);
7811                            }
7812                            let mut clip_copy = clip_copy;
7813                            clip_copy.start = to.sample_offset;
7814                            let max_lane = to_track.midi.ins.len().saturating_sub(1);
7815                            clip_copy.input_channel = to.input_channel.min(max_lane);
7816                            to_track.midi.clips.push(clip_copy);
7817                        }
7818                    }
7819                }
7820            }
7821            Action::AddClip {
7822                ref name,
7823                ref track_name,
7824                start,
7825                length,
7826                offset,
7827                input_channel,
7828                muted,
7829                ref peaks_file,
7830                kind,
7831                fade_enabled,
7832                fade_in_samples,
7833                fade_out_samples,
7834                ref source_name,
7835                source_offset,
7836                source_length,
7837                ref preview_name,
7838                ref pitch_correction_points,
7839                pitch_correction_frame_likeness,
7840                pitch_correction_inertia_ms,
7841                pitch_correction_formant_compensation,
7842                ref plugin_graph_json,
7843            } => {
7844                self.add_clip_to_track(ClipAddRequest {
7845                    name,
7846                    track_name,
7847                    start,
7848                    length,
7849                    offset,
7850                    input_channel,
7851                    muted,
7852                    peaks_file: peaks_file.clone(),
7853                    kind,
7854                    fade_enabled,
7855                    fade_in_samples,
7856                    fade_out_samples,
7857                    source_name: source_name.clone(),
7858                    source_offset,
7859                    source_length,
7860                    preview_name: preview_name.clone(),
7861                    pitch_correction_points: pitch_correction_points.clone(),
7862                    pitch_correction_frame_likeness,
7863                    pitch_correction_inertia_ms,
7864                    pitch_correction_formant_compensation,
7865                    plugin_graph_json: plugin_graph_json.clone(),
7866                });
7867                if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
7868                    let track_name = track_name.clone();
7869                    tokio::task::spawn_blocking(move || {
7870                        track.lock().preload_clips();
7871                        tracing::debug!("Preloaded clips for track '{}' after AddClip", track_name);
7872                    });
7873                }
7874            }
7875            Action::AddGroupedClip {
7876                ref track_name,
7877                kind,
7878                ref audio_clip,
7879                ref midi_clip,
7880            } => {
7881                self.add_grouped_clip_to_track(
7882                    track_name,
7883                    kind,
7884                    audio_clip.clone(),
7885                    midi_clip.clone(),
7886                );
7887                if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
7888                    let track_name = track_name.clone();
7889                    tokio::task::spawn_blocking(move || {
7890                        track.lock().preload_clips();
7891                        tracing::debug!(
7892                            "Preloaded clips for track '{}' after AddGroupedClip",
7893                            track_name
7894                        );
7895                    });
7896                }
7897            }
7898            Action::RemoveClip {
7899                ref track_name,
7900                kind,
7901                ref clip_indices,
7902            } => {
7903                self.remove_clips_from_track(track_name, kind, clip_indices);
7904            }
7905            Action::RenameClip {
7906                ref track_name,
7907                kind,
7908                clip_index,
7909                ref new_name,
7910            } => {
7911                self.rename_clip_references(track_name, kind, clip_index, new_name);
7912            }
7913            Action::SetClipSourceName {
7914                ref track_name,
7915                kind,
7916                clip_index,
7917                ref name,
7918            } => {
7919                self.set_clip_source_name(track_name, clip_index, kind, name.clone());
7920            }
7921            Action::SetClipFade {
7922                ref track_name,
7923                clip_index,
7924                kind,
7925                fade_enabled,
7926                fade_in_samples,
7927                fade_out_samples,
7928            } => {
7929                self.set_clip_fade(
7930                    track_name,
7931                    clip_index,
7932                    kind,
7933                    fade_enabled,
7934                    fade_in_samples,
7935                    fade_out_samples,
7936                );
7937            }
7938            Action::SetClipBounds {
7939                ref track_name,
7940                clip_index,
7941                kind,
7942                start,
7943                length,
7944                offset,
7945            } => {
7946                self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
7947            }
7948            Action::SyncClipBounds {
7949                ref track_name,
7950                clip_index,
7951                kind,
7952                start,
7953                length,
7954                offset,
7955            } => {
7956                self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
7957            }
7958            Action::SetClipMuted {
7959                ref track_name,
7960                clip_index,
7961                kind,
7962                muted,
7963            } => {
7964                self.set_clip_muted(track_name, clip_index, kind, muted);
7965            }
7966            Action::SetClipPluginGraphJson {
7967                ref track_name,
7968                clip_index,
7969                ref plugin_graph_json,
7970            } => {
7971                self.set_clip_plugin_graph_json(track_name, clip_index, plugin_graph_json.clone());
7972            }
7973            Action::SetClipPitchCorrection {
7974                ref track_name,
7975                clip_index,
7976                ref preview_name,
7977                ref source_name,
7978                source_offset,
7979                source_length,
7980                ref pitch_correction_points,
7981                pitch_correction_frame_likeness,
7982                pitch_correction_inertia_ms,
7983                pitch_correction_formant_compensation,
7984            } => {
7985                self.set_clip_pitch_correction(
7986                    track_name,
7987                    clip_index,
7988                    preview_name.clone(),
7989                    source_name.clone(),
7990                    source_offset,
7991                    source_length,
7992                    pitch_correction_points.clone(),
7993                    pitch_correction_frame_likeness,
7994                    pitch_correction_inertia_ms,
7995                    pitch_correction_formant_compensation,
7996                );
7997            }
7998            Action::Connect {
7999                ref from_track,
8000                from_port,
8001                ref to_track,
8002                to_port,
8003                kind,
8004            } => {
8005                match kind {
8006                    Kind::Audio => {
8007                        let from_audio_io = if from_track == "hw:in" {
8008                            self.hw_input_audio_port(from_port)
8009                        } else {
8010                            self.state
8011                                .lock()
8012                                .tracks
8013                                .get(from_track)
8014                                .and_then(|t| t.lock().audio.outs.get(from_port).cloned())
8015                        };
8016                        let to_audio_io = if to_track == "hw:out" {
8017                            self.hw_output_audio_port(to_port)
8018                        } else {
8019                            self.state
8020                                .lock()
8021                                .tracks
8022                                .get(to_track)
8023                                .and_then(|t| t.lock().audio.ins.get(to_port).cloned())
8024                        };
8025                        match (from_audio_io, to_audio_io) {
8026                            (Some(source), Some(target)) => {
8027                                if from_track != "hw:in"
8028                                    && to_track != "hw:out"
8029                                    && self.check_if_leads_to_kind(
8030                                        Kind::Audio,
8031                                        to_track,
8032                                        from_track,
8033                                    )
8034                                {
8035                                    self.notify_clients(Err(
8036                                        "Circular routing is not allowed!".into()
8037                                    ))
8038                                    .await;
8039                                    return;
8040                                }
8041                                crate::audio::io::AudioIO::connect(&source, &target);
8042                            }
8043                            (None, _) => {
8044                                self.notify_clients(Err(format!(
8045                                    "Source track '{}' not found",
8046                                    from_track
8047                                )))
8048                                .await;
8049                                return;
8050                            }
8051                            (_, None) => {
8052                                self.notify_clients(Err(format!(
8053                                    "Destination track '{}' not found",
8054                                    to_track
8055                                )))
8056                                .await;
8057                                return;
8058                            }
8059                        }
8060                    }
8061                    Kind::MIDI => {
8062                        let from_hw_in_device = Self::midi_hw_in_device(from_track);
8063                        let to_hw_out_device = Self::midi_hw_out_device(to_track);
8064                        let from_is_invalid_hw = Self::midi_hw_out_device(from_track).is_some();
8065                        let to_is_invalid_hw = Self::midi_hw_in_device(to_track).is_some();
8066
8067                        if from_is_invalid_hw || to_is_invalid_hw {
8068                            self.notify_clients(Err(
8069                                "Invalid MIDI hardware connection direction".to_string()
8070                            ))
8071                            .await;
8072                            return;
8073                        }
8074
8075                        if from_hw_in_device.is_none()
8076                            && to_hw_out_device.is_none()
8077                            && self.check_if_leads_to_kind(Kind::MIDI, to_track, from_track)
8078                        {
8079                            self.notify_clients(Err("Circular routing is not allowed!".into()))
8080                                .await;
8081                            return;
8082                        }
8083
8084                        let state = self.state.lock();
8085                        let from_track_handle = state.tracks.get(from_track);
8086                        let to_track_handle = state.tracks.get(to_track);
8087
8088                        if let (Some(from_device), Some(to_device)) =
8089                            (from_hw_in_device, to_hw_out_device)
8090                        {
8091                            let route = MidiHwThruRoute {
8092                                from_device: from_device.to_string(),
8093                                to_device: to_device.to_string(),
8094                            };
8095                            if !self.midi_hw_thru_routes.iter().any(|r| r == &route) {
8096                                self.midi_hw_thru_routes.push(route);
8097                            }
8098                        } else if let Some(device) = from_hw_in_device {
8099                            if let Some(t_t) = to_track_handle {
8100                                if t_t.lock().midi.ins.get(to_port).is_none() {
8101                                    self.notify_clients(Err(format!(
8102                                        "MIDI input port {} not found on track '{}'",
8103                                        to_port, to_track
8104                                    )))
8105                                    .await;
8106                                    return;
8107                                }
8108                                let route = MidiHwInRoute {
8109                                    device: device.to_string(),
8110                                    to_track: to_track.to_string(),
8111                                    to_port,
8112                                };
8113                                if !self.midi_hw_in_routes.iter().any(|r| r == &route) {
8114                                    self.midi_hw_in_routes.push(route);
8115                                }
8116                            } else {
8117                                self.notify_clients(Err(format!(
8118                                    "MIDI destination track not found: {}",
8119                                    to_track
8120                                )))
8121                                .await;
8122                                return;
8123                            }
8124                        } else if let Some(device) = to_hw_out_device {
8125                            if let Some(f_t) = from_track_handle {
8126                                if f_t.lock().midi.outs.get(from_port).is_none() {
8127                                    self.notify_clients(Err(format!(
8128                                        "MIDI output port {} not found on track '{}'",
8129                                        from_port, from_track
8130                                    )))
8131                                    .await;
8132                                    return;
8133                                }
8134                                let route = MidiHwOutRoute {
8135                                    from_track: from_track.to_string(),
8136                                    from_port,
8137                                    device: device.to_string(),
8138                                };
8139                                if !self.midi_hw_out_routes.iter().any(|r| r == &route) {
8140                                    self.midi_hw_out_routes.push(route);
8141                                }
8142                            } else {
8143                                self.notify_clients(Err(format!(
8144                                    "MIDI source track not found: {}",
8145                                    from_track
8146                                )))
8147                                .await;
8148                                return;
8149                            }
8150                        } else {
8151                            match (from_track_handle, to_track_handle) {
8152                                (Some(f_t), Some(t_t)) => {
8153                                    let to_in_res = t_t.lock().midi.ins.get(to_port).cloned();
8154                                    if let Some(to_in) = to_in_res {
8155                                        let from_track = f_t.lock();
8156                                        if let Err(e) =
8157                                            from_track.midi.connect_out(from_port, to_in)
8158                                        {
8159                                            self.notify_clients(Err(e)).await;
8160                                            return;
8161                                        }
8162                                        from_track.invalidate_midi_route_cache();
8163                                    } else {
8164                                        self.notify_clients(Err(format!(
8165                                            "MIDI input port {} not found on track '{}'",
8166                                            to_port, to_track
8167                                        )))
8168                                        .await;
8169                                        return;
8170                                    }
8171                                }
8172                                _ => {
8173                                    self.notify_clients(Err(format!(
8174                                        "MIDI tracks not found: {} or {}",
8175                                        from_track, to_track
8176                                    )))
8177                                    .await;
8178                                    return;
8179                                }
8180                            }
8181                        }
8182                    }
8183                };
8184            }
8185            Action::Disconnect {
8186                ref from_track,
8187                from_port,
8188                ref to_track,
8189                to_port,
8190                kind,
8191            } => {
8192                if kind == Kind::Audio {
8193                    if let Err(e) = self.disconnect_audio_route_and_notify(a.clone()).await {
8194                        self.notify_clients(Err(e)).await;
8195                    }
8196                } else if kind == Kind::MIDI {
8197                    let from_hw_in_device = Self::midi_hw_in_device(from_track);
8198                    let to_hw_out_device = Self::midi_hw_out_device(to_track);
8199
8200                    if let (Some(from_device), Some(to_device)) =
8201                        (from_hw_in_device, to_hw_out_device)
8202                    {
8203                        let before = self.midi_hw_thru_routes.len();
8204                        self.midi_hw_thru_routes.retain(|r| {
8205                            !(r.from_device == from_device && r.to_device == to_device)
8206                        });
8207                        if self.midi_hw_thru_routes.len() < before {
8208                            self.notify_clients(Ok(a.clone())).await;
8209                        } else {
8210                            self.notify_clients(Err(format!(
8211                                "Disconnect failed: MIDI route not found ({} -> {})",
8212                                from_track, to_track
8213                            )))
8214                            .await;
8215                        }
8216                        return;
8217                    }
8218
8219                    if let Some(device) = from_hw_in_device {
8220                        let before = self.midi_hw_in_routes.len();
8221                        self.midi_hw_in_routes.retain(|r| {
8222                            !(r.device == device && r.to_track == *to_track && r.to_port == to_port)
8223                        });
8224                        if self.midi_hw_in_routes.len() < before {
8225                            self.notify_clients(Ok(a.clone())).await;
8226                        } else {
8227                            self.notify_clients(Err(format!(
8228                                "Disconnect failed: MIDI route not found ({} -> {})",
8229                                from_track, to_track
8230                            )))
8231                            .await;
8232                        }
8233                        return;
8234                    }
8235
8236                    if let Some(device) = to_hw_out_device {
8237                        let before = self.midi_hw_out_routes.len();
8238                        self.midi_hw_out_routes.retain(|r| {
8239                            !(r.from_track == *from_track
8240                                && r.from_port == from_port
8241                                && r.device == device)
8242                        });
8243                        if self.midi_hw_out_routes.len() < before {
8244                            self.notify_clients(Ok(a.clone())).await;
8245                        } else {
8246                            self.notify_clients(Err(format!(
8247                                "Disconnect failed: MIDI route not found ({} -> {})",
8248                                from_track, to_track
8249                            )))
8250                            .await;
8251                        }
8252                        return;
8253                    }
8254
8255                    let state = self.state.lock();
8256                    if let (Some(f_t), Some(t_t)) =
8257                        (state.tracks.get(from_track), state.tracks.get(to_track))
8258                        && let Some(to_in) = t_t.lock().midi.ins.get(to_port).cloned()
8259                    {
8260                        let from_track = f_t.lock();
8261                        if let Err(e) = from_track.midi.disconnect_out(from_port, &to_in) {
8262                            self.notify_clients(Err(e)).await;
8263                        } else {
8264                            from_track.invalidate_midi_route_cache();
8265                            self.notify_clients(Ok(a.clone())).await;
8266                        }
8267                    } else {
8268                        self.notify_clients(Err(format!(
8269                            "Disconnect failed: MIDI ports not found ({} -> {})",
8270                            from_track, to_track
8271                        )))
8272                        .await;
8273                    }
8274                }
8275            }
8276
8277            Action::OpenAudioDevice {
8278                ref device,
8279                ref input_device,
8280                sample_rate_hz,
8281                bits,
8282                exclusive,
8283                period_frames,
8284                nperiods,
8285                sync_mode,
8286                ..
8287            } => {
8288                #[cfg(unix)]
8289                {
8290                    let request = AudioOpenRequest {
8291                        device,
8292                        input_device: input_device.as_deref(),
8293                        sample_rate_hz,
8294                        bits,
8295                        exclusive,
8296                        period_frames,
8297                        nperiods,
8298                        sync_mode,
8299                    };
8300                    if self.maybe_open_jack_runtime(request).await.is_some() {
8301                        return;
8302                    }
8303                }
8304                let hw_opts = Self::build_hw_options(exclusive, period_frames, nperiods, sync_mode);
8305                let open_result = self
8306                    .open_non_jack_audio_device(
8307                        device,
8308                        input_device.as_deref(),
8309                        sample_rate_hz,
8310                        bits,
8311                        hw_opts,
8312                    )
8313                    .await;
8314                match open_result {
8315                    Ok(()) => {}
8316                    Err(e) => {
8317                        error!("Failed to open audio device: {e}");
8318                        self.notify_clients(Err(e)).await;
8319                        return;
8320                    }
8321                }
8322                self.finalize_open_audio_device().await;
8323                if let Some(hw) = &self.hw_driver {
8324                    let effective_action = {
8325                        let hw = hw.lock();
8326                        Action::OpenAudioDevice {
8327                            device: device.clone(),
8328                            input_device: input_device.clone(),
8329                            sample_rate_hz: hw.sample_rate(),
8330                            bits: hw.sample_bits(),
8331                            exclusive,
8332                            period_frames,
8333                            nperiods,
8334                            sync_mode,
8335                            actual_period_frames: hw.cycle_samples(),
8336                            input_channels: hw.input_channels(),
8337                            output_channels: hw.output_channels(),
8338                            bytes_per_frame: hw.frame_size_bytes(),
8339                        }
8340                    };
8341                    action_to_process = effective_action;
8342                }
8343            }
8344            Action::JackAddAudioInputPort => {
8345                #[cfg(unix)]
8346                {
8347                    if let Some(jack) = self.jack_runtime.clone() {
8348                        let (input_channels, output_channels, rate) = {
8349                            let jack = jack.lock();
8350                            if let Err(e) = jack.add_audio_input_port() {
8351                                self.notify_clients(Err(e)).await;
8352                                return;
8353                            }
8354                            (
8355                                jack.input_channels(),
8356                                jack.output_channels(),
8357                                jack.sample_rate,
8358                            )
8359                        };
8360                        self.publish_hw_infos(input_channels, output_channels, rate)
8361                            .await;
8362                        self.notify_clients(Ok(a.clone())).await;
8363                    } else {
8364                        self.notify_clients(Err(
8365                            "JACK runtime is not active; open the JACK backend first".to_string(),
8366                        ))
8367                        .await;
8368                    }
8369                }
8370                #[cfg(not(unix))]
8371                {
8372                    self.notify_clients(Err(
8373                        "JACK backend is not available on this platform build".to_string(),
8374                    ))
8375                    .await;
8376                }
8377            }
8378            Action::JackRemoveAudioInputPort(_removed_port) => {
8379                #[cfg(unix)]
8380                {
8381                    let removed_port = _removed_port;
8382                    if let Some(jack) = self.jack_runtime.clone() {
8383                        let (removed_port, removed_io) = {
8384                            let jack = jack.lock();
8385                            let removed_port = Some(removed_port);
8386                            let removed_io =
8387                                removed_port.and_then(|port| jack.input_audio_port(port));
8388                            match (removed_port, removed_io) {
8389                                (Some(port), Some(io)) => (port, io),
8390                                _ => {
8391                                    self.notify_clients(Err(
8392                                        "JACK audio input port index is out of range".to_string(),
8393                                    ))
8394                                    .await;
8395                                    return;
8396                                }
8397                            }
8398                        };
8399                        let reindex_notifications =
8400                            self.reindex_notifications_for_removed_hw_input(removed_port);
8401                        for disconnect in
8402                            self.disconnect_actions_for_removed_hw_input(removed_port, &removed_io)
8403                        {
8404                            if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await
8405                            {
8406                                self.notify_clients(Err(e)).await;
8407                                return;
8408                            }
8409                        }
8410                        let (input_channels, output_channels, rate) = {
8411                            let jack = jack.lock();
8412                            if let Err(e) = jack.remove_audio_input_port(removed_port) {
8413                                self.notify_clients(Err(e)).await;
8414                                return;
8415                            }
8416                            (
8417                                jack.input_channels(),
8418                                jack.output_channels(),
8419                                jack.sample_rate,
8420                            )
8421                        };
8422                        for action in reindex_notifications {
8423                            self.notify_clients(Ok(action)).await;
8424                        }
8425                        self.publish_hw_infos(input_channels, output_channels, rate)
8426                            .await;
8427                        self.notify_clients(Ok(a.clone())).await;
8428                    } else {
8429                        self.notify_clients(Err(
8430                            "JACK runtime is not active; open the JACK backend first".to_string(),
8431                        ))
8432                        .await;
8433                    }
8434                }
8435                #[cfg(not(unix))]
8436                {
8437                    self.notify_clients(Err(
8438                        "JACK backend is not available on this platform build".to_string(),
8439                    ))
8440                    .await;
8441                }
8442            }
8443            Action::JackAddAudioOutputPort => {
8444                #[cfg(unix)]
8445                {
8446                    if let Some(jack) = self.jack_runtime.clone() {
8447                        let (input_channels, output_channels, rate) = {
8448                            let jack = jack.lock();
8449                            if let Err(e) = jack.add_audio_output_port() {
8450                                self.notify_clients(Err(e)).await;
8451                                return;
8452                            }
8453                            (
8454                                jack.input_channels(),
8455                                jack.output_channels(),
8456                                jack.sample_rate,
8457                            )
8458                        };
8459                        self.publish_hw_infos(input_channels, output_channels, rate)
8460                            .await;
8461                        self.notify_clients(Ok(a.clone())).await;
8462                    } else {
8463                        self.notify_clients(Err(
8464                            "JACK runtime is not active; open the JACK backend first".to_string(),
8465                        ))
8466                        .await;
8467                    }
8468                }
8469                #[cfg(not(unix))]
8470                {
8471                    self.notify_clients(Err(
8472                        "JACK backend is not available on this platform build".to_string(),
8473                    ))
8474                    .await;
8475                }
8476            }
8477            Action::JackRemoveAudioOutputPort(_removed_port) => {
8478                #[cfg(unix)]
8479                {
8480                    let removed_port = _removed_port;
8481                    if let Some(jack) = self.jack_runtime.clone() {
8482                        let (removed_port, removed_io) = {
8483                            let jack = jack.lock();
8484                            let removed_port = Some(removed_port);
8485                            let removed_io =
8486                                removed_port.and_then(|port| jack.output_audio_port(port));
8487                            match (removed_port, removed_io) {
8488                                (Some(port), Some(io)) => (port, io),
8489                                _ => {
8490                                    self.notify_clients(Err(
8491                                        "JACK audio output port index is out of range".to_string(),
8492                                    ))
8493                                    .await;
8494                                    return;
8495                                }
8496                            }
8497                        };
8498                        let reindex_notifications =
8499                            self.reindex_notifications_for_removed_hw_output(removed_port);
8500                        for disconnect in
8501                            self.disconnect_actions_for_removed_hw_output(removed_port, &removed_io)
8502                        {
8503                            if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await
8504                            {
8505                                self.notify_clients(Err(e)).await;
8506                                return;
8507                            }
8508                        }
8509                        let (input_channels, output_channels, rate) = {
8510                            let jack = jack.lock();
8511                            if let Err(e) = jack.remove_audio_output_port(removed_port) {
8512                                self.notify_clients(Err(e)).await;
8513                                return;
8514                            }
8515                            (
8516                                jack.input_channels(),
8517                                jack.output_channels(),
8518                                jack.sample_rate,
8519                            )
8520                        };
8521                        for action in reindex_notifications {
8522                            self.notify_clients(Ok(action)).await;
8523                        }
8524                        self.publish_hw_infos(input_channels, output_channels, rate)
8525                            .await;
8526                        self.notify_clients(Ok(a.clone())).await;
8527                    } else {
8528                        self.notify_clients(Err(
8529                            "JACK runtime is not active; open the JACK backend first".to_string(),
8530                        ))
8531                        .await;
8532                    }
8533                }
8534                #[cfg(not(unix))]
8535                {
8536                    self.notify_clients(Err(
8537                        "JACK backend is not available on this platform build".to_string(),
8538                    ))
8539                    .await;
8540                }
8541            }
8542            Action::OpenMidiInputDevice(ref device) => {
8543                let midi_hub = self.midi_hub.lock();
8544                if let Err(e) = midi_hub.open_input(device) {
8545                    self.notify_clients(Err(e)).await;
8546                    return;
8547                }
8548            }
8549            Action::OpenMidiOutputDevice(ref device) => {
8550                let midi_hub = self.midi_hub.lock();
8551                if let Err(e) = midi_hub.open_output(device) {
8552                    self.notify_clients(Err(e)).await;
8553                    return;
8554                }
8555            }
8556            Action::RequestSessionDiagnostics => {
8557                let (
8558                    track_count,
8559                    frozen_track_count,
8560                    audio_clip_count,
8561                    midi_clip_count,
8562                    lv2_instance_count,
8563                    vst3_instance_count,
8564                    clap_instance_count,
8565                ) = {
8566                    let tracks = &self.state.lock().tracks;
8567                    let mut track_count = 0usize;
8568                    let mut frozen_track_count = 0usize;
8569                    let mut audio_clip_count = 0usize;
8570                    let mut midi_clip_count = 0usize;
8571                    #[cfg(all(unix, not(target_os = "macos")))]
8572                    let mut lv2_instance_count = 0usize;
8573                    #[cfg(not(all(unix, not(target_os = "macos"))))]
8574                    let lv2_instance_count = 0usize;
8575                    let mut vst3_instance_count = 0usize;
8576                    let mut clap_instance_count = 0usize;
8577                    for track in tracks.values() {
8578                        let t = track.lock();
8579                        track_count += 1;
8580                        if t.frozen {
8581                            frozen_track_count += 1;
8582                        }
8583                        audio_clip_count += t.audio.clips.len();
8584                        midi_clip_count += t.midi.clips.len();
8585                        #[cfg(all(unix, not(target_os = "macos")))]
8586                        {
8587                            lv2_instance_count += t.lv2_plugins.len();
8588                        }
8589                        vst3_instance_count += t.vst3_plugins.len();
8590                        clap_instance_count += t.clap_plugins.len();
8591                    }
8592                    (
8593                        track_count,
8594                        frozen_track_count,
8595                        audio_clip_count,
8596                        midi_clip_count,
8597                        lv2_instance_count,
8598                        vst3_instance_count,
8599                        clap_instance_count,
8600                    )
8601                };
8602                #[cfg(not(all(unix, not(target_os = "macos"))))]
8603                let _lv2_instance_count = lv2_instance_count;
8604                let pending_hw_midi_events = self.pending_hw_midi_events.len()
8605                    + self
8606                        .pending_hw_midi_events_by_device
8607                        .values()
8608                        .map(std::vec::Vec::len)
8609                        .sum::<usize>();
8610                let sample_rate_hz = if let Some(hw) = &self.hw_driver {
8611                    hw.lock().sample_rate() as usize
8612                } else {
8613                    #[cfg(unix)]
8614                    {
8615                        self.jack_runtime
8616                            .as_ref()
8617                            .map(|j| j.lock().sample_rate)
8618                            .unwrap_or(0)
8619                    }
8620                    #[cfg(not(unix))]
8621                    0
8622                };
8623                let cycle_samples = self.current_cycle_samples();
8624                self.notify_clients(Ok(Action::SessionDiagnosticsReport {
8625                    track_count,
8626                    frozen_track_count,
8627                    audio_clip_count,
8628                    midi_clip_count,
8629                    #[cfg(all(unix, not(target_os = "macos")))]
8630                    lv2_instance_count,
8631                    vst3_instance_count,
8632                    clap_instance_count,
8633                    pending_requests: self.pending_requests.len(),
8634                    workers_total: self.workers.len(),
8635                    workers_ready: self.ready_workers.len(),
8636                    pending_hw_midi_events,
8637                    playing: self.playing,
8638                    transport_sample: self.transport_sample,
8639                    tempo_bpm: self.tempo_bpm,
8640                    sample_rate_hz,
8641                    cycle_samples,
8642                }))
8643                .await;
8644            }
8645            Action::RequestMidiLearnMappingsReport => {
8646                let mut lines = Vec::<String>::new();
8647                let fmt_binding = |b: &crate::message::MidiLearnBinding| {
8648                    let device = b.device.as_deref().unwrap_or("*");
8649                    format!("{device} CH{} CC{}", b.channel + 1, b.cc)
8650                };
8651                if let Some(b) = self.global_midi_learn_play_pause.as_ref() {
8652                    lines.push(format!("Global PlayPause: {}", fmt_binding(b)));
8653                }
8654                if let Some(b) = self.global_midi_learn_stop.as_ref() {
8655                    lines.push(format!("Global Stop: {}", fmt_binding(b)));
8656                }
8657                if let Some(b) = self.global_midi_learn_record_toggle.as_ref() {
8658                    lines.push(format!("Global RecordToggle: {}", fmt_binding(b)));
8659                }
8660                for (track_name, track) in self.state.lock().tracks.iter() {
8661                    let t = track.lock();
8662                    if let Some(b) = t.midi_learn_volume.as_ref() {
8663                        lines.push(format!("{} Volume: {}", track_name, fmt_binding(b)));
8664                    }
8665                    if let Some(b) = t.midi_learn_balance.as_ref() {
8666                        lines.push(format!("{} Balance: {}", track_name, fmt_binding(b)));
8667                    }
8668                    if let Some(b) = t.midi_learn_mute.as_ref() {
8669                        lines.push(format!("{} Mute: {}", track_name, fmt_binding(b)));
8670                    }
8671                    if let Some(b) = t.midi_learn_solo.as_ref() {
8672                        lines.push(format!("{} Solo: {}", track_name, fmt_binding(b)));
8673                    }
8674                    if let Some(b) = t.midi_learn_arm.as_ref() {
8675                        lines.push(format!("{} Arm: {}", track_name, fmt_binding(b)));
8676                    }
8677                    if let Some(b) = t.midi_learn_input_monitor.as_ref() {
8678                        lines.push(format!("{} InputMonitor: {}", track_name, fmt_binding(b)));
8679                    }
8680                    if let Some(b) = t.midi_learn_disk_monitor.as_ref() {
8681                        lines.push(format!("{} DiskMonitor: {}", track_name, fmt_binding(b)));
8682                    }
8683                }
8684                if lines.is_empty() {
8685                    lines.push("No MIDI learn mappings configured".to_string());
8686                }
8687                self.notify_clients(Ok(Action::MidiLearnMappingsReport { lines }))
8688                    .await;
8689            }
8690            Action::ClearAllMidiLearnBindings => {
8691                self.pending_midi_learn = None;
8692                self.pending_global_midi_learn = None;
8693                self.global_midi_learn_play_pause = None;
8694                self.global_midi_learn_stop = None;
8695                self.global_midi_learn_record_toggle = None;
8696                self.midi_cc_gate.clear();
8697                for track in self.state.lock().tracks.values() {
8698                    let t = track.lock();
8699                    t.midi_learn_volume = None;
8700                    t.midi_learn_balance = None;
8701                    t.midi_learn_mute = None;
8702                    t.midi_learn_solo = None;
8703                    t.midi_learn_arm = None;
8704                    t.midi_learn_input_monitor = None;
8705                    t.midi_learn_disk_monitor = None;
8706                }
8707            }
8708            #[cfg(all(unix, not(target_os = "macos")))]
8709            Action::TrackLv2PluginControls { .. } => {}
8710            #[cfg(all(unix, not(target_os = "macos")))]
8711            Action::ClipLv2PluginControls { .. } => {}
8712            #[cfg(all(unix, not(target_os = "macos")))]
8713            Action::TrackLv2Midnam { .. } => {}
8714            Action::TrackClapNoteNames { .. } => {}
8715            Action::SessionDiagnosticsReport { .. } => {}
8716            Action::MidiLearnMappingsReport { .. } => {}
8717            Action::HWInfo { .. } => {}
8718            Action::HistoryState { .. } => {}
8719            Action::Undo => {}
8720            Action::Redo => {}
8721            Action::ApplyGroupedActions(_) => {}
8722            _ => {}
8723        }
8724
8725        if let Some(inverse) = inverse_actions {
8726            if let Some(group) = self.history_group.as_mut() {
8727                group.forward_actions.push(action_to_process.clone());
8728                group.inverse_actions.splice(0..0, inverse);
8729            } else {
8730                self.history.record(UndoEntry {
8731                    forward_actions: vec![action_to_process.clone()],
8732                    inverse_actions: inverse,
8733                });
8734            }
8735        }
8736
8737        self.notify_clients(Ok(action_to_process)).await;
8738    }
8739
8740    pub async fn work(&mut self) {
8741        while let Some(message) = self.rx.recv().await {
8742            match message {
8743                Message::Ready(id) => self.push_ready_worker(id),
8744                Message::Finished {
8745                    worker_id,
8746                    task,
8747                    output_linear,
8748                    process_epoch,
8749                    parameter_updates,
8750                } => {
8751                    tracing::debug!(
8752                        "engine received Finished from worker {} for task {:?} (epoch {} vs {})",
8753                        worker_id,
8754                        task,
8755                        process_epoch,
8756                        self.track_process_epoch
8757                    );
8758                    self.push_ready_worker(worker_id);
8759                    let task_key = Self::task_key(&task);
8760                    self.task_processing_started_at.remove(&task_key);
8761                    if process_epoch != self.track_process_epoch {
8762                        if let Some(track) = self
8763                            .state
8764                            .lock()
8765                            .tracks
8766                            .get(&Self::task_track_name(&task))
8767                            .cloned()
8768                        {
8769                            let t = track.lock();
8770                            t.audio.finished = false;
8771                            t.audio.processing = false;
8772                        }
8773                        continue;
8774                    }
8775                    self.cycle_tasks_running
8776                        .retain(|t| Self::task_key(t) != task_key);
8777                    self.cycle_tasks_finished.push(task.clone());
8778                    let track_name = Self::task_track_name(&task);
8779                    self.track_meter_linear_by_track
8780                        .insert(track_name.clone(), output_linear);
8781                    for action in parameter_updates {
8782                        self.notify_clients(Ok(action)).await;
8783                    }
8784                    self.force_stalled_task_completions();
8785                    let all_finished = self.send_tasks().await;
8786                    tracing::debug!(
8787                        "engine after Finished for {}: all_finished={}",
8788                        track_name,
8789                        all_finished
8790                    );
8791                    if all_finished {
8792                        self.on_all_tracks_finished().await;
8793                    }
8794                }
8795                Message::Channel(s) => {
8796                    self.clients.push(s);
8797                }
8798
8799                Message::Request(a) => {
8800                    match a {
8801                        Action::TrackOfflineBounceCancel { track_name } => {
8802                            if let Some(job) = self.offline_bounce_jobs.get(&track_name) {
8803                                job.cancel.store(true, Ordering::Relaxed);
8804                            }
8805                        }
8806                        Action::TrackOfflineBounceCancelAll => {
8807                            for job in self.offline_bounce_jobs.values() {
8808                                job.cancel.store(true, Ordering::Relaxed);
8809                            }
8810                        }
8811                        _ if !self.offline_bounce_jobs.is_empty() => {
8812                            self.pending_requests.push_back(a);
8813                        }
8814                        Action::OpenAudioDevice { .. }
8815                        | Action::OpenMidiInputDevice(_)
8816                        | Action::OpenMidiOutputDevice(_)
8817                        | Action::RequestMeterSnapshot
8818                        | Action::Quit
8819                        | Action::Log { .. }
8820                        | Action::Play
8821                        | Action::Pause
8822                        | Action::Stop
8823                        | Action::TransportPosition(_)
8824                        | Action::JumpToEnd
8825                        | Action::SetLoopEnabled(_)
8826                        | Action::SetLoopRange(_)
8827                        | Action::SetPunchEnabled(_)
8828                        | Action::SetPunchRange(_)
8829                        | Action::SetMetronomeEnabled(_)
8830                        | Action::SetTempo(_)
8831                        | Action::SetTimeSignature { .. }
8832                        | Action::SetOscEnabled(_)
8833                        | Action::SetClipPlaybackEnabled(_)
8834                        | Action::SetRecordEnabled(_)
8835                        | Action::SetStepRecording(_)
8836                        | Action::StepRecordMidiNote { .. }
8837                        | Action::SetSessionPath(_)
8838                        | Action::ClearHistory
8839                        | Action::BeginSessionRestore
8840                        | Action::PianoKey { .. }
8841                        | Action::ModifyMidiNotes { .. }
8842                        | Action::ModifyMidiControllers { .. }
8843                        | Action::DeleteMidiControllers { .. }
8844                        | Action::InsertMidiControllers { .. }
8845                        | Action::DeleteMidiNotes { .. }
8846                        | Action::InsertMidiNotes { .. }
8847                        | Action::SetMidiSysExEvents { .. } => {
8848                            self.handle_request(a).await;
8849                        }
8850                        #[cfg(all(unix, not(target_os = "macos")))]
8851                        Action::ListLv2Plugins => {
8852                            self.handle_request(a).await;
8853                        }
8854                        Action::ListVst3Plugins => {
8855                            self.handle_request(a).await;
8856                        }
8857                        Action::ListClapPlugins => {
8858                            self.handle_request(a).await;
8859                        }
8860                        Action::ListClapPluginsWithCapabilities => {
8861                            self.handle_request(a).await;
8862                        }
8863                        _ => {
8864                            self.pending_requests.push_back(a);
8865                            if self.can_schedule_hw_cycle() {
8866                                self.request_hw_cycle().await;
8867                            } else {
8868                                while let Some(next) = self.pending_requests.pop_front() {
8869                                    self.handle_request(next).await;
8870                                }
8871                            }
8872                        }
8873                    };
8874                    self.publish_clap_state_dirty().await;
8875                }
8876                Message::OfflineBounceFinished { result } => {
8877                    if let Ok(Action::TrackOfflineBounce { track_name, .. }) = &result {
8878                        self.offline_bounce_jobs.remove(track_name);
8879                    }
8880                    self.notify_clients(result).await;
8881                    if self.offline_bounce_jobs.is_empty() {
8882                        while let Some(next) = self.pending_requests.pop_front() {
8883                            self.handle_request(next).await;
8884                        }
8885                    }
8886                }
8887                Message::HWFinished => {
8888                    if !self.awaiting_hwfinished {
8889                        tracing::debug!("HWFinished ignored (not awaiting)");
8890                        continue;
8891                    }
8892                    tracing::debug!("HWFinished handling; playing={}", self.playing);
8893                    self.handling_hwfinished = true;
8894                    self.awaiting_hwfinished = false;
8895                    #[cfg(unix)]
8896                    {
8897                        if let Some(jack) = &self.jack_runtime {
8898                            if !self.pending_hw_midi_out_events.is_empty() {
8899                                let out_events =
8900                                    std::mem::take(&mut self.pending_hw_midi_out_events);
8901                                jack.lock().write_events(&out_events);
8902                            }
8903                            let mut in_events = vec![];
8904                            jack.lock().read_events_into(&mut in_events);
8905                            if !in_events.is_empty() {
8906                                self.pending_hw_midi_events.extend(in_events);
8907                            }
8908                        }
8909                    }
8910                    #[cfg(unix)]
8911                    if self.jack_runtime.is_some() {
8912                        self.sync_from_jack_transport().await;
8913                    }
8914                    while let Some(a) = self.pending_requests.pop_front() {
8915                        self.handle_request(a).await;
8916                    }
8917                    self.apply_mute_solo_policy();
8918                    self.append_recorded_cycle();
8919                    self.flush_completed_recordings().await;
8920                    let hw_in_routes = self.midi_hw_in_routes.clone();
8921                    let pending_hw_in_by_device = self.pending_hw_midi_events_by_device.clone();
8922                    let mut reconfigured_tracks = Vec::new();
8923                    for (track_name, track) in self.state.lock().tracks.iter() {
8924                        let track_lock = track.lock();
8925                        if self.jack_runtime_is_some() {
8926                            if !self.pending_hw_midi_events.is_empty() {
8927                                track_lock.push_hw_midi_events(&self.pending_hw_midi_events);
8928                            }
8929                        } else {
8930                            for route in hw_in_routes.iter().filter(|r| &r.to_track == track_name) {
8931                                if let Some(events) = pending_hw_in_by_device.get(&route.device) {
8932                                    track_lock.push_hw_midi_events_to_port(route.to_port, events);
8933                                }
8934                            }
8935                        }
8936                        if track_lock.setup() {
8937                            reconfigured_tracks.push(track_name.clone());
8938                        }
8939                    }
8940                    self.publish_track_meters().await;
8941                    self.publish_clap_state_dirty().await;
8942                    for track_name in reconfigured_tracks {
8943                        let track = self.state.lock().tracks.get(&track_name).cloned();
8944                        if let Some(track) = track {
8945                            let (plugins, connections, connectable_connections) = {
8946                                let track_lock = track.lock();
8947                                (
8948                                    track_lock.plugin_graph_plugins(),
8949                                    track_lock.plugin_graph_connections(),
8950                                    track_lock.connectable_connections(),
8951                                )
8952                            };
8953                            self.notify_clients(Ok(Action::TrackPluginGraph {
8954                                track_name: track_name.clone(),
8955                                plugins,
8956                                connections,
8957                                connectable_connections,
8958                            }))
8959                            .await;
8960                        }
8961                    }
8962                    self.pending_hw_midi_events.clear();
8963                    self.pending_hw_midi_events_by_device.clear();
8964                    if self.playing {
8965                        if self.transport_panic_flush_pending {
8966                            self.transport_panic_flush_pending = false;
8967                        } else if self.transport_restart_pending {
8968                            self.transport_restart_pending = false;
8969                        } else {
8970                            let next = self
8971                                .transport_sample
8972                                .saturating_add(self.current_cycle_samples());
8973                            let normalized = self.normalize_transport_sample(next);
8974                            let wrapped = normalized != next;
8975                            self.transport_sample = normalized;
8976                            if wrapped {
8977                                if self.notified_loop_wrap_sample == Some(self.transport_sample) {
8978                                    self.notified_loop_wrap_sample = None;
8979                                } else {
8980                                    self.notify_clients(Ok(Action::TransportPosition(
8981                                        self.transport_sample,
8982                                    )))
8983                                    .await;
8984                                }
8985                            }
8986                        }
8987                    }
8988                    {
8989                        let echoes = self.apply_modulators(self.transport_sample);
8990                        for action in echoes {
8991                            self.notify_clients(Ok(action)).await;
8992                        }
8993                    }
8994                    self.invalidate_track_cycle_state();
8995                    let all_finished = self.send_tasks().await;
8996                    tracing::debug!(
8997                        "HWFinished send_tasks finished={} hw_worker={}",
8998                        all_finished,
8999                        self.hw_worker.is_some()
9000                    );
9001                    if all_finished && self.hw_worker.is_some() {
9002                        self.request_hw_cycle().await;
9003                    }
9004                    #[cfg(unix)]
9005                    {
9006                        if self.jack_runtime.is_some() {
9007                            self.awaiting_hwfinished = true;
9008                        }
9009                    }
9010                    self.handling_hwfinished = false;
9011                }
9012                Message::HWMidiEvents(events) => {
9013                    for hw_event in events {
9014                        let thru_targets: Vec<String> = self
9015                            .midi_hw_thru_routes
9016                            .iter()
9017                            .filter(|route| route.from_device == hw_event.device)
9018                            .map(|route| route.to_device.clone())
9019                            .collect();
9020                        for device in thru_targets {
9021                            self.pending_hw_midi_out_events_by_device.push(HwMidiEvent {
9022                                device,
9023                                event: hw_event.event.clone(),
9024                            });
9025                        }
9026                        if hw_event.event.data.len() >= 3 {
9027                            let status = hw_event.event.data[0];
9028                            if status & 0xF0 == 0xB0 {
9029                                let channel = status & 0x0F;
9030                                let cc = hw_event.event.data[1];
9031                                let value = hw_event.event.data[2];
9032                                self.handle_incoming_hw_cc(&hw_event.device, channel, cc, value)
9033                                    .await;
9034                            }
9035                            if self.step_recording_enabled && status & 0xF0 == 0x90 {
9036                                let channel = status & 0x0F;
9037                                let pitch = hw_event.event.data[1];
9038                                let velocity = hw_event.event.data[2];
9039                                if velocity > 0 {
9040                                    self.notify_clients(Ok(Action::StepRecordMidiNote {
9041                                        device: hw_event.device.clone(),
9042                                        channel,
9043                                        pitch,
9044                                        velocity,
9045                                    }))
9046                                    .await;
9047                                }
9048                            }
9049                        }
9050                        self.pending_hw_midi_events_by_device
9051                            .entry(hw_event.device)
9052                            .or_default()
9053                            .push(hw_event.event);
9054                    }
9055                }
9056                _ => {}
9057            }
9058        }
9059    }
9060
9061    fn collect_hw_midi_output_events(&self) -> Vec<MidiEvent> {
9062        let mut events = vec![];
9063        for track in self.state.lock().tracks.values() {
9064            events.extend(
9065                track
9066                    .lock()
9067                    .take_hw_midi_out_events()
9068                    .into_iter()
9069                    .map(|evt| evt.event),
9070            );
9071        }
9072        events.sort_by_key(|a| a.frame);
9073        events
9074    }
9075
9076    fn collect_hw_midi_output_events_by_device(&mut self) -> Vec<HwMidiEvent> {
9077        let mut events = Vec::<HwMidiEvent>::new();
9078        let routes = self.midi_hw_out_routes.clone();
9079        let mut events_by_track = HashMap::<String, Vec<crate::track::HwMidiOutEvent>>::new();
9080        {
9081            let state = self.state.lock();
9082            for route in &routes {
9083                if events_by_track.contains_key(&route.from_track) {
9084                    continue;
9085                }
9086                let Some(track) = state.tracks.get(&route.from_track) else {
9087                    continue;
9088                };
9089                events_by_track.insert(
9090                    route.from_track.clone(),
9091                    track.lock().take_hw_midi_out_events(),
9092                );
9093            }
9094        }
9095
9096        for route in routes {
9097            let Some(track_events) = events_by_track.get(&route.from_track) else {
9098                continue;
9099            };
9100            for hw_event in track_events
9101                .iter()
9102                .filter(|evt| evt.port == route.from_port)
9103            {
9104                self.update_active_hw_notes_for_track(
9105                    &route.from_track,
9106                    &route.device,
9107                    &hw_event.event.data,
9108                );
9109                events.push(HwMidiEvent {
9110                    device: route.device.clone(),
9111                    event: hw_event.event.clone(),
9112                });
9113            }
9114        }
9115        events.sort_by(|a, b| {
9116            a.event
9117                .frame
9118                .cmp(&b.event.frame)
9119                .then_with(|| a.device.cmp(&b.device))
9120        });
9121        events
9122    }
9123}
9124
9125#[cfg(test)]
9126mod tests {
9127    use super::*;
9128    use crate::mutex::UnsafeMutex;
9129    use tokio::sync::mpsc::channel;
9130    use tokio::time::{Duration as TokioDuration, timeout};
9131
9132    #[test]
9133    #[cfg(unix)]
9134    fn jack_transport_sync_decision_starts_and_syncs_position_on_external_play() {
9135        let decision = Engine::jack_transport_sync_decision(false, 128, true, 256, 64);
9136
9137        assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Start));
9138        assert_eq!(decision.position_sync, Some(256));
9139    }
9140
9141    #[test]
9142    #[cfg(unix)]
9143    fn jack_transport_sync_decision_stops_and_syncs_position_on_external_stop() {
9144        let decision = Engine::jack_transport_sync_decision(true, 512, false, 96, 64);
9145
9146        assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Stop));
9147        assert_eq!(decision.position_sync, Some(96));
9148    }
9149
9150    #[test]
9151    #[cfg(unix)]
9152    fn jack_transport_sync_decision_ignores_small_rolling_drift() {
9153        let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1040, 64);
9154
9155        assert_eq!(decision.play_sync, None);
9156        assert_eq!(decision.position_sync, None);
9157    }
9158
9159    #[test]
9160    #[cfg(unix)]
9161    fn jack_transport_sync_decision_syncs_large_rolling_jump() {
9162        let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1200, 64);
9163
9164        assert_eq!(decision.play_sync, None);
9165        assert_eq!(decision.position_sync, Some(1200));
9166    }
9167
9168    #[test]
9169    #[cfg(unix)]
9170    fn jack_transport_sync_decision_syncs_locate_while_stopped() {
9171        let decision = Engine::jack_transport_sync_decision(false, 400, false, 900, 64);
9172
9173        assert_eq!(decision.play_sync, None);
9174        assert_eq!(decision.position_sync, Some(900));
9175    }
9176
9177    fn make_engine_with_client() -> (Engine, tokio::sync::mpsc::Receiver<Message>) {
9178        let (engine_tx, engine_rx) = channel(16);
9179        let mut engine = Engine::new(engine_rx, engine_tx);
9180        let (client_tx, client_rx) = channel(16);
9181        engine.clients.push(client_tx);
9182        (engine, client_rx)
9183    }
9184
9185    fn insert_track(engine: &mut Engine, track: Track) {
9186        engine.state.lock().tracks.insert(
9187            track.name.clone(),
9188            Arc::new(UnsafeMutex::new(Box::new(track))),
9189        );
9190    }
9191
9192    fn osc_packet(address: &str) -> Vec<u8> {
9193        fn push_padded_osc_string(packet: &mut Vec<u8>, value: &str) {
9194            packet.extend_from_slice(value.as_bytes());
9195            packet.push(0);
9196            while !packet.len().is_multiple_of(4) {
9197                packet.push(0);
9198            }
9199        }
9200
9201        let mut packet = Vec::new();
9202        push_padded_osc_string(&mut packet, address);
9203        push_padded_osc_string(&mut packet, ",");
9204        packet
9205    }
9206
9207    #[tokio::test]
9208    async fn set_osc_enabled_starts_and_stops_server() {
9209        let (mut engine, _client_rx) = make_engine_with_client();
9210
9211        engine
9212            .set_osc_enabled_with(true, |tx| OscServer::start_on_addr(tx, "127.0.0.1:0"))
9213            .expect("start osc server on ephemeral port");
9214        assert!(engine.osc_server.is_some());
9215
9216        engine
9217            .set_osc_enabled_with(false, OscServer::start)
9218            .expect("stop osc server");
9219        assert!(engine.osc_server.is_none());
9220    }
9221
9222    #[tokio::test]
9223    async fn osc_server_forwards_transport_packets_to_engine_channel() {
9224        let (tx, mut rx) = channel(4);
9225        let mut server =
9226            OscServer::start_on_addr(tx, "127.0.0.1:0").expect("start osc test server");
9227        let socket = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind sender socket");
9228        let packet = osc_packet("/transport/play");
9229        socket
9230            .send_to(&packet, server.listen_addr())
9231            .expect("send osc packet");
9232
9233        let message = timeout(TokioDuration::from_secs(1), rx.recv())
9234            .await
9235            .expect("packet delivery timeout")
9236            .expect("osc message");
9237        match message {
9238            Message::Request(Action::Play) => {}
9239            other => panic!("unexpected osc message: {other:?}"),
9240        }
9241
9242        server.stop();
9243    }
9244
9245    #[tokio::test]
9246    async fn track_offline_bounce_rejects_zero_length_requests() {
9247        let (mut engine, mut client_rx) = make_engine_with_client();
9248        insert_track(
9249            &mut engine,
9250            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
9251        );
9252
9253        engine
9254            .handle_request(Action::TrackOfflineBounce {
9255                track_name: "track".to_string(),
9256                output_path: "/tmp/out.wav".to_string(),
9257                start_sample: 0,
9258                length_samples: 0,
9259                automation_lanes: vec![],
9260                apply_fader: false,
9261            })
9262            .await;
9263
9264        match client_rx.recv().await.expect("response") {
9265            Message::Response(Err(err)) => {
9266                assert!(err.contains("has no renderable content for offline bounce"));
9267            }
9268            other => panic!("unexpected message: {other:?}"),
9269        }
9270    }
9271
9272    #[tokio::test]
9273    async fn track_offline_bounce_rejects_when_same_track_is_active() {
9274        let (mut engine, mut client_rx) = make_engine_with_client();
9275        engine.offline_bounce_jobs.insert(
9276            "other".to_string(),
9277            OfflineBounceJob {
9278                cancel: Arc::new(AtomicBool::new(false)),
9279            },
9280        );
9281
9282        engine
9283            .handle_request(Action::TrackOfflineBounce {
9284                track_name: "other".to_string(),
9285                output_path: "/tmp/out.wav".to_string(),
9286                start_sample: 0,
9287                length_samples: 128,
9288                automation_lanes: vec![],
9289                apply_fader: false,
9290            })
9291            .await;
9292
9293        match client_rx.recv().await.expect("response") {
9294            Message::Response(Err(err)) => {
9295                assert!(err.contains("already in progress"));
9296            }
9297            other => panic!("unexpected message: {other:?}"),
9298        }
9299    }
9300
9301    #[tokio::test]
9302    async fn track_offline_bounce_allows_different_track_concurrently() {
9303        let (mut engine, _client_rx) = make_engine_with_client();
9304        insert_track(
9305            &mut engine,
9306            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
9307        );
9308        engine.offline_bounce_jobs.insert(
9309            "other".to_string(),
9310            OfflineBounceJob {
9311                cancel: Arc::new(AtomicBool::new(false)),
9312            },
9313        );
9314
9315        engine
9316            .handle_request(Action::TrackOfflineBounce {
9317                track_name: "track".to_string(),
9318                output_path: "/tmp/out.wav".to_string(),
9319                start_sample: 0,
9320                length_samples: 128,
9321                automation_lanes: vec![],
9322                apply_fader: false,
9323            })
9324            .await;
9325
9326        assert!(engine.offline_bounce_jobs.contains_key("other"));
9327        assert_eq!(engine.pending_requests.len(), 1);
9328    }
9329
9330    #[tokio::test]
9331    async fn reject_if_track_frozen_sends_error_and_blocks_operation() {
9332        let (mut engine, mut client_rx) = make_engine_with_client();
9333        let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
9334        track.set_frozen(true);
9335        insert_track(&mut engine, track);
9336
9337        let rejected = engine
9338            .reject_if_track_frozen("track", "arming/disarming")
9339            .await;
9340
9341        assert!(rejected);
9342        match client_rx.recv().await.expect("response") {
9343            Message::Response(Err(err)) => {
9344                assert_eq!(err, "Track 'track' is frozen; arming/disarming is blocked");
9345            }
9346            other => panic!("unexpected message: {other:?}"),
9347        }
9348    }
9349
9350    #[tokio::test]
9351    async fn undo_restores_original_clip_bounds_after_stretch_style_group() {
9352        let (mut engine, _client_rx) = make_engine_with_client();
9353        let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
9354        let mut clip = AudioClip::new("audio/original.wav".to_string(), 100, 220);
9355        clip.offset = 12;
9356        clip.fade_in_samples = 20;
9357        clip.fade_out_samples = 30;
9358        track.audio.clips.push(clip);
9359        insert_track(&mut engine, track);
9360
9361        engine.handle_request(Action::BeginHistoryGroup).await;
9362        engine
9363            .handle_request(Action::SetClipBounds {
9364                track_name: "track".to_string(),
9365                clip_index: 0,
9366                kind: Kind::Audio,
9367                start: 120,
9368                length: 180,
9369                offset: 0,
9370            })
9371            .await;
9372        engine
9373            .handle_request(Action::SetClipSourceName {
9374                track_name: "track".to_string(),
9375                clip_index: 0,
9376                kind: Kind::Audio,
9377                name: "audio/stretched.wav".to_string(),
9378            })
9379            .await;
9380        engine
9381            .handle_request(Action::SetClipFade {
9382                track_name: "track".to_string(),
9383                clip_index: 0,
9384                kind: Kind::Audio,
9385                fade_enabled: true,
9386                fade_in_samples: 12,
9387                fade_out_samples: 12,
9388            })
9389            .await;
9390        engine.handle_request(Action::EndHistoryGroup).await;
9391
9392        engine.handle_request(Action::Undo).await;
9393
9394        let state = engine.state.lock();
9395        let track = state.tracks.get("track").expect("track exists").lock();
9396        let clip = track.audio.clips.first().expect("clip exists");
9397        assert_eq!(clip.name, "audio/original.wav");
9398        assert_eq!(clip.start, 100);
9399        assert_eq!(clip.end, 220);
9400        assert_eq!(clip.end.saturating_sub(clip.start), 120);
9401        assert_eq!(clip.offset, 12);
9402    }
9403
9404    #[tokio::test]
9405    async fn track_offline_bounce_queues_when_no_worker_is_ready() {
9406        let (mut engine, _client_rx) = make_engine_with_client();
9407        insert_track(
9408            &mut engine,
9409            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
9410        );
9411
9412        engine
9413            .handle_request(Action::TrackOfflineBounce {
9414                track_name: "track".to_string(),
9415                output_path: "/tmp/out.wav".to_string(),
9416                start_sample: 0,
9417                length_samples: 128,
9418                automation_lanes: vec![],
9419                apply_fader: false,
9420            })
9421            .await;
9422
9423        assert!(engine.offline_bounce_jobs.is_empty());
9424        assert_eq!(engine.pending_requests.len(), 1);
9425        assert!(matches!(
9426            engine.pending_requests.front(),
9427            Some(Action::TrackOfflineBounce { track_name, length_samples, .. })
9428                if track_name == "track" && *length_samples == 128
9429        ));
9430    }
9431
9432    #[tokio::test]
9433    async fn track_offline_bounce_returns_missing_track_error() {
9434        let (mut engine, mut client_rx) = make_engine_with_client();
9435
9436        engine
9437            .handle_request(Action::TrackOfflineBounce {
9438                track_name: "missing".to_string(),
9439                output_path: "/tmp/out.wav".to_string(),
9440                start_sample: 0,
9441                length_samples: 128,
9442                automation_lanes: vec![],
9443                apply_fader: false,
9444            })
9445            .await;
9446
9447        match client_rx.recv().await.expect("response") {
9448            Message::Response(Err(err)) => {
9449                assert_eq!(err, "Track not found: missing");
9450            }
9451            other => panic!("unexpected message: {other:?}"),
9452        }
9453    }
9454
9455    #[tokio::test]
9456    async fn track_offline_bounce_clears_job_when_worker_send_fails() {
9457        let (mut engine, mut client_rx) = make_engine_with_client();
9458        insert_track(
9459            &mut engine,
9460            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
9461        );
9462        let (worker_tx, worker_rx) = channel(1);
9463        drop(worker_rx);
9464        engine
9465            .workers
9466            .push(WorkerData::new(worker_tx, tokio::spawn(async {})));
9467        engine.ready_workers.push(0);
9468
9469        engine
9470            .handle_request(Action::TrackOfflineBounce {
9471                track_name: "track".to_string(),
9472                output_path: "/tmp/out.wav".to_string(),
9473                start_sample: 0,
9474                length_samples: 128,
9475                automation_lanes: vec![],
9476                apply_fader: false,
9477            })
9478            .await;
9479
9480        assert!(engine.offline_bounce_jobs.is_empty());
9481        match client_rx.recv().await.expect("response") {
9482            Message::Response(Err(err)) => {
9483                assert!(err.contains("Failed to schedule offline bounce"));
9484            }
9485            other => panic!("unexpected message: {other:?}"),
9486        }
9487    }
9488
9489    #[tokio::test]
9490    async fn play_stop_play_keeps_clip_output_audible() {
9491        use crate::audio::clip::AudioClip;
9492        use crate::audio_codec::write_wav_f32;
9493
9494        let (engine_tx, engine_rx) = channel(16);
9495        let mut engine = Engine::new(engine_rx, engine_tx);
9496        let state = engine.state();
9497        let (client_tx, mut client_rx) = channel(16);
9498        engine.clients.push(client_tx);
9499        engine.init().await;
9500
9501        let tmp_dir = std::env::temp_dir().join("maolan_play_stop_play_test");
9502        let _ = std::fs::create_dir_all(&tmp_dir);
9503        let wav_path = tmp_dir.join("tone.wav");
9504        let sample_rate = 48_000u32;
9505        let clip_samples = sample_rate as usize;
9506        let mut samples = Vec::with_capacity(clip_samples);
9507        for i in 0..clip_samples {
9508            let phase = i as f32 / sample_rate as f32 * 2.0 * std::f32::consts::PI * 440.0;
9509            samples.push(phase.sin() * 0.5);
9510        }
9511        write_wav_f32(&wav_path, &samples, 1, sample_rate).expect("write wav");
9512
9513        let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 1024, sample_rate as f64);
9514        let mut clip = AudioClip::new(wav_path.to_string_lossy().to_string(), 0, clip_samples);
9515        clip.fade_enabled = false;
9516        track.audio.clips.push(clip);
9517        track.session_base_dir = Some(tmp_dir.clone());
9518        insert_track(&mut engine, track);
9519
9520        let tx = engine.tx.clone();
9521        let work_handle = tokio::spawn(async move {
9522            engine.work().await;
9523        });
9524
9525        // Wait for worker tasks to start up and send Ready messages.
9526        tokio::time::sleep(TokioDuration::from_millis(100)).await;
9527
9528        async fn drain_responses(
9529            client_rx: &mut tokio::sync::mpsc::Receiver<Message>,
9530            count: usize,
9531        ) {
9532            for _ in 0..count {
9533                let _ = tokio::time::timeout(TokioDuration::from_secs(2), client_rx.recv()).await;
9534            }
9535        }
9536
9537        async fn wait_for_track_processed(
9538            client_rx: &mut tokio::sync::mpsc::Receiver<Message>,
9539            state: &Arc<UnsafeMutex<State>>,
9540        ) -> bool {
9541            let deadline = Instant::now() + Duration::from_secs(5);
9542            while Instant::now() < deadline {
9543                let msg =
9544                    tokio::time::timeout(TokioDuration::from_millis(100), client_rx.recv()).await;
9545                if let Ok(Some(Message::Response(Ok(Action::TransportPosition(_)))))
9546                | Ok(Some(Message::Response(Ok(Action::Play)))) = msg
9547                {
9548                    let track_deadline = Instant::now() + Duration::from_secs(5);
9549                    while Instant::now() < track_deadline {
9550                        if state
9551                            .lock()
9552                            .tracks
9553                            .get("track")
9554                            .map(|t| t.lock().audio.finished)
9555                            .unwrap_or(false)
9556                        {
9557                            return true;
9558                        }
9559                        tokio::time::sleep(TokioDuration::from_millis(10)).await;
9560                    }
9561                }
9562            }
9563            false
9564        }
9565
9566        tx.send(Message::Request(Action::SetClipPlaybackEnabled(true)))
9567            .await
9568            .unwrap();
9569        tx.send(Message::Request(Action::Play)).await.unwrap();
9570        assert!(
9571            wait_for_track_processed(&mut client_rx, &state).await,
9572            "track did not process on first play"
9573        );
9574        let first_peak = {
9575            let state = state.lock();
9576            let track = state.tracks.get("track").expect("track").lock();
9577            let input = track.audio.ins[0].buffer.lock();
9578            crate::simd::peak_abs(input)
9579        };
9580        assert!(
9581            first_peak > 0.001,
9582            "expected audible input on first play, got {first_peak}"
9583        );
9584
9585        tx.send(Message::Request(Action::SetClipPlaybackEnabled(true)))
9586            .await
9587            .unwrap();
9588        tx.send(Message::Request(Action::Stop)).await.unwrap();
9589        drain_responses(&mut client_rx, 2).await;
9590
9591        tx.send(Message::Request(Action::SetClipPlaybackEnabled(true)))
9592            .await
9593            .unwrap();
9594        tx.send(Message::Request(Action::Play)).await.unwrap();
9595        assert!(
9596            wait_for_track_processed(&mut client_rx, &state).await,
9597            "track did not process on second play"
9598        );
9599        let second_peak = {
9600            let state = state.lock();
9601            let track = state.tracks.get("track").expect("track").lock();
9602            let input = track.audio.ins[0].buffer.lock();
9603            crate::simd::peak_abs(input)
9604        };
9605        assert!(
9606            second_peak > 0.001,
9607            "expected audible input on second play after stop, got {second_peak}"
9608        );
9609
9610        let _ = tx.send(Message::Request(Action::Quit)).await;
9611        tokio::time::sleep(TokioDuration::from_millis(200)).await;
9612        work_handle.abort();
9613        let _ = std::fs::remove_dir_all(&tmp_dir);
9614    }
9615
9616    #[test]
9617    fn modulator_sets_track_volume() {
9618        let (mut engine, _client_rx) = make_engine_with_client();
9619        let track = Track::new("vol-track".to_string(), 0, 2, 0, 0, 128, 48_000.0);
9620        insert_track(&mut engine, track);
9621
9622        engine.modulators = vec![crate::modulator::Modulator {
9623            id: 1,
9624            name: "LFO".to_string(),
9625            shape: crate::modulator::ModulatorShape::Sine,
9626            rate_hz: 1.0,
9627            phase: 0.0,
9628            enabled: true,
9629            targets: vec![crate::modulator::ModulatorTarget::TrackVolume {
9630                track_name: "vol-track".to_string(),
9631                min: -90.0,
9632                max: 20.0,
9633            }],
9634        }];
9635
9636        // At sample 12000 (1/4 period at 48kHz/1Hz), sine value maps to 1.0 -> max 20 dB.
9637        let echoes = engine.apply_modulators(12_000);
9638        let track = engine.state.lock().tracks["vol-track"].lock();
9639        assert!(
9640            (track.level() - 20.0).abs() < 0.01,
9641            "expected 20 dB, got {}",
9642            track.level()
9643        );
9644        assert!(
9645            echoes
9646                .iter()
9647                .any(|a| matches!(a, Action::TrackAutomationLevel(name, _) if name == "vol-track"))
9648        );
9649    }
9650
9651    #[test]
9652    fn modulator_sets_track_balance() {
9653        let (mut engine, _client_rx) = make_engine_with_client();
9654        let track = Track::new("pan-track".to_string(), 0, 2, 0, 0, 128, 48_000.0);
9655        insert_track(&mut engine, track);
9656
9657        engine.modulators = vec![crate::modulator::Modulator {
9658            id: 1,
9659            name: "LFO".to_string(),
9660            shape: crate::modulator::ModulatorShape::Sine,
9661            rate_hz: 1.0,
9662            phase: 0.0,
9663            enabled: true,
9664            targets: vec![crate::modulator::ModulatorTarget::TrackBalance {
9665                track_name: "pan-track".to_string(),
9666                min: -1.0,
9667                max: 1.0,
9668            }],
9669        }];
9670
9671        // At sample 12000 (1/4 period), sine value maps to 1.0 -> max balance 1.0.
9672        let echoes = engine.apply_modulators(12_000);
9673        let track = engine.state.lock().tracks["pan-track"].lock();
9674        assert!(
9675            (track.balance - 1.0).abs() < 0.01,
9676            "expected balance 1.0, got {}",
9677            track.balance
9678        );
9679        assert!(
9680            echoes.iter().any(
9681                |a| matches!(a, Action::TrackAutomationBalance(name, _) if name == "pan-track")
9682            )
9683        );
9684    }
9685
9686    #[tokio::test]
9687    async fn track_set_parent_wires_folder_input_to_child_input_and_child_output_to_folder_output()
9688    {
9689        let (mut engine, mut client_rx) = make_engine_with_client();
9690        let folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9691        let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9692        insert_track(&mut engine, folder);
9693        insert_track(&mut engine, child);
9694
9695        engine
9696            .handle_request_inner(
9697                Action::TrackSetParent {
9698                    track_name: "child".to_string(),
9699                    parent_name: Some("folder".to_string()),
9700                },
9701                false,
9702            )
9703            .await;
9704
9705        // Drain client messages so the channel does not block later drops.
9706        while let Ok(Some(_)) =
9707            tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
9708        {}
9709
9710        let state = engine.state.lock();
9711        let folder = state.tracks.get("folder").unwrap().lock();
9712        let child = state.tracks.get("child").unwrap().lock();
9713
9714        assert!(folder.child_tracks.iter().any(|c| c.lock().name == "child"));
9715        assert_eq!(child.parent_track.as_deref(), Some("folder"));
9716
9717        // Folder input -> child input.
9718        for (i, (parent_in, child_in)) in folder.audio.ins.iter().zip(&child.audio.ins).enumerate()
9719        {
9720            assert!(
9721                child_in
9722                    .connections
9723                    .lock()
9724                    .iter()
9725                    .any(|c| Arc::ptr_eq(c, parent_in)),
9726                "folder input {i} is not routed to child input {i}"
9727            );
9728            assert!(
9729                !parent_in
9730                    .connections
9731                    .lock()
9732                    .iter()
9733                    .any(|c| Arc::ptr_eq(c, child_in)),
9734                "folder input {i} should not read from child input {i}"
9735            );
9736        }
9737
9738        // Child output -> folder output.
9739        for (i, (child_out, parent_out)) in
9740            child.audio.outs.iter().zip(&folder.audio.outs).enumerate()
9741        {
9742            assert!(
9743                parent_out
9744                    .connections
9745                    .lock()
9746                    .iter()
9747                    .any(|c| Arc::ptr_eq(c, child_out)),
9748                "child output {i} is not routed to folder output {i}"
9749            );
9750        }
9751
9752        // Child passthrough is restored so audio can flow through.
9753        for (i, child_out) in child.audio.outs.iter().enumerate() {
9754            assert!(
9755                child_out.connections.lock().iter().any(|c| {
9756                    child
9757                        .audio
9758                        .ins
9759                        .get(i)
9760                        .is_some_and(|inp| Arc::ptr_eq(c, inp))
9761                }),
9762                "child output {i} is not connected to child input {i}"
9763            );
9764        }
9765    }
9766
9767    #[tokio::test]
9768    async fn track_set_parent_to_none_restores_root_passthrough() {
9769        let (mut engine, mut client_rx) = make_engine_with_client();
9770        let folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9771        let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9772        insert_track(&mut engine, folder);
9773        insert_track(&mut engine, child);
9774
9775        engine
9776            .handle_request_inner(
9777                Action::TrackSetParent {
9778                    track_name: "child".to_string(),
9779                    parent_name: Some("folder".to_string()),
9780                },
9781                false,
9782            )
9783            .await;
9784        engine
9785            .handle_request_inner(
9786                Action::TrackSetParent {
9787                    track_name: "child".to_string(),
9788                    parent_name: None,
9789                },
9790                false,
9791            )
9792            .await;
9793
9794        while let Ok(Some(_)) =
9795            tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
9796        {}
9797
9798        let state = engine.state.lock();
9799        let folder = state.tracks.get("folder").unwrap().lock();
9800        let child = state.tracks.get("child").unwrap().lock();
9801
9802        assert!(folder.child_tracks.is_empty());
9803        assert!(child.parent_track.is_none());
9804
9805        for (i, child_out) in child.audio.outs.iter().enumerate() {
9806            assert!(
9807                child_out.connections.lock().iter().any(|c| {
9808                    child
9809                        .audio
9810                        .ins
9811                        .get(i)
9812                        .is_some_and(|inp| Arc::ptr_eq(c, inp))
9813                }),
9814                "child output {i} should be connected to child input {i} after moving to root"
9815            );
9816        }
9817    }
9818
9819    #[tokio::test]
9820    async fn track_set_parent_wires_folder_midi_to_child_midi() {
9821        let (mut engine, mut client_rx) = make_engine_with_client();
9822        let folder = Track::new_folder("folder".to_string(), 0, 0, 1, 1, 64, 48_000.0);
9823        let child = Track::new("child".to_string(), 0, 0, 1, 1, 64, 48_000.0);
9824        insert_track(&mut engine, folder);
9825        insert_track(&mut engine, child);
9826
9827        engine
9828            .handle_request_inner(
9829                Action::TrackSetParent {
9830                    track_name: "child".to_string(),
9831                    parent_name: Some("folder".to_string()),
9832                },
9833                false,
9834            )
9835            .await;
9836
9837        while let Ok(Some(_)) =
9838            tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
9839        {}
9840
9841        let state = engine.state.lock();
9842        let folder = state.tracks.get("folder").unwrap().lock();
9843        let child = state.tracks.get("child").unwrap().lock();
9844
9845        let folder_midi_in = &folder.midi.ins[0];
9846        let child_midi_in = &child.midi.ins[0];
9847        assert!(
9848            child_midi_in
9849                .lock()
9850                .connections
9851                .iter()
9852                .any(|c| Arc::ptr_eq(c, folder_midi_in)),
9853            "folder MIDI input should be routed to child MIDI input"
9854        );
9855
9856        let child_midi_out = &child.midi.outs[0];
9857        let folder_midi_out = &folder.midi.outs[0];
9858        assert!(
9859            child_midi_out
9860                .lock()
9861                .connections
9862                .iter()
9863                .any(|c| Arc::ptr_eq(c, folder_midi_out)),
9864            "child MIDI output should be routed to folder MIDI output"
9865        );
9866    }
9867
9868    #[test]
9869    fn nested_folder_expands_in_task_graph() {
9870        let (mut engine, _client_rx) = make_engine_with_client();
9871        let outer = Track::new_folder("outer".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9872        let inner = Track::new_folder("inner".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9873        let leaf = Track::new("leaf".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9874        insert_track(&mut engine, outer);
9875        insert_track(&mut engine, inner);
9876        insert_track(&mut engine, leaf);
9877
9878        {
9879            let state = engine.state.lock();
9880            let outer = state.tracks.get("outer").unwrap().clone();
9881            let inner = state.tracks.get("inner").unwrap().clone();
9882            let leaf = state.tracks.get("leaf").unwrap().clone();
9883            outer.lock().child_tracks.push(inner.clone());
9884            inner.lock().child_tracks.push(leaf.clone());
9885            inner.lock().parent_track = Some("outer".to_string());
9886            leaf.lock().parent_track = Some("inner".to_string());
9887        }
9888
9889        let (tasks, deps) = engine.build_task_graph();
9890        let names: Vec<String> = tasks
9891            .iter()
9892            .map(|t| match t {
9893                ProcessTask::Track(t) => format!("track:{}", t.lock().name.clone()),
9894                ProcessTask::FolderInput(t) => format!("in:{}", t.lock().name.clone()),
9895                ProcessTask::FolderOutput(t) => format!("out:{}", t.lock().name.clone()),
9896                ProcessTask::Plugin { track, .. } => {
9897                    format!("plugin:{}", track.lock().name.clone())
9898                }
9899            })
9900            .collect();
9901
9902        let expected = vec![
9903            "in:outer",
9904            "in:inner",
9905            "track:leaf",
9906            "out:inner",
9907            "out:outer",
9908        ];
9909        assert_eq!(names, expected, "task graph should expand nested folders");
9910
9911        // Each task should depend on the previous one.
9912        for window in tasks.windows(2) {
9913            let prev = &window[0];
9914            let next = &window[1];
9915            let prev_key = Engine::task_key(prev);
9916            let next_key = Engine::task_key(next);
9917            assert!(
9918                deps.get(&next_key).is_some_and(|d| d.contains(&prev_key)),
9919                "{:?} should depend on {:?}",
9920                next,
9921                prev
9922            );
9923        }
9924    }
9925
9926    #[test]
9927    fn child_to_plugin_to_folder_output_task_graph_has_no_cycle() {
9928        use crate::message::ConnectableRef;
9929
9930        let plugin_path = Path::new(env!("CARGO_MANIFEST_DIR"))
9931            .parent()
9932            .unwrap()
9933            .join("daw")
9934            .join("plugin-host")
9935            .join("tests")
9936            .join("test_passthrough.clap");
9937        if !plugin_path.exists() {
9938            return;
9939        }
9940        if crate::plugins::ipc::find_plugin_host_binary().is_none() {
9941            return;
9942        }
9943
9944        let (mut engine, _client_rx) = make_engine_with_client();
9945        let mut folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9946        let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9947
9948        folder
9949            .load_clap_plugin(
9950                &format!("{}::com.maolan.test.passthrough", plugin_path.display()),
9951                None,
9952            )
9953            .expect("should load CLAP plugin on folder");
9954        folder.clap_plugins[0].processor.lock().setup_audio_ports();
9955        let plugin_id = folder.clap_plugins[0].id;
9956
9957        insert_track(&mut engine, folder);
9958        insert_track(&mut engine, child);
9959
9960        {
9961            let state = engine.state.lock();
9962            let folder = state.tracks.get("folder").unwrap().clone();
9963            let child = state.tracks.get("child").unwrap().clone();
9964            folder.lock().child_tracks.push(child.clone());
9965            child.lock().parent_track = Some("folder".to_string());
9966
9967            folder
9968                .lock()
9969                .connect_audio_connectable(
9970                    ConnectableRef::ChildTrack("child".to_string()),
9971                    0,
9972                    ConnectableRef::ClapPlugin(plugin_id),
9973                    0,
9974                )
9975                .expect("connect child L to plugin L");
9976            folder
9977                .lock()
9978                .connect_audio_connectable(
9979                    ConnectableRef::ChildTrack("child".to_string()),
9980                    1,
9981                    ConnectableRef::ClapPlugin(plugin_id),
9982                    1,
9983                )
9984                .expect("connect child R to plugin R");
9985            folder
9986                .lock()
9987                .connect_audio_connectable(
9988                    ConnectableRef::ClapPlugin(plugin_id),
9989                    0,
9990                    ConnectableRef::TrackOutput,
9991                    0,
9992                )
9993                .expect("connect plugin L to folder output L");
9994            folder
9995                .lock()
9996                .connect_audio_connectable(
9997                    ConnectableRef::ClapPlugin(plugin_id),
9998                    1,
9999                    ConnectableRef::TrackOutput,
10000                    1,
10001                )
10002                .expect("connect plugin R to folder output R");
10003        }
10004
10005        let (tasks, deps) = engine.build_task_graph();
10006
10007        let folder_in_key = tasks
10008            .iter()
10009            .find(|t| matches!(t, ProcessTask::FolderInput(t) if t.lock().name == "folder"))
10010            .map(Engine::task_key)
10011            .expect("folder input task");
10012        let child_key = tasks
10013            .iter()
10014            .find(|t| matches!(t, ProcessTask::Track(t) if t.lock().name == "child"))
10015            .map(Engine::task_key)
10016            .expect("child task");
10017        let plugin_key = tasks
10018            .iter()
10019            .find(|t| {
10020                matches!(
10021                    t,
10022                    ProcessTask::Plugin {
10023                        track,
10024                        kind: PluginKind::Clap,
10025                        index: 0,
10026                    } if track.lock().name == "folder"
10027                )
10028            })
10029            .map(Engine::task_key)
10030            .expect("plugin task");
10031        let folder_out_key = tasks
10032            .iter()
10033            .find(|t| matches!(t, ProcessTask::FolderOutput(t) if t.lock().name == "folder"))
10034            .map(Engine::task_key)
10035            .expect("folder output task");
10036
10037        assert!(
10038            deps.get(&child_key)
10039                .is_some_and(|d| d.contains(&folder_in_key)),
10040            "child task should depend on folder input"
10041        );
10042        assert!(
10043            deps.get(&plugin_key)
10044                .is_some_and(|d| d.contains(&folder_in_key) && d.contains(&child_key)),
10045            "plugin task should depend on folder input and child"
10046        );
10047        assert!(
10048            deps.get(&folder_out_key).is_some_and(|d| {
10049                d.contains(&folder_in_key) && d.contains(&plugin_key) && d.contains(&child_key)
10050            }),
10051            "folder output should depend on folder input, plugin, and child"
10052        );
10053
10054        fn has_cycle(deps: &HashMap<String, Vec<String>>) -> bool {
10055            let mut state: HashMap<String, u8> = HashMap::new();
10056            fn visit(
10057                node: &str,
10058                deps: &HashMap<String, Vec<String>>,
10059                state: &mut HashMap<String, u8>,
10060            ) -> bool {
10061                match state.get(node).copied() {
10062                    Some(1) => return true,
10063                    Some(2) => return false,
10064                    _ => {}
10065                }
10066                state.insert(node.to_string(), 1);
10067                for next in deps.get(node).into_iter().flatten() {
10068                    if visit(next, deps, state) {
10069                        return true;
10070                    }
10071                }
10072                state.insert(node.to_string(), 2);
10073                false
10074            }
10075            for node in deps.keys() {
10076                if visit(node, deps, &mut state) {
10077                    return true;
10078                }
10079            }
10080            false
10081        }
10082
10083        assert!(
10084            !has_cycle(&deps),
10085            "task graph should not contain a cycle when a plugin reads from a child track"
10086        );
10087    }
10088
10089    #[tokio::test]
10090    async fn track_set_parent_wires_child_io_to_folder_even_after_addtrack() {
10091        let (mut engine, mut client_rx) = make_engine_with_client();
10092        let folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
10093        let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
10094        insert_track(&mut engine, folder);
10095        insert_track(&mut engine, child);
10096
10097        engine
10098            .handle_request_inner(
10099                Action::TrackSetParent {
10100                    track_name: "child".to_string(),
10101                    parent_name: Some("folder".to_string()),
10102                },
10103                false,
10104            )
10105            .await;
10106
10107        while let Ok(Some(_)) =
10108            tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
10109        {}
10110
10111        let state = engine.state.lock();
10112        let folder = state.tracks.get("folder").unwrap().lock();
10113        let child = state.tracks.get("child").unwrap().lock();
10114
10115        // Folder input -> child input.
10116        for (i, (parent_in, child_in)) in folder.audio.ins.iter().zip(&child.audio.ins).enumerate()
10117        {
10118            assert!(
10119                child_in
10120                    .connections
10121                    .lock()
10122                    .iter()
10123                    .any(|c| Arc::ptr_eq(c, parent_in)),
10124                "folder input {i} is not routed to child input {i}"
10125            );
10126        }
10127
10128        // Child output -> folder output.
10129        for (i, (child_out, parent_out)) in
10130            child.audio.outs.iter().zip(&folder.audio.outs).enumerate()
10131        {
10132            assert!(
10133                parent_out
10134                    .connections
10135                    .lock()
10136                    .iter()
10137                    .any(|c| Arc::ptr_eq(c, child_out)),
10138                "child output {i} is not routed to folder output {i}"
10139            );
10140        }
10141    }
10142
10143    #[tokio::test]
10144    async fn folder_child_audio_passes_through() {
10145        let (mut engine, mut client_rx) = make_engine_with_client();
10146        let folder = Track::new_folder("folder".to_string(), 1, 1, 0, 0, 64, 48_000.0);
10147        let child = Track::new("child".to_string(), 1, 1, 0, 0, 64, 48_000.0);
10148        insert_track(&mut engine, folder);
10149        insert_track(&mut engine, child);
10150
10151        engine
10152            .handle_request_inner(
10153                Action::TrackSetParent {
10154                    track_name: "child".to_string(),
10155                    parent_name: Some("folder".to_string()),
10156                },
10157                false,
10158            )
10159            .await;
10160        while let Ok(Some(_)) =
10161            tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
10162        {}
10163
10164        {
10165            let state = engine.state.lock();
10166            let folder = state.tracks.get("folder").unwrap().clone();
10167            let child = state.tracks.get("child").unwrap().clone();
10168
10169            folder.lock().input_monitor = vec![true];
10170            child.lock().input_monitor = vec![true];
10171
10172            // Feed a signal into the folder input from an external source.
10173            let source = Arc::new(crate::audio::io::AudioIO::new(64));
10174            for sample in source.buffer.lock().iter_mut() {
10175                *sample = 0.75;
10176            }
10177            crate::audio::io::AudioIO::connect(&source, &folder.lock().audio.ins[0]);
10178
10179            folder.lock().process_folder_input();
10180            child.lock().process();
10181            folder.lock().process_folder_output();
10182
10183            let output = folder.lock().audio.outs[0].buffer.lock();
10184            assert!(
10185                output.iter().any(|s| (*s - 0.75).abs() < 1e-5),
10186                "folder output should contain the child-processed folder input signal, got {:?}",
10187                output.iter().take(8).collect::<Vec<_>>()
10188            );
10189        }
10190    }
10191
10192    #[tokio::test]
10193    async fn remove_folder_track_deletes_descendants_recursively() {
10194        let (mut engine, mut client_rx) = make_engine_with_client();
10195        let folder = Track::new_folder("folder".to_string(), 1, 1, 0, 0, 64, 48_000.0);
10196        let child = Track::new_folder("child".to_string(), 1, 1, 0, 0, 64, 48_000.0);
10197        let grandchild = Track::new("grandchild".to_string(), 1, 1, 0, 0, 64, 48_000.0);
10198        insert_track(&mut engine, folder);
10199        insert_track(&mut engine, child);
10200        insert_track(&mut engine, grandchild);
10201
10202        engine
10203            .handle_request(Action::TrackSetParent {
10204                track_name: "child".to_string(),
10205                parent_name: Some("folder".to_string()),
10206            })
10207            .await;
10208        engine
10209            .handle_request(Action::TrackSetParent {
10210                track_name: "grandchild".to_string(),
10211                parent_name: Some("child".to_string()),
10212            })
10213            .await;
10214
10215        // Drain TrackSetParent notifications so we can inspect the removal notifications.
10216        while let Ok(Some(_)) =
10217            tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
10218        {}
10219
10220        engine
10221            .handle_request(Action::RemoveTrack("folder".to_string()))
10222            .await;
10223
10224        {
10225            let state = engine.state.lock();
10226            assert!(
10227                !state.tracks.contains_key("folder"),
10228                "folder should have been removed"
10229            );
10230            assert!(
10231                !state.tracks.contains_key("child"),
10232                "child should have been removed"
10233            );
10234            assert!(
10235                !state.tracks.contains_key("grandchild"),
10236                "grandchild should have been removed"
10237            );
10238        }
10239
10240        let mut removed_names = Vec::new();
10241        for _ in 0..3 {
10242            let msg = tokio::time::timeout(TokioDuration::from_millis(100), client_rx.recv()).await;
10243            if let Ok(Some(Message::Response(Ok(Action::RemoveTrack(name))))) = msg {
10244                removed_names.push(name);
10245            }
10246        }
10247        assert_eq!(
10248            removed_names,
10249            vec!["grandchild", "child", "folder"],
10250            "descendants should be removed before the folder and clients notified"
10251        );
10252    }
10253}