Skip to main content

maolan_engine/
engine.rs

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