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