Skip to main content

maolan_engine/
engine.rs

1use midly::{
2    Arena, Format, Header, MetaMessage, Smf, Timing, TrackEvent, TrackEventKind,
3    live::LiveEvent,
4    num::{u15, u24, u28},
5};
6#[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
7use std::fs::read_dir;
8use std::{
9    collections::{HashMap, VecDeque},
10    fs::File,
11    path::{Path, PathBuf},
12    sync::{
13        Arc,
14        atomic::{AtomicBool, Ordering},
15    },
16    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
17};
18use tokio::sync::mpsc::{Receiver, Sender, channel};
19use tokio::task::JoinHandle;
20use tracing::error;
21
22/// Hardware device information: (input_channels, output_channels, sample_rate, latency_ranges)
23type HwDeviceInfo = (usize, usize, usize, ((usize, usize), (usize, usize)));
24
25#[cfg(target_os = "linux")]
26use crate::hw::alsa::{HwDriver, HwOptions, MidiHub};
27#[cfg(target_os = "macos")]
28use crate::hw::coreaudio::{HwDriver, HwOptions, MidiHub};
29#[cfg(unix)]
30use crate::hw::jack::JackRuntime;
31#[cfg(target_os = "windows")]
32use crate::hw::options::HwOptions;
33#[cfg(target_os = "freebsd")]
34use crate::hw::oss as hw;
35#[cfg(target_os = "freebsd")]
36use crate::hw::oss::{HwDriver, HwOptions, MidiHub};
37#[cfg(target_os = "openbsd")]
38use crate::hw::sndio::{HwDriver, HwOptions, MidiHub};
39#[cfg(target_os = "windows")]
40use crate::hw::wasapi::{self, HwDriver, MidiHub};
41#[cfg(target_os = "linux")]
42use crate::workers::alsa_worker::HwWorker;
43#[cfg(target_os = "macos")]
44use crate::workers::coreaudio_worker::HwWorker;
45#[cfg(target_os = "freebsd")]
46use crate::workers::oss_worker::HwWorker;
47#[cfg(target_os = "openbsd")]
48use crate::workers::sndio_worker::HwWorker;
49#[cfg(target_os = "windows")]
50use crate::workers::wasapi_worker::HwWorker;
51use crate::{
52    audio::clip::AudioClip,
53    audio::io::AudioIO,
54    history::{History, UndoEntry, create_inverse_actions, should_record},
55    hw::{config, traits::HwDevice},
56    kind::Kind,
57    message::{Action, HwMidiEvent, Message, MidiControllerData, MidiNoteData},
58    midi::clip::MIDIClip,
59    midi::io::MidiEvent,
60    mutex::UnsafeMutex,
61    osc::OscServer,
62    routing,
63    state::State,
64    track::Track,
65    workers::worker::Worker,
66};
67
68#[derive(Debug)]
69struct WorkerData {
70    tx: Sender<Message>,
71    handle: JoinHandle<()>,
72}
73
74impl WorkerData {
75    pub fn new(tx: Sender<Message>, handle: JoinHandle<()>) -> Self {
76        Self { tx, handle }
77    }
78}
79
80#[derive(Clone, Copy, Debug, PartialEq, Eq)]
81enum WorkerClass {
82    Realtime,
83    Refill,
84}
85
86#[derive(Debug, Clone)]
87struct RecordingSession {
88    start_sample: usize,
89    samples: Vec<f32>,
90    channels: usize,
91    file_name: String,
92}
93
94#[derive(Debug, Clone)]
95struct MidiRecordingSession {
96    start_sample: usize,
97    events: Vec<(u64, Vec<u8>)>,
98    file_name: String,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Hash)]
102struct MidiHwInRoute {
103    device: String,
104    to_track: String,
105    to_port: usize,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Hash)]
109struct MidiHwOutRoute {
110    from_track: String,
111    from_port: usize,
112    device: String,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Hash)]
116struct MidiHwThruRoute {
117    from_device: String,
118    to_device: String,
119}
120
121struct OfflineBounceJob {
122    cancel: Arc<AtomicBool>,
123}
124
125#[cfg(unix)]
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127enum JackTransportPlaySync {
128    Start,
129    Stop,
130}
131
132#[derive(Clone, Copy)]
133#[cfg(unix)]
134struct AudioOpenRequest<'a> {
135    device: &'a str,
136    input_device: Option<&'a str>,
137    sample_rate_hz: i32,
138    bits: i32,
139    exclusive: bool,
140    period_frames: usize,
141    realtime_frames: usize,
142    low_watermark_frames: usize,
143    nperiods: usize,
144    sync_mode: bool,
145    hybrid_enabled: bool,
146}
147
148struct ClipAddRequest<'a> {
149    name: &'a str,
150    track_name: &'a str,
151    start: usize,
152    length: usize,
153    offset: usize,
154    input_channel: usize,
155    muted: bool,
156    peaks_file: Option<String>,
157    kind: Kind,
158    fade_enabled: bool,
159    fade_in_samples: usize,
160    fade_out_samples: usize,
161    source_name: Option<String>,
162    source_offset: Option<usize>,
163    source_length: Option<usize>,
164    preview_name: Option<String>,
165    pitch_correction_points: Vec<crate::message::PitchCorrectionPointData>,
166    pitch_correction_frame_likeness: Option<f32>,
167    pitch_correction_inertia_ms: Option<u16>,
168    pitch_correction_formant_compensation: Option<bool>,
169    plugin_graph_json: Option<serde_json::Value>,
170}
171
172#[cfg(unix)]
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174struct JackTransportSyncDecision {
175    play_sync: Option<JackTransportPlaySync>,
176    position_sync: Option<usize>,
177}
178
179#[derive(Clone, Debug, PartialEq, Eq)]
180enum MidiLearnSlot {
181    Track(String, crate::message::TrackMidiLearnTarget),
182    Global(crate::message::GlobalMidiLearnTarget),
183}
184
185pub struct Engine {
186    clients: Vec<Sender<Message>>,
187    rx: Receiver<Message>,
188    state: Arc<UnsafeMutex<State>>,
189    tx: Sender<Message>,
190    workers: Vec<WorkerData>,
191    hw_driver: Option<Arc<UnsafeMutex<HwDriver>>>,
192    #[cfg(unix)]
193    jack_runtime: Option<Arc<UnsafeMutex<JackRuntime>>>,
194    midi_hub: Arc<UnsafeMutex<MidiHub>>,
195    hw_worker: Option<WorkerData>,
196    osc_server: Option<OscServer>,
197    pending_hw_midi_events: Vec<MidiEvent>,
198    pending_hw_midi_events_by_device: HashMap<String, Vec<MidiEvent>>,
199    pending_hw_midi_out_events: Vec<MidiEvent>,
200    pending_hw_midi_out_events_by_device: Vec<HwMidiEvent>,
201    active_hw_notes_by_track: HashMap<String, std::collections::HashSet<(String, u8, u8)>>,
202    active_hw_notes_cycle_start: HashMap<String, std::collections::HashSet<(String, u8, u8)>>,
203    midi_hw_in_routes: Vec<MidiHwInRoute>,
204    midi_hw_out_routes: Vec<MidiHwOutRoute>,
205    midi_hw_thru_routes: Vec<MidiHwThruRoute>,
206    worker_classes: Vec<WorkerClass>,
207    ready_realtime_workers: Vec<usize>,
208    ready_refill_workers: Vec<usize>,
209    pending_requests: VecDeque<Action>,
210    awaiting_hwfinished: bool,
211    handling_hwfinished: bool,
212    track_process_epoch: usize,
213    transport_panic_flush_pending: bool,
214    transport_restart_pending: bool,
215    transport_sample: usize,
216    loop_enabled: bool,
217    loop_range_samples: Option<(usize, usize)>,
218    metronome_enabled: bool,
219    tempo_bpm: f64,
220    tsig_num: u16,
221    tsig_denom: u16,
222    punch_enabled: bool,
223    punch_range_samples: Option<(usize, usize)>,
224    audio_recordings: std::collections::HashMap<String, RecordingSession>,
225    midi_recordings: std::collections::HashMap<String, MidiRecordingSession>,
226    completed_audio_recordings: Vec<(String, RecordingSession)>,
227    completed_midi_recordings: Vec<(String, MidiRecordingSession)>,
228    playing: bool,
229    clip_playback_enabled: bool,
230    record_enabled: bool,
231    session_dir: Option<PathBuf>,
232    hw_out_level_db: f32,
233    hw_out_balance: f32,
234    hw_out_muted: bool,
235    last_hw_out_meter_publish: Option<Instant>,
236    #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
237    last_hw_out_meter_linear: Vec<f32>,
238    hw_out_peak_hold_linear: Vec<f32>,
239    #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
240    hw_out_meter_publish_phase: bool,
241    last_track_meter_publish: Option<Instant>,
242    track_meter_linear_by_track: HashMap<String, Vec<f32>>,
243    track_processing_started_at: HashMap<String, Instant>,
244    latest_hw_out_meter_db: Arc<Vec<f32>>,
245    latest_track_meter_snapshot: Arc<Vec<(String, Vec<f32>)>>,
246    history: History,
247    history_group: Option<UndoEntry>,
248    history_suspended: bool,
249    offline_bounce_jobs: HashMap<String, OfflineBounceJob>,
250    pending_midi_learn: Option<(String, crate::message::TrackMidiLearnTarget, Option<String>)>,
251    pending_global_midi_learn: Option<crate::message::GlobalMidiLearnTarget>,
252    global_midi_learn_play_pause: Option<crate::message::MidiLearnBinding>,
253    global_midi_learn_stop: Option<crate::message::MidiLearnBinding>,
254    global_midi_learn_record_toggle: Option<crate::message::MidiLearnBinding>,
255    midi_cc_gate: HashMap<(String, u8, u8), bool>,
256    hybrid_low_watermark_frames: usize,
257    hybrid_realtime_frames: usize,
258    hybrid_playback_frames: usize,
259    hybrid_enabled: bool,
260    refill_budget_per_pass: usize,
261    realtime_fallback_enabled: bool,
262    realtime_fallback_budget_per_pass: usize,
263    refill_budget_throttle_count: usize,
264    realtime_fallback_dispatch_count: usize,
265}
266
267type MidiEditParseResult = (
268    Vec<MidiNoteData>,
269    Vec<MidiControllerData>,
270    Vec<(u64, Vec<u8>)>,
271);
272
273impl Engine {
274    pub fn state(&self) -> Arc<UnsafeMutex<State>> {
275        self.state.clone()
276    }
277
278    const METRONOME_TRACK: &'static str = "metronome";
279    const METRONOME_DEFAULT_LEVEL_DB: f32 = -10.0;
280    const MIDI_CC_ALL_SOUND_OFF: u8 = 120;
281    const MIDI_CC_ALL_NOTES_OFF: u8 = 123;
282    const MIDI_CC_SUSTAIN_PEDAL: u8 = 64;
283
284    fn default_clip_plugin_graph_json(audio_ins: usize, audio_outs: usize) -> serde_json::Value {
285        let connections = (0..audio_ins.min(audio_outs))
286            .map(|port| {
287                serde_json::json!({
288                    "from_node": "TrackInput",
289                    "from_port": port,
290                    "to_node": "TrackOutput",
291                    "to_port": port,
292                    "kind": "Audio",
293                })
294            })
295            .collect::<Vec<_>>();
296        serde_json::json!({
297            "plugins": [],
298            "connections": connections,
299        })
300    }
301
302    fn meter_linear_to_db(peak: f32) -> f32 {
303        if peak <= 1.0e-6 {
304            -90.0
305        } else {
306            (20.0 * peak.log10()).clamp(-90.0, 20.0)
307        }
308    }
309
310    fn note_off_events_for_track(&mut self, track_name: &str) -> Vec<HwMidiEvent> {
311        let Some(active) = self.active_hw_notes_by_track.remove(track_name) else {
312            return vec![];
313        };
314        let mut channels = std::collections::HashSet::<(String, u8)>::new();
315        let mut events = Vec::with_capacity(active.len() * 2);
316        for (device, channel, pitch) in active {
317            channels.insert((device.clone(), channel));
318            events.push(HwMidiEvent {
319                device,
320                event: MidiEvent::new(0, vec![0x80 | channel.min(15), pitch.min(127), 64]),
321            });
322        }
323        for (device, channel) in channels {
324            events.push(HwMidiEvent {
325                device,
326                event: MidiEvent::new(
327                    0,
328                    vec![0xB0 | channel.min(15), Self::MIDI_CC_SUSTAIN_PEDAL, 0],
329                ),
330            });
331        }
332        events
333    }
334
335    fn set_clip_plugin_graph_json(
336        &mut self,
337        track_name: &str,
338        clip_index: usize,
339        plugin_graph_json: Option<serde_json::Value>,
340    ) {
341        if let Some(track) = self.state.lock().tracks.get(track_name) {
342            let track = track.lock();
343            if let Some(clip) = track.audio.clips.get_mut(clip_index) {
344                clip.plugin_graph_json = plugin_graph_json;
345            }
346        }
347    }
348
349    fn update_active_hw_notes_for_track(&mut self, track_name: &str, device: &str, data: &[u8]) {
350        let Some(status) = data.first().copied() else {
351            return;
352        };
353        let channel = status & 0x0F;
354        match status & 0xF0 {
355            0x80 => {
356                if let Some(&pitch) = data.get(1)
357                    && let Some(active) = self.active_hw_notes_by_track.get_mut(track_name)
358                {
359                    active.remove(&(device.to_string(), channel, pitch));
360                    if active.is_empty() {
361                        self.active_hw_notes_by_track.remove(track_name);
362                    }
363                }
364            }
365            0x90 => {
366                let Some(&pitch) = data.get(1) else {
367                    return;
368                };
369                let velocity = data.get(2).copied().unwrap_or(0);
370                if velocity == 0 {
371                    if let Some(active) = self.active_hw_notes_by_track.get_mut(track_name) {
372                        active.remove(&(device.to_string(), channel, pitch));
373                        if active.is_empty() {
374                            self.active_hw_notes_by_track.remove(track_name);
375                        }
376                    }
377                } else {
378                    self.active_hw_notes_by_track
379                        .entry(track_name.to_string())
380                        .or_default()
381                        .insert((device.to_string(), channel, pitch));
382                }
383            }
384            _ => {}
385        }
386    }
387
388    fn note_off_events_for_all_active_tracks(&mut self) -> Vec<HwMidiEvent> {
389        let track_names: Vec<String> = self.active_hw_notes_by_track.keys().cloned().collect();
390        let mut events = Vec::new();
391        for track_name in track_names {
392            events.extend(self.note_off_events_for_track(&track_name));
393        }
394        events
395    }
396
397    fn panic_events_for_all_hw_midi_outputs(&self) -> Vec<HwMidiEvent> {
398        let devices = {
399            let midi_hub = self.midi_hub.lock();
400            midi_hub.output_devices()
401        };
402        let mut events = Vec::with_capacity(devices.len() * 16 * 3);
403        for device in devices {
404            for channel in 0..16_u8 {
405                events.push(HwMidiEvent {
406                    device: device.clone(),
407                    event: MidiEvent::new(0, vec![0xB0 | channel, Self::MIDI_CC_SUSTAIN_PEDAL, 0]),
408                });
409                events.push(HwMidiEvent {
410                    device: device.clone(),
411                    event: MidiEvent::new(0, vec![0xB0 | channel, Self::MIDI_CC_ALL_SOUND_OFF, 0]),
412                });
413                events.push(HwMidiEvent {
414                    device: device.clone(),
415                    event: MidiEvent::new(0, vec![0xB0 | channel, Self::MIDI_CC_ALL_NOTES_OFF, 0]),
416                });
417            }
418        }
419        events
420    }
421
422    fn note_off_events_for_active_snapshot(
423        &self,
424        snapshot: &HashMap<String, std::collections::HashSet<(String, u8, u8)>>,
425        frame: u32,
426    ) -> Vec<HwMidiEvent> {
427        let mut channels = std::collections::HashSet::<(String, u8)>::new();
428        let mut events = Vec::new();
429        for active in snapshot.values() {
430            for (device, channel, pitch) in active {
431                channels.insert((device.clone(), *channel));
432                events.push(HwMidiEvent {
433                    device: device.clone(),
434                    event: MidiEvent::new(
435                        frame,
436                        vec![0x80 | (*channel).min(15), (*pitch).min(127), 64],
437                    ),
438                });
439            }
440        }
441        for (device, channel) in channels {
442            events.push(HwMidiEvent {
443                device,
444                event: MidiEvent::new(
445                    frame,
446                    vec![0xB0 | channel.min(15), Self::MIDI_CC_SUSTAIN_PEDAL, 0],
447                ),
448            });
449        }
450        events
451    }
452
453    fn parse_midi_clip_for_edit(
454        path: &Path,
455        sample_rate: f64,
456        clip_start: usize,
457    ) -> Result<MidiEditParseResult, String> {
458        let bytes = std::fs::read(path).map_err(|e| e.to_string())?;
459        let smf = Smf::parse(&bytes).map_err(|e| e.to_string())?;
460        let Timing::Metrical(ppq) = smf.header.timing else {
461            return Ok((vec![], vec![], vec![]));
462        };
463        let ppq = u64::from(ppq.as_int().max(1));
464
465        let mut tempo_changes: Vec<(u64, u32)> = vec![(0, 500_000)];
466        for track in &smf.tracks {
467            let mut tick = 0_u64;
468            for event in track {
469                tick = tick.saturating_add(event.delta.as_int() as u64);
470                if let TrackEventKind::Meta(MetaMessage::Tempo(us_per_q)) = event.kind {
471                    tempo_changes.push((tick, us_per_q.as_int()));
472                }
473            }
474        }
475        tempo_changes.sort_by_key(|(tick, _)| *tick);
476        let mut normalized_tempos: Vec<(u64, u32)> = Vec::with_capacity(tempo_changes.len());
477        for (tick, tempo) in tempo_changes {
478            if let Some(last) = normalized_tempos.last_mut()
479                && last.0 == tick
480            {
481                last.1 = tempo;
482            } else {
483                normalized_tempos.push((tick, tempo));
484            }
485        }
486        let tempo_changes = normalized_tempos;
487
488        let ticks_to_samples = |tick: u64| -> usize {
489            let mut total_us: u128 = 0;
490            let mut prev_tick = 0_u64;
491            let mut current_tempo_us = 500_000_u32;
492            for (change_tick, tempo_us) in &tempo_changes {
493                if *change_tick > tick {
494                    break;
495                }
496                let seg_ticks = change_tick.saturating_sub(prev_tick);
497                total_us = total_us.saturating_add(
498                    u128::from(seg_ticks).saturating_mul(u128::from(current_tempo_us))
499                        / u128::from(ppq),
500                );
501                prev_tick = *change_tick;
502                current_tempo_us = *tempo_us;
503            }
504            let rem = tick.saturating_sub(prev_tick);
505            total_us = total_us.saturating_add(
506                u128::from(rem).saturating_mul(u128::from(current_tempo_us)) / u128::from(ppq),
507            );
508            ((total_us as f64 / 1_000_000.0) * sample_rate).round() as usize
509        };
510
511        let mut notes = Vec::<MidiNoteData>::new();
512        let mut controllers = Vec::<MidiControllerData>::new();
513        let mut passthrough_events = Vec::<(u64, Vec<u8>)>::new();
514        let mut active_notes: HashMap<(u8, u8), Vec<(u64, u8)>> = HashMap::new();
515
516        for track in &smf.tracks {
517            let mut tick = 0_u64;
518            for event in track {
519                tick = tick.saturating_add(event.delta.as_int() as u64);
520                match event.kind {
521                    TrackEventKind::Midi { channel, message } => {
522                        let channel_u8 = channel.as_int();
523                        match message {
524                            midly::MidiMessage::NoteOn { key, vel } => {
525                                let pitch = key.as_int();
526                                let velocity = vel.as_int();
527                                if velocity == 0 {
528                                    if let Some(starts) = active_notes.get_mut(&(channel_u8, pitch))
529                                        && let Some((start_tick, start_vel)) = starts.pop()
530                                    {
531                                        let start_sample = ticks_to_samples(start_tick);
532                                        let end_sample = ticks_to_samples(tick);
533                                        notes.push(MidiNoteData {
534                                            start_sample,
535                                            length_samples: end_sample
536                                                .saturating_sub(start_sample)
537                                                .max(1),
538                                            pitch,
539                                            velocity: start_vel,
540                                            channel: channel_u8,
541                                        });
542                                    }
543                                } else {
544                                    active_notes
545                                        .entry((channel_u8, pitch))
546                                        .or_default()
547                                        .push((tick, velocity));
548                                }
549                            }
550                            midly::MidiMessage::NoteOff { key, .. } => {
551                                let pitch = key.as_int();
552                                if let Some(starts) = active_notes.get_mut(&(channel_u8, pitch))
553                                    && let Some((start_tick, start_vel)) = starts.pop()
554                                {
555                                    let start_sample = ticks_to_samples(start_tick);
556                                    let end_sample = ticks_to_samples(tick);
557                                    notes.push(MidiNoteData {
558                                        start_sample,
559                                        length_samples: end_sample
560                                            .saturating_sub(start_sample)
561                                            .max(1),
562                                        pitch,
563                                        velocity: start_vel,
564                                        channel: channel_u8,
565                                    });
566                                }
567                            }
568                            midly::MidiMessage::Controller { controller, value } => {
569                                controllers.push(MidiControllerData {
570                                    sample: ticks_to_samples(tick),
571                                    controller: controller.as_int(),
572                                    value: value.as_int(),
573                                    channel: channel_u8,
574                                });
575                            }
576                            _ => {
577                                let mut data = Vec::with_capacity(3);
578                                if (LiveEvent::Midi { channel, message })
579                                    .write(&mut data)
580                                    .is_ok()
581                                {
582                                    passthrough_events.push((ticks_to_samples(tick) as u64, data));
583                                }
584                            }
585                        }
586                    }
587                    TrackEventKind::SysEx(payload) => {
588                        let mut data = Vec::with_capacity(payload.len() + 2);
589                        data.push(0xF0);
590                        data.extend_from_slice(payload);
591                        if data.last().copied() != Some(0xF7) {
592                            data.push(0xF7);
593                        }
594                        passthrough_events.push((ticks_to_samples(tick) as u64, data));
595                    }
596                    TrackEventKind::Escape(payload) => {
597                        let mut data = Vec::with_capacity(payload.len() + 1);
598                        data.push(0xF7);
599                        data.extend_from_slice(payload);
600                        passthrough_events.push((ticks_to_samples(tick) as u64, data));
601                    }
602                    _ => {}
603                }
604            }
605        }
606
607        for ((channel, pitch), starts) in active_notes {
608            for (start_tick, velocity) in starts {
609                let start_sample = ticks_to_samples(start_tick);
610                let end_sample = ticks_to_samples(start_tick.saturating_add(ppq / 8));
611                notes.push(MidiNoteData {
612                    start_sample,
613                    length_samples: end_sample.saturating_sub(start_sample).max(1),
614                    pitch,
615                    velocity,
616                    channel,
617                });
618            }
619        }
620
621        notes.sort_by_key(|n| (n.start_sample, n.pitch));
622        controllers.sort_by_key(|c| (c.sample, c.controller));
623        passthrough_events.sort_by_key(|(sample, _)| *sample);
624
625        let min_sample = notes
626            .iter()
627            .map(|n| n.start_sample)
628            .chain(controllers.iter().map(|c| c.sample))
629            .chain(passthrough_events.iter().map(|(s, _)| *s as usize))
630            .min()
631            .unwrap_or(0);
632        if min_sample >= clip_start && clip_start > 0 {
633            for note in &mut notes {
634                note.start_sample = note.start_sample.saturating_sub(clip_start);
635            }
636            for ctrl in &mut controllers {
637                ctrl.sample = ctrl.sample.saturating_sub(clip_start);
638            }
639            for (sample, _) in &mut passthrough_events {
640                *sample = sample.saturating_sub(clip_start as u64);
641            }
642        }
643
644        Ok((notes, controllers, passthrough_events))
645    }
646
647    fn midi_events_from_notes_and_controllers(
648        notes: &[MidiNoteData],
649        controllers: &[MidiControllerData],
650    ) -> Vec<(u64, Vec<u8>)> {
651        let mut events: Vec<(u64, u8, Vec<u8>)> = Vec::new();
652        for note in notes {
653            let channel = note.channel.min(15);
654            let pitch = note.pitch.min(127);
655            let velocity = note.velocity.min(127);
656            let start = note.start_sample as u64;
657            let end = note.start_sample.saturating_add(note.length_samples).max(1) as u64;
658            events.push((start, 2, vec![0x90 | channel, pitch, velocity]));
659            events.push((end, 0, vec![0x80 | channel, pitch, 64]));
660        }
661        for ctrl in controllers {
662            let channel = ctrl.channel.min(15);
663            let controller = ctrl.controller.min(127);
664            let value = ctrl.value.min(127);
665            events.push((
666                ctrl.sample as u64,
667                1,
668                vec![0xB0 | channel, controller, value],
669            ));
670        }
671        events.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
672        events
673            .into_iter()
674            .map(|(sample, _, data)| (sample, data))
675            .collect()
676    }
677
678    fn is_track_frozen(&self, track_name: &str) -> bool {
679        self.state
680            .lock()
681            .tracks
682            .get(track_name)
683            .map(|track| track.lock().frozen())
684            .unwrap_or(false)
685    }
686
687    async fn reject_if_track_frozen(&mut self, track_name: &str, operation: &str) -> bool {
688        if self.is_track_frozen(track_name) {
689            self.notify_clients(Err(format!(
690                "Track '{track_name}' is frozen; {operation} is blocked"
691            )))
692            .await;
693            true
694        } else {
695            false
696        }
697    }
698
699    fn apply_midi_edit_action(&mut self, action: &Action) -> Result<(), String> {
700        let (track_name, clip_index) = match action {
701            Action::ModifyMidiNotes {
702                track_name,
703                clip_index,
704                ..
705            }
706            | Action::InsertMidiNotes {
707                track_name,
708                clip_index,
709                ..
710            }
711            | Action::DeleteMidiNotes {
712                track_name,
713                clip_index,
714                ..
715            }
716            | Action::ModifyMidiControllers {
717                track_name,
718                clip_index,
719                ..
720            }
721            | Action::InsertMidiControllers {
722                track_name,
723                clip_index,
724                ..
725            }
726            | Action::DeleteMidiControllers {
727                track_name,
728                clip_index,
729                ..
730            }
731            | Action::SetMidiSysExEvents {
732                track_name,
733                clip_index,
734                ..
735            } => (track_name, *clip_index),
736            _ => return Ok(()),
737        };
738
739        let track_handle = self
740            .state
741            .lock()
742            .tracks
743            .get(track_name)
744            .cloned()
745            .ok_or_else(|| format!("Track not found: {track_name}"))?;
746        let (clip_name, clip_path, sample_rate, clip_start) = {
747            let track = track_handle.lock();
748            if clip_index >= track.midi.clips.len() {
749                return Err(format!(
750                    "Invalid MIDI clip index {clip_index} for '{track_name}'"
751                ));
752            }
753            let clip = &track.midi.clips[clip_index];
754            let clip_name = clip.name.clone();
755            let clip_path = track.resolve_clip_path(&clip_name);
756            (clip_name, clip_path, track.sample_rate, clip.start)
757        };
758
759        let (mut notes, mut controllers, mut passthrough_events) =
760            Self::parse_midi_clip_for_edit(&clip_path, sample_rate, clip_start)?;
761
762        match action {
763            Action::ModifyMidiNotes {
764                note_indices,
765                new_notes,
766                ..
767            } => {
768                for (idx, new_note) in note_indices.iter().zip(new_notes.iter()) {
769                    if let Some(note) = notes.get_mut(*idx) {
770                        *note = new_note.clone();
771                    }
772                }
773            }
774            Action::DeleteMidiNotes { note_indices, .. } => {
775                let mut indices = note_indices.clone();
776                indices.sort_unstable();
777                indices.dedup();
778                for idx in indices.into_iter().rev() {
779                    if idx < notes.len() {
780                        notes.remove(idx);
781                    }
782                }
783            }
784            Action::InsertMidiNotes {
785                notes: inserted, ..
786            } => {
787                let mut sorted = inserted.clone();
788                sorted.sort_unstable_by_key(|(idx, _)| *idx);
789                for (idx, note) in sorted {
790                    let at = idx.min(notes.len());
791                    notes.insert(at, note);
792                }
793            }
794            Action::ModifyMidiControllers {
795                controller_indices,
796                new_controllers,
797                ..
798            } => {
799                for (idx, new_ctrl) in controller_indices.iter().zip(new_controllers.iter()) {
800                    if let Some(ctrl) = controllers.get_mut(*idx) {
801                        *ctrl = new_ctrl.clone();
802                    }
803                }
804            }
805            Action::DeleteMidiControllers {
806                controller_indices, ..
807            } => {
808                let mut indices = controller_indices.clone();
809                indices.sort_unstable();
810                indices.dedup();
811                for idx in indices.into_iter().rev() {
812                    if idx < controllers.len() {
813                        controllers.remove(idx);
814                    }
815                }
816            }
817            Action::InsertMidiControllers {
818                controllers: inserted,
819                ..
820            } => {
821                let mut sorted = inserted.clone();
822                sorted.sort_unstable_by_key(|(idx, _)| *idx);
823                for (idx, ctrl) in sorted {
824                    let at = idx.min(controllers.len());
825                    controllers.insert(at, ctrl);
826                }
827            }
828            Action::SetMidiSysExEvents {
829                new_sysex_events, ..
830            } => {
831                passthrough_events
832                    .retain(|(_, data)| !matches!(data.first(), Some(0xF0) | Some(0xF7)));
833                passthrough_events.extend(
834                    new_sysex_events
835                        .iter()
836                        .map(|ev| (ev.sample as u64, ev.data.clone())),
837                );
838            }
839            _ => {}
840        }
841
842        notes.sort_by_key(|n| (n.start_sample, n.pitch));
843        controllers.sort_by_key(|c| (c.sample, c.controller));
844        passthrough_events.sort_by_key(|(sample, _)| *sample);
845        let mut events = Self::midi_events_from_notes_and_controllers(&notes, &controllers);
846        events.extend(passthrough_events);
847        events.sort_by_key(|(sample, _)| *sample);
848        Self::write_midi_file(&clip_path, sample_rate.max(1.0) as u32, &events)?;
849        track_handle.lock().invalidate_midi_clip_cache(&clip_name);
850        Ok(())
851    }
852
853    const METER_PUBLISH_INTERVAL: Duration = Duration::from_millis(50);
854    const TRACK_PROCESS_TIMEOUT: Duration = Duration::from_millis(250);
855    #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
856    const HW_OUT_METER_LINEAR_EPSILON: f32 = 0.0025;
857
858    #[cfg(all(unix, not(target_os = "macos")))]
859    fn session_plugins_dir(&self) -> Option<PathBuf> {
860        self.session_dir.as_ref().map(|d| d.join("plugins"))
861    }
862
863    fn session_audio_dir(&self) -> Option<PathBuf> {
864        self.session_dir.as_ref().map(|d| d.join("audio"))
865    }
866
867    fn session_midi_dir(&self) -> Option<PathBuf> {
868        self.session_dir.as_ref().map(|d| d.join("midi"))
869    }
870
871    fn ensure_session_subdirs(&self) {
872        if let Some(root) = &self.session_dir {
873            let _ = std::fs::create_dir_all(root.join("plugins"));
874            let _ = std::fs::create_dir_all(root.join("audio"));
875            let _ = std::fs::create_dir_all(root.join("midi"));
876        }
877    }
878
879    fn finalize_midi_hw_devices(mut devices: Vec<String>) -> Vec<String> {
880        devices.sort();
881        devices.dedup();
882        devices
883    }
884
885    #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"))]
886    fn discover_midi_hw_devices_from_dir(path: &str, prefixes: &[&str]) -> Vec<String> {
887        let devices = read_dir(path)
888            .map(|rd| {
889                rd.filter_map(Result::ok)
890                    .map(|e| e.path())
891                    .filter_map(|path| {
892                        let name = path.file_name()?.to_str()?;
893                        prefixes
894                            .iter()
895                            .any(|prefix| name.starts_with(prefix))
896                            .then(|| path.to_string_lossy().into_owned())
897                    })
898                    .collect()
899            })
900            .unwrap_or_default();
901        Self::finalize_midi_hw_devices(devices)
902    }
903
904    fn discover_midi_hw_devices() -> Vec<String> {
905        #[cfg(target_os = "freebsd")]
906        let devices = Self::discover_midi_hw_devices_from_dir("/dev", &["umidi", "midi"]);
907        #[cfg(target_os = "linux")]
908        let devices = Self::discover_midi_hw_devices_from_dir("/dev/snd", &["midiC"]);
909        #[cfg(target_os = "openbsd")]
910        let devices = Self::discover_midi_hw_devices_from_dir("/dev", &["midi"]);
911        #[cfg(target_os = "windows")]
912        let devices = {
913            let mut devices = wasapi::list_midi_input_devices();
914            devices.extend(wasapi::list_midi_output_devices());
915            Self::finalize_midi_hw_devices(devices)
916        };
917        #[cfg(target_os = "macos")]
918        let devices = {
919            let mut devices = Vec::new();
920            for source in coremidi::Sources {
921                if let Some(name) = source.display_name() {
922                    devices.push(name);
923                }
924            }
925            for dest in coremidi::Destinations {
926                if let Some(name) = dest.display_name() {
927                    devices.push(name);
928                }
929            }
930            Self::finalize_midi_hw_devices(devices)
931        };
932        devices
933    }
934
935    pub fn new(rx: Receiver<Message>, tx: Sender<Message>) -> Self {
936        Self {
937            rx,
938            tx,
939            clients: vec![],
940            state: Arc::new(UnsafeMutex::new(State::default())),
941            workers: vec![],
942            hw_driver: None,
943            #[cfg(unix)]
944            jack_runtime: None,
945            midi_hub: Arc::new(UnsafeMutex::new(MidiHub::default())),
946            hw_worker: None,
947            osc_server: None,
948            pending_hw_midi_events: vec![],
949            pending_hw_midi_events_by_device: HashMap::new(),
950            pending_hw_midi_out_events: vec![],
951            pending_hw_midi_out_events_by_device: vec![],
952            active_hw_notes_by_track: HashMap::new(),
953            active_hw_notes_cycle_start: HashMap::new(),
954            midi_hw_in_routes: vec![],
955            midi_hw_out_routes: vec![],
956            midi_hw_thru_routes: vec![],
957            worker_classes: vec![],
958            ready_realtime_workers: vec![],
959            ready_refill_workers: vec![],
960            pending_requests: VecDeque::new(),
961            awaiting_hwfinished: false,
962            handling_hwfinished: false,
963            track_process_epoch: 0,
964            transport_panic_flush_pending: false,
965            transport_restart_pending: false,
966            transport_sample: 0,
967            loop_enabled: false,
968            loop_range_samples: None,
969            metronome_enabled: false,
970            tempo_bpm: 120.0,
971            tsig_num: 4,
972            tsig_denom: 4,
973            punch_enabled: false,
974            punch_range_samples: None,
975            audio_recordings: std::collections::HashMap::new(),
976            midi_recordings: std::collections::HashMap::new(),
977            completed_audio_recordings: Vec::new(),
978            completed_midi_recordings: Vec::new(),
979            playing: false,
980            clip_playback_enabled: true,
981            record_enabled: false,
982            session_dir: None,
983            hw_out_level_db: 0.0,
984            hw_out_balance: 0.0,
985            hw_out_muted: false,
986            last_hw_out_meter_publish: None,
987            #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
988            last_hw_out_meter_linear: vec![],
989            hw_out_peak_hold_linear: vec![],
990            #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
991            hw_out_meter_publish_phase: false,
992            last_track_meter_publish: None,
993            track_meter_linear_by_track: HashMap::new(),
994            track_processing_started_at: HashMap::new(),
995            latest_hw_out_meter_db: Arc::new(Vec::new()),
996            latest_track_meter_snapshot: Arc::new(Vec::new()),
997            history: History::default(),
998            history_group: None,
999            history_suspended: false,
1000            offline_bounce_jobs: HashMap::new(),
1001            pending_midi_learn: None,
1002            pending_global_midi_learn: None,
1003            global_midi_learn_play_pause: None,
1004            global_midi_learn_stop: None,
1005            global_midi_learn_record_toggle: None,
1006            midi_cc_gate: HashMap::new(),
1007            hybrid_low_watermark_frames: 0,
1008            hybrid_realtime_frames: 0,
1009            hybrid_playback_frames: 0,
1010            hybrid_enabled: false,
1011            refill_budget_per_pass: 2,
1012            realtime_fallback_enabled: true,
1013            realtime_fallback_budget_per_pass: 1,
1014            refill_budget_throttle_count: 0,
1015            realtime_fallback_dispatch_count: 0,
1016        }
1017    }
1018
1019    fn hw_driver_cycle_samples(&self) -> Option<usize> {
1020        self.hw_driver.as_ref().map(|o| o.lock().cycle_samples())
1021    }
1022
1023    #[cfg(unix)]
1024    fn jack_cycle_samples(&self) -> Option<usize> {
1025        self.jack_runtime.as_ref().map(|j| j.lock().buffer_size)
1026    }
1027
1028    #[cfg(not(unix))]
1029    fn jack_cycle_samples(&self) -> Option<usize> {
1030        None
1031    }
1032
1033    fn current_cycle_samples(&self) -> usize {
1034        self.hw_driver_cycle_samples()
1035            .or_else(|| self.jack_cycle_samples())
1036            .unwrap_or(0)
1037    }
1038
1039    fn session_end_sample(&self) -> usize {
1040        self.state
1041            .lock()
1042            .tracks
1043            .values()
1044            .map(|track| {
1045                let track = track.lock();
1046                let audio_end = track
1047                    .audio
1048                    .clips
1049                    .iter()
1050                    .map(|clip| clip.end)
1051                    .max()
1052                    .unwrap_or(0);
1053                let midi_end = track
1054                    .midi
1055                    .clips
1056                    .iter()
1057                    .map(|clip| clip.end)
1058                    .max()
1059                    .unwrap_or(0);
1060                audio_end.max(midi_end)
1061            })
1062            .max()
1063            .unwrap_or(0)
1064    }
1065
1066    async fn ensure_metronome_track(&mut self) {
1067        if self.state.lock().tracks.contains_key(Self::METRONOME_TRACK) {
1068            return;
1069        }
1070        let (cycle_samples, sample_rate_hz, output_channels): (usize, f64, usize) =
1071            if let Some(hw) = &self.hw_driver {
1072                let hw = hw.lock();
1073                (
1074                    hw.cycle_samples(),
1075                    hw.sample_rate() as f64,
1076                    hw.output_channels(),
1077                )
1078            } else {
1079                #[cfg(unix)]
1080                {
1081                    if let Some(jack) = &self.jack_runtime {
1082                        let jack = jack.lock();
1083                        (
1084                            jack.buffer_size,
1085                            jack.sample_rate as f64,
1086                            jack.audio_outs().len(),
1087                        )
1088                    } else {
1089                        return;
1090                    }
1091                }
1092                #[cfg(not(unix))]
1093                {
1094                    return;
1095                }
1096            };
1097        if output_channels == 0 {
1098            return;
1099        }
1100        self.state.lock().tracks.insert(
1101            Self::METRONOME_TRACK.to_string(),
1102            Arc::new(UnsafeMutex::new(Box::new(Track::new(
1103                Self::METRONOME_TRACK.to_string(),
1104                0,
1105                1,
1106                0,
1107                0,
1108                cycle_samples.max(1),
1109                sample_rate_hz.max(1.0),
1110            )))),
1111        );
1112        if let Some(track) = self.state.lock().tracks.get(Self::METRONOME_TRACK).cloned() {
1113            track.lock().set_level(Self::METRONOME_DEFAULT_LEVEL_DB);
1114            track.lock().set_metronome_enabled(self.metronome_enabled);
1115        }
1116        self.notify_clients(Ok(Action::AddTrack {
1117            name: Self::METRONOME_TRACK.to_string(),
1118            audio_ins: 0,
1119            midi_ins: 0,
1120            audio_outs: 1,
1121            midi_outs: 0,
1122        }))
1123        .await;
1124        self.notify_clients(Ok(Action::TrackLevel(
1125            Self::METRONOME_TRACK.to_string(),
1126            Self::METRONOME_DEFAULT_LEVEL_DB,
1127        )))
1128        .await;
1129    }
1130
1131    fn open_hw_driver(
1132        device: &str,
1133        _input_device: Option<&str>,
1134        sample_rate_hz: i32,
1135        bits: i32,
1136        hw_opts: HwOptions,
1137    ) -> Result<HwDriver, String> {
1138        #[cfg(any(target_os = "windows", target_os = "freebsd", target_os = "linux"))]
1139        {
1140            HwDriver::new_with_options(device, _input_device, sample_rate_hz, bits, hw_opts)
1141                .map_err(|e| e.to_string())
1142        }
1143        #[cfg(target_os = "openbsd")]
1144        {
1145            HwDriver::new_with_options(device, sample_rate_hz, bits, hw_opts)
1146                .map_err(|e| e.to_string())
1147        }
1148    }
1149
1150    fn hw_profile_backend_label(_device: &str) -> &'static str {
1151        #[cfg(target_os = "windows")]
1152        let label = "WASAPI";
1153        #[cfg(target_os = "linux")]
1154        let label = "ALSA";
1155        #[cfg(target_os = "freebsd")]
1156        let label = "OSS";
1157        #[cfg(target_os = "openbsd")]
1158        let label = "sndio";
1159        #[cfg(target_os = "macos")]
1160        let label = "CoreAudio";
1161        label
1162    }
1163
1164    #[cfg(target_os = "freebsd")]
1165    fn maybe_start_freebsd_sync_group(&self) {
1166        if let Some(oss) = &self.hw_driver {
1167            let in_fd = oss.lock().input_fd();
1168            let out_fd = oss.lock().output_fd();
1169            let mut group = 0;
1170            let in_group = hw::add_to_sync_group(in_fd, group, true);
1171            if in_group > 0 {
1172                group = in_group;
1173            }
1174            let out_group = hw::add_to_sync_group(out_fd, group, false);
1175            if out_group > 0 {
1176                group = out_group;
1177            }
1178            let sync_started = if group > 0 {
1179                hw::start_sync_group(in_fd, group).is_ok()
1180            } else {
1181                false
1182            };
1183            if !sync_started {
1184                let _ = oss.lock().start_input_trigger();
1185                let _ = oss.lock().start_output_trigger();
1186            }
1187        }
1188    }
1189
1190    #[cfg(not(target_os = "freebsd"))]
1191    fn maybe_start_freebsd_sync_group(&self) {}
1192
1193    async fn open_discovered_midi_hw_devices(&mut self) {
1194        for device in Self::discover_midi_hw_devices() {
1195            let (opened_in, opened_out) = {
1196                let midi_hub = self.midi_hub.lock();
1197                let opened_in = midi_hub.open_input(&device).is_ok();
1198                let opened_out = midi_hub.open_output(&device).is_ok();
1199                (opened_in, opened_out)
1200            };
1201
1202            if opened_in {
1203                self.notify_clients(Ok(Action::OpenMidiInputDevice(device.clone())))
1204                    .await;
1205            }
1206            if opened_out {
1207                self.notify_clients(Ok(Action::OpenMidiOutputDevice(device.clone())))
1208                    .await;
1209            }
1210        }
1211    }
1212
1213    #[cfg(unix)]
1214    async fn maybe_open_jack_runtime(&mut self, request: AudioOpenRequest<'_>) -> Option<()> {
1215        if !request.device.eq_ignore_ascii_case("jack") {
1216            return None;
1217        }
1218        match JackRuntime::new(
1219            "maolan",
1220            crate::hw::jack::Config::default(),
1221            self.tx.clone(),
1222        ) {
1223            Ok(runtime) => {
1224                let input_channels = runtime.input_channels();
1225                let output_channels = runtime.output_channels();
1226                self.hybrid_playback_frames = request.period_frames.max(1);
1227                self.hybrid_realtime_frames = request.realtime_frames.max(1);
1228                self.hybrid_low_watermark_frames = request.low_watermark_frames.max(1);
1229                self.hybrid_enabled = request.hybrid_enabled;
1230                let midi_inputs = runtime.midi_input_devices();
1231                let midi_outputs = runtime.midi_output_devices();
1232                let rate = runtime.sample_rate;
1233                self.hw_driver = None;
1234                if let Some(worker) = self.hw_worker.take() {
1235                    let _ = worker.tx.send(Message::Request(Action::Quit)).await;
1236                    let _ = worker.handle.await;
1237                }
1238                self.jack_runtime = Some(Arc::new(UnsafeMutex::new(runtime)));
1239                self.publish_hw_infos(input_channels, output_channels, rate)
1240                    .await;
1241                for device in midi_inputs {
1242                    self.notify_clients(Ok(Action::OpenMidiInputDevice(device)))
1243                        .await;
1244                }
1245                for device in midi_outputs {
1246                    self.notify_clients(Ok(Action::OpenMidiOutputDevice(device)))
1247                        .await;
1248                }
1249                self.notify_clients(Ok(Action::OpenAudioDevice {
1250                    device: request.device.to_string(),
1251                    input_device: request.input_device.map(ToOwned::to_owned),
1252                    sample_rate_hz: request.sample_rate_hz,
1253                    bits: request.bits,
1254                    exclusive: request.exclusive,
1255                    period_frames: request.period_frames,
1256                    realtime_frames: request.realtime_frames,
1257                    low_watermark_frames: request.low_watermark_frames,
1258                    nperiods: request.nperiods,
1259                    sync_mode: request.sync_mode,
1260                    hybrid_enabled: request.hybrid_enabled,
1261                }))
1262                .await;
1263                self.awaiting_hwfinished = true;
1264            }
1265            Err(e) => {
1266                error!("Failed to open JACK runtime: {e}");
1267                self.notify_clients(Err(e)).await;
1268            }
1269        }
1270        Some(())
1271    }
1272
1273    fn hw_driver_input_audio_port(&self, from_port: usize) -> Option<Arc<AudioIO>> {
1274        self.hw_driver
1275            .as_ref()
1276            .and_then(|h| h.lock().input_port(from_port))
1277    }
1278
1279    fn hw_driver_output_audio_port(&self, to_port: usize) -> Option<Arc<AudioIO>> {
1280        self.hw_driver
1281            .as_ref()
1282            .and_then(|h| h.lock().output_port(to_port))
1283    }
1284
1285    #[cfg(unix)]
1286    fn jack_input_audio_port(&self, from_port: usize) -> Option<Arc<AudioIO>> {
1287        self.jack_runtime
1288            .as_ref()
1289            .and_then(|j| j.lock().input_audio_port(from_port))
1290    }
1291
1292    #[cfg(not(unix))]
1293    fn jack_input_audio_port(&self, _from_port: usize) -> Option<Arc<AudioIO>> {
1294        None
1295    }
1296
1297    #[cfg(unix)]
1298    fn jack_output_audio_port(&self, to_port: usize) -> Option<Arc<AudioIO>> {
1299        self.jack_runtime
1300            .as_ref()
1301            .and_then(|j| j.lock().output_audio_port(to_port))
1302    }
1303
1304    #[cfg(not(unix))]
1305    fn jack_output_audio_port(&self, _to_port: usize) -> Option<Arc<AudioIO>> {
1306        None
1307    }
1308
1309    fn normalize_transport_sample(&self, sample: usize) -> usize {
1310        if self.loop_enabled
1311            && let Some((loop_start, loop_end)) = self.loop_range_samples
1312            && loop_end > loop_start
1313            && sample >= loop_end
1314        {
1315            let loop_len = loop_end - loop_start;
1316            return loop_start + (sample - loop_start) % loop_len;
1317        }
1318        sample
1319    }
1320
1321    #[cfg(unix)]
1322    fn jack_transport_sync_decision(
1323        current_playing: bool,
1324        current_sample: usize,
1325        jack_playing: bool,
1326        normalized_frame: usize,
1327        cycle_samples: usize,
1328    ) -> JackTransportSyncDecision {
1329        let play_sync = match (current_playing, jack_playing) {
1330            (false, true) => Some(JackTransportPlaySync::Start),
1331            (true, false) => Some(JackTransportPlaySync::Stop),
1332            _ => None,
1333        };
1334        let position_drift = normalized_frame.abs_diff(current_sample);
1335        let position_changed = normalized_frame != current_sample;
1336        let should_sync_position = position_changed
1337            && (!jack_playing || play_sync.is_some() || position_drift > cycle_samples.max(1));
1338
1339        JackTransportSyncDecision {
1340            play_sync,
1341            position_sync: should_sync_position.then_some(normalized_frame),
1342        }
1343    }
1344
1345    #[cfg(unix)]
1346    async fn sync_from_jack_transport(&mut self) {
1347        let Some(jack) = self.jack_runtime.clone() else {
1348            return;
1349        };
1350        let Ok((jack_state, jack_frame)) = jack.lock().transport_state_and_frame() else {
1351            return;
1352        };
1353
1354        let jack_playing = matches!(
1355            jack_state,
1356            jack::TransportState::Rolling | jack::TransportState::Starting
1357        );
1358        let normalized_frame = self.normalize_transport_sample(jack_frame);
1359        let decision = Self::jack_transport_sync_decision(
1360            self.playing,
1361            self.transport_sample,
1362            jack_playing,
1363            normalized_frame,
1364            self.current_cycle_samples(),
1365        );
1366
1367        if let Some(play_sync) = decision.play_sync {
1368            self.playing = matches!(play_sync, JackTransportPlaySync::Start);
1369            if matches!(play_sync, JackTransportPlaySync::Start) {
1370                self.transport_restart_pending = false;
1371                self.transport_panic_flush_pending = false;
1372                self.invalidate_track_cycle_state();
1373                self.notify_clients(Ok(Action::Play)).await;
1374            } else {
1375                self.transport_panic_flush_pending = false;
1376                self.transport_restart_pending = false;
1377                let panic_events = self.note_off_events_for_all_active_tracks();
1378                self.pending_hw_midi_out_events_by_device
1379                    .extend(panic_events);
1380                self.flush_recordings().await;
1381                self.notify_clients(Ok(Action::Stop)).await;
1382            }
1383        }
1384
1385        if let Some(sample) = decision.position_sync {
1386            self.transport_sample = sample;
1387            self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
1388                .await;
1389        }
1390    }
1391
1392    fn cycle_segments(&self, frames: usize) -> Vec<(usize, usize, usize)> {
1393        if frames == 0 {
1394            return vec![];
1395        }
1396        if !self.loop_enabled {
1397            return vec![(
1398                self.transport_sample,
1399                self.transport_sample.saturating_add(frames),
1400                0,
1401            )];
1402        }
1403        let Some((loop_start, loop_end)) = self.loop_range_samples else {
1404            return vec![(
1405                self.transport_sample,
1406                self.transport_sample.saturating_add(frames),
1407                0,
1408            )];
1409        };
1410        if loop_end <= loop_start {
1411            return vec![(
1412                self.transport_sample,
1413                self.transport_sample.saturating_add(frames),
1414                0,
1415            )];
1416        }
1417        let mut segments = Vec::new();
1418        let mut remaining = frames;
1419        let mut out_offset = 0usize;
1420        let mut current = self.transport_sample;
1421        while remaining > 0 {
1422            let take = loop_end.saturating_sub(current).min(remaining);
1423            if take == 0 {
1424                current = loop_start;
1425                continue;
1426            }
1427            segments.push((current, current.saturating_add(take), out_offset));
1428            out_offset = out_offset.saturating_add(take);
1429            remaining -= take;
1430            current = if remaining > 0 {
1431                loop_start
1432            } else {
1433                current.saturating_add(take)
1434            };
1435        }
1436        segments
1437    }
1438
1439    fn recording_segments_for_cycle(&self, frames: usize) -> Vec<(usize, usize, usize)> {
1440        let segments = self.cycle_segments(frames);
1441        if !self.punch_enabled {
1442            return segments;
1443        }
1444        let Some((punch_start, punch_end)) = self.punch_range_samples else {
1445            return vec![];
1446        };
1447        if punch_end <= punch_start {
1448            return vec![];
1449        }
1450        let mut clipped = Vec::new();
1451        for (segment_start, segment_end, frame_offset) in segments {
1452            let start = segment_start.max(punch_start);
1453            let end = segment_end.min(punch_end);
1454            if end <= start {
1455                continue;
1456            }
1457            let clipped_offset = frame_offset.saturating_add(start.saturating_sub(segment_start));
1458            clipped.push((start, end, clipped_offset));
1459        }
1460        clipped
1461    }
1462
1463    fn hw_device_info<D: HwDevice>(d: &D) -> HwDeviceInfo {
1464        (
1465            d.input_channels(),
1466            d.output_channels(),
1467            d.sample_rate() as usize,
1468            d.latency_ranges(),
1469        )
1470    }
1471
1472    async fn publish_hw_infos(
1473        &mut self,
1474        input_channels: usize,
1475        output_channels: usize,
1476        rate: usize,
1477    ) {
1478        self.notify_clients(Ok(Action::HWInfo {
1479            channels: input_channels,
1480            rate,
1481            input: true,
1482        }))
1483        .await;
1484        self.notify_clients(Ok(Action::HWInfo {
1485            channels: output_channels,
1486            rate,
1487            input: false,
1488        }))
1489        .await;
1490    }
1491
1492    #[cfg(unix)]
1493    fn jack_runtime_is_some(&self) -> bool {
1494        self.jack_runtime.is_some()
1495    }
1496
1497    #[cfg(not(unix))]
1498    fn jack_runtime_is_some(&self) -> bool {
1499        false
1500    }
1501
1502    fn can_schedule_hw_cycle(&self) -> bool {
1503        self.hw_worker.is_some() || self.jack_runtime_is_some()
1504    }
1505
1506    async fn ensure_hw_worker_running(&mut self) {
1507        if self.hw_worker.is_some() || self.hw_driver.is_none() {
1508            return;
1509        }
1510        let (tx, rx) = channel::<Message>(32);
1511        let hw = self.hw_driver.clone().unwrap();
1512        let midi_hub = self.midi_hub.clone();
1513        let tx_engine = self.tx.clone();
1514        let handler = tokio::spawn(async move {
1515            let worker = HwWorker::new(hw, midi_hub, rx, tx_engine);
1516            worker.work().await;
1517        });
1518        self.hw_worker = Some(WorkerData::new(tx, handler));
1519    }
1520
1521    fn build_hw_options(
1522        exclusive: bool,
1523        period_frames: usize,
1524        nperiods: usize,
1525        sync_mode: bool,
1526    ) -> HwOptions {
1527        HwOptions {
1528            exclusive,
1529            period_frames: period_frames.max(1).next_power_of_two(),
1530            nperiods: nperiods.max(1),
1531            sync_mode,
1532            ..Default::default()
1533        }
1534    }
1535
1536    async fn open_non_jack_audio_device(
1537        &mut self,
1538        device: &str,
1539        input_device: Option<&str>,
1540        sample_rate_hz: i32,
1541        bits: i32,
1542        hw_opts: HwOptions,
1543    ) -> Result<(), String> {
1544        let hw_profile_enabled = config::env_flag(config::HW_PROFILE_ENV);
1545        let d = Self::open_hw_driver(device, input_device, sample_rate_hz, bits, hw_opts)?;
1546        let (in_channels, out_channels, rate, (in_lat, out_lat)) = Self::hw_device_info(&d);
1547        if hw_profile_enabled {
1548            let label = Self::hw_profile_backend_label(device);
1549            error!(
1550                "{} config: exclusive={}, period={}, nperiods={}, ignore_hwbuf={}, sync_mode={}, in_latency_extra={}, out_latency_extra={}, input_range={:?}, output_range={:?}",
1551                label,
1552                hw_opts.exclusive,
1553                hw_opts.period_frames,
1554                hw_opts.nperiods,
1555                hw_opts.ignore_hwbuf,
1556                hw_opts.sync_mode,
1557                hw_opts.input_latency_frames,
1558                hw_opts.output_latency_frames,
1559                in_lat,
1560                out_lat
1561            );
1562        }
1563        #[cfg(unix)]
1564        {
1565            self.jack_runtime = None;
1566        }
1567        self.hw_driver = Some(Arc::new(UnsafeMutex::new(d)));
1568        self.publish_hw_infos(in_channels, out_channels, rate).await;
1569        Ok(())
1570    }
1571
1572    async fn finalize_open_audio_device(&mut self) {
1573        self.maybe_start_freebsd_sync_group();
1574        if self.metronome_enabled {
1575            self.ensure_metronome_track().await;
1576        }
1577        if self.hw_worker.is_none() && self.hw_driver.is_some() {
1578            self.ensure_hw_worker_running().await;
1579            self.request_hw_cycle().await;
1580        }
1581        self.open_discovered_midi_hw_devices().await;
1582    }
1583
1584    fn hw_input_audio_port(&self, from_port: usize) -> Option<Arc<AudioIO>> {
1585        self.hw_driver_input_audio_port(from_port)
1586            .or_else(|| self.jack_input_audio_port(from_port))
1587    }
1588
1589    fn hw_output_audio_port(&self, to_port: usize) -> Option<Arc<AudioIO>> {
1590        self.hw_driver_output_audio_port(to_port)
1591            .or_else(|| self.jack_output_audio_port(to_port))
1592    }
1593
1594    fn all_hw_output_audio_ports(&self) -> Vec<Arc<AudioIO>> {
1595        if let Some(driver) = &self.hw_driver {
1596            let count = driver.lock().output_channels();
1597            return (0..count)
1598                .filter_map(|idx| self.hw_driver_output_audio_port(idx))
1599                .collect();
1600        }
1601        #[cfg(unix)]
1602        if let Some(jack) = &self.jack_runtime {
1603            return jack.lock().audio_outs();
1604        }
1605        Vec::new()
1606    }
1607
1608    #[cfg(unix)]
1609    fn audio_ports_connected(source: &Arc<AudioIO>, target: &Arc<AudioIO>) -> bool {
1610        source
1611            .connections
1612            .lock()
1613            .iter()
1614            .any(|conn| Arc::ptr_eq(conn, target))
1615    }
1616
1617    fn resolve_audio_route_ports(
1618        &self,
1619        from_track: &str,
1620        from_port: usize,
1621        to_track: &str,
1622        to_port: usize,
1623    ) -> (Option<Arc<AudioIO>>, Option<Arc<AudioIO>>) {
1624        let from_audio_io = if from_track == "hw:in" {
1625            self.hw_input_audio_port(from_port)
1626        } else {
1627            let state = self.state.lock();
1628            state
1629                .tracks
1630                .get(from_track)
1631                .and_then(|t| t.lock().audio.outs.get(from_port).cloned())
1632        };
1633        let to_audio_io = if to_track == "hw:out" {
1634            self.hw_output_audio_port(to_port)
1635        } else {
1636            let state = self.state.lock();
1637            state
1638                .tracks
1639                .get(to_track)
1640                .and_then(|t| t.lock().audio.ins.get(to_port).cloned())
1641        };
1642        (from_audio_io, to_audio_io)
1643    }
1644
1645    async fn disconnect_audio_route_and_notify(&mut self, action: Action) -> Result<(), String> {
1646        let Action::Disconnect {
1647            from_track,
1648            from_port,
1649            to_track,
1650            to_port,
1651            kind,
1652        } = &action
1653        else {
1654            return Err("disconnect_audio_route_and_notify requires Disconnect action".to_string());
1655        };
1656        if *kind != Kind::Audio {
1657            return Err("disconnect_audio_route_and_notify only supports audio routes".to_string());
1658        }
1659        let (from_audio_io, to_audio_io) =
1660            self.resolve_audio_route_ports(from_track, *from_port, to_track, *to_port);
1661        match (from_audio_io, to_audio_io) {
1662            (Some(source), Some(target)) => {
1663                crate::audio::io::AudioIO::disconnect(&source, &target)
1664                    .map_err(|e| format!("Disconnect failed: {e}"))?;
1665                self.notify_clients(Ok(action)).await;
1666                Ok(())
1667            }
1668            _ => Err(format!(
1669                "Disconnect failed: Port not found ({} -> {})",
1670                from_track, to_track
1671            )),
1672        }
1673    }
1674
1675    #[cfg(unix)]
1676    fn disconnect_actions_for_removed_hw_input(
1677        &self,
1678        removed_port: usize,
1679        removed_io: &Arc<AudioIO>,
1680    ) -> Vec<Action> {
1681        let mut actions = Vec::new();
1682        {
1683            let state = self.state.lock();
1684            for (track_name, track) in &state.tracks {
1685                let track = track.lock();
1686                for (to_port, target) in track.audio.ins.iter().enumerate() {
1687                    if Self::audio_ports_connected(removed_io, target) {
1688                        actions.push(Action::Disconnect {
1689                            from_track: "hw:in".to_string(),
1690                            from_port: removed_port,
1691                            to_track: track_name.clone(),
1692                            to_port,
1693                            kind: Kind::Audio,
1694                        });
1695                    }
1696                }
1697            }
1698        }
1699        for (to_port, target) in self.all_hw_output_audio_ports().into_iter().enumerate() {
1700            if Self::audio_ports_connected(removed_io, &target) {
1701                actions.push(Action::Disconnect {
1702                    from_track: "hw:in".to_string(),
1703                    from_port: removed_port,
1704                    to_track: "hw:out".to_string(),
1705                    to_port,
1706                    kind: Kind::Audio,
1707                });
1708            }
1709        }
1710        actions
1711    }
1712
1713    #[cfg(unix)]
1714    fn disconnect_actions_for_removed_hw_output(
1715        &self,
1716        removed_port: usize,
1717        removed_io: &Arc<AudioIO>,
1718    ) -> Vec<Action> {
1719        let mut actions = Vec::new();
1720        {
1721            let state = self.state.lock();
1722            for (track_name, track) in &state.tracks {
1723                let track = track.lock();
1724                for (from_port, source) in track.audio.outs.iter().enumerate() {
1725                    if Self::audio_ports_connected(source, removed_io) {
1726                        actions.push(Action::Disconnect {
1727                            from_track: track_name.clone(),
1728                            from_port,
1729                            to_track: "hw:out".to_string(),
1730                            to_port: removed_port,
1731                            kind: Kind::Audio,
1732                        });
1733                    }
1734                }
1735            }
1736        }
1737        #[cfg(unix)]
1738        if let Some(jack) = &self.jack_runtime {
1739            for (from_port, source) in jack.lock().audio_ins().into_iter().enumerate() {
1740                if Self::audio_ports_connected(&source, removed_io) {
1741                    actions.push(Action::Disconnect {
1742                        from_track: "hw:in".to_string(),
1743                        from_port,
1744                        to_track: "hw:out".to_string(),
1745                        to_port: removed_port,
1746                        kind: Kind::Audio,
1747                    });
1748                }
1749            }
1750        }
1751        actions
1752    }
1753
1754    #[cfg(unix)]
1755    fn reindex_notifications_for_removed_hw_input(&self, removed_port: usize) -> Vec<Action> {
1756        let mut actions = Vec::new();
1757        #[cfg(unix)]
1758        if let Some(jack) = &self.jack_runtime {
1759            let jack = jack.lock();
1760            for from_port in (removed_port + 1)..jack.input_channels() {
1761                let Some(source) = jack.input_audio_port(from_port) else {
1762                    continue;
1763                };
1764                {
1765                    let state = self.state.lock();
1766                    for (track_name, track) in &state.tracks {
1767                        let track = track.lock();
1768                        for (to_port, target) in track.audio.ins.iter().enumerate() {
1769                            if Self::audio_ports_connected(&source, target) {
1770                                actions.push(Action::Disconnect {
1771                                    from_track: "hw:in".to_string(),
1772                                    from_port,
1773                                    to_track: track_name.clone(),
1774                                    to_port,
1775                                    kind: Kind::Audio,
1776                                });
1777                                actions.push(Action::Connect {
1778                                    from_track: "hw:in".to_string(),
1779                                    from_port: from_port - 1,
1780                                    to_track: track_name.clone(),
1781                                    to_port,
1782                                    kind: Kind::Audio,
1783                                });
1784                            }
1785                        }
1786                    }
1787                }
1788                for (to_port, target) in self.all_hw_output_audio_ports().into_iter().enumerate() {
1789                    if Self::audio_ports_connected(&source, &target) {
1790                        actions.push(Action::Disconnect {
1791                            from_track: "hw:in".to_string(),
1792                            from_port,
1793                            to_track: "hw:out".to_string(),
1794                            to_port,
1795                            kind: Kind::Audio,
1796                        });
1797                        actions.push(Action::Connect {
1798                            from_track: "hw:in".to_string(),
1799                            from_port: from_port - 1,
1800                            to_track: "hw:out".to_string(),
1801                            to_port,
1802                            kind: Kind::Audio,
1803                        });
1804                    }
1805                }
1806            }
1807        }
1808        actions
1809    }
1810
1811    #[cfg(unix)]
1812    fn reindex_notifications_for_removed_hw_output(&self, removed_port: usize) -> Vec<Action> {
1813        let mut actions = Vec::new();
1814        #[cfg(unix)]
1815        if let Some(jack) = &self.jack_runtime {
1816            let jack = jack.lock();
1817            for to_port in (removed_port + 1)..jack.output_channels() {
1818                let Some(target) = jack.output_audio_port(to_port) else {
1819                    continue;
1820                };
1821                {
1822                    let state = self.state.lock();
1823                    for (track_name, track) in &state.tracks {
1824                        let track = track.lock();
1825                        for (from_port, source) in track.audio.outs.iter().enumerate() {
1826                            if Self::audio_ports_connected(source, &target) {
1827                                actions.push(Action::Disconnect {
1828                                    from_track: track_name.clone(),
1829                                    from_port,
1830                                    to_track: "hw:out".to_string(),
1831                                    to_port,
1832                                    kind: Kind::Audio,
1833                                });
1834                                actions.push(Action::Connect {
1835                                    from_track: track_name.clone(),
1836                                    from_port,
1837                                    to_track: "hw:out".to_string(),
1838                                    to_port: to_port - 1,
1839                                    kind: Kind::Audio,
1840                                });
1841                            }
1842                        }
1843                    }
1844                }
1845                for (from_port, source) in jack.audio_ins().into_iter().enumerate() {
1846                    if Self::audio_ports_connected(&source, &target) {
1847                        actions.push(Action::Disconnect {
1848                            from_track: "hw:in".to_string(),
1849                            from_port,
1850                            to_track: "hw:out".to_string(),
1851                            to_port,
1852                            kind: Kind::Audio,
1853                        });
1854                        actions.push(Action::Connect {
1855                            from_track: "hw:in".to_string(),
1856                            from_port,
1857                            to_track: "hw:out".to_string(),
1858                            to_port: to_port - 1,
1859                            kind: Kind::Audio,
1860                        });
1861                    }
1862                }
1863            }
1864        }
1865        actions
1866    }
1867
1868    fn midi_hw_in_device(track: &str) -> Option<&str> {
1869        track.strip_prefix("midi:hw:in:")
1870    }
1871
1872    fn midi_hw_out_device(track: &str) -> Option<&str> {
1873        track.strip_prefix("midi:hw:out:")
1874    }
1875
1876    fn midi_binding_matches(
1877        a: &crate::message::MidiLearnBinding,
1878        b: &crate::message::MidiLearnBinding,
1879    ) -> bool {
1880        if a.channel != b.channel || a.cc != b.cc {
1881            return false;
1882        }
1883        match (&a.device, &b.device) {
1884            (Some(ad), Some(bd)) => ad == bd,
1885            _ => true,
1886        }
1887    }
1888
1889    fn midi_learn_slot_conflicts(
1890        &self,
1891        binding: &crate::message::MidiLearnBinding,
1892        ignore: Option<MidiLearnSlot>,
1893    ) -> Vec<String> {
1894        let mut conflicts = Vec::<String>::new();
1895        let state = self.state.lock();
1896        let mut push_conflict = |slot: MidiLearnSlot, label: String| {
1897            if ignore.as_ref().is_some_and(|i| i == &slot) {
1898                return;
1899            }
1900            conflicts.push(label);
1901        };
1902        let check_global =
1903            |current: &Option<crate::message::MidiLearnBinding>,
1904             target: crate::message::GlobalMidiLearnTarget,
1905             label: &str,
1906             push_conflict: &mut dyn FnMut(MidiLearnSlot, String)| {
1907                if let Some(existing) = current
1908                    && Self::midi_binding_matches(binding, existing)
1909                {
1910                    push_conflict(MidiLearnSlot::Global(target), format!("Global {label}"));
1911                }
1912            };
1913        check_global(
1914            &self.global_midi_learn_play_pause,
1915            crate::message::GlobalMidiLearnTarget::PlayPause,
1916            "PlayPause",
1917            &mut push_conflict,
1918        );
1919        check_global(
1920            &self.global_midi_learn_stop,
1921            crate::message::GlobalMidiLearnTarget::Stop,
1922            "Stop",
1923            &mut push_conflict,
1924        );
1925        check_global(
1926            &self.global_midi_learn_record_toggle,
1927            crate::message::GlobalMidiLearnTarget::RecordToggle,
1928            "RecordToggle",
1929            &mut push_conflict,
1930        );
1931        for (track_name, track) in state.tracks.iter() {
1932            let t = track.lock();
1933            let mut check_track = |current: &Option<crate::message::MidiLearnBinding>,
1934                                   target: crate::message::TrackMidiLearnTarget,
1935                                   label: &str| {
1936                if let Some(existing) = current
1937                    && Self::midi_binding_matches(binding, existing)
1938                {
1939                    push_conflict(
1940                        MidiLearnSlot::Track(track_name.clone(), target),
1941                        format!("{track_name} {label}"),
1942                    );
1943                }
1944            };
1945            check_track(
1946                &t.midi_learn_volume,
1947                crate::message::TrackMidiLearnTarget::Volume,
1948                "Volume",
1949            );
1950            check_track(
1951                &t.midi_learn_balance,
1952                crate::message::TrackMidiLearnTarget::Balance,
1953                "Balance",
1954            );
1955            check_track(
1956                &t.midi_learn_mute,
1957                crate::message::TrackMidiLearnTarget::Mute,
1958                "Mute",
1959            );
1960            check_track(
1961                &t.midi_learn_solo,
1962                crate::message::TrackMidiLearnTarget::Solo,
1963                "Solo",
1964            );
1965            check_track(
1966                &t.midi_learn_arm,
1967                crate::message::TrackMidiLearnTarget::Arm,
1968                "Arm",
1969            );
1970            check_track(
1971                &t.midi_learn_input_monitor,
1972                crate::message::TrackMidiLearnTarget::InputMonitor,
1973                "InputMonitor",
1974            );
1975            check_track(
1976                &t.midi_learn_disk_monitor,
1977                crate::message::TrackMidiLearnTarget::DiskMonitor,
1978                "DiskMonitor",
1979            );
1980        }
1981        conflicts
1982    }
1983
1984    async fn handle_incoming_hw_cc(&mut self, device: &str, channel: u8, cc: u8, value: u8) {
1985        let gate_key = (device.to_string(), channel, cc);
1986        let high = value >= 64;
1987        let prev_high = self.midi_cc_gate.get(&gate_key).copied().unwrap_or(false);
1988        self.midi_cc_gate.insert(gate_key, high);
1989        let rising = high && !prev_high;
1990
1991        if let Some((track_name, target, armed_device)) = self.pending_midi_learn.clone() {
1992            let binding = crate::message::MidiLearnBinding {
1993                device: armed_device.or(Some(device.to_string())),
1994                channel,
1995                cc,
1996            };
1997            let conflicts = self.midi_learn_slot_conflicts(
1998                &binding,
1999                Some(MidiLearnSlot::Track(track_name.clone(), target)),
2000            );
2001            if !conflicts.is_empty() {
2002                self.pending_midi_learn = None;
2003                self.notify_clients(Err(format!(
2004                    "MIDI learn conflict for '{}' {:?}: {}",
2005                    track_name,
2006                    target,
2007                    conflicts.join(", ")
2008                )))
2009                .await;
2010                return;
2011            }
2012            if let Some(track) = self.state.lock().tracks.get(&track_name) {
2013                match target {
2014                    crate::message::TrackMidiLearnTarget::Volume => {
2015                        track.lock().midi_learn_volume = Some(binding.clone());
2016                    }
2017                    crate::message::TrackMidiLearnTarget::Balance => {
2018                        track.lock().midi_learn_balance = Some(binding.clone());
2019                    }
2020                    crate::message::TrackMidiLearnTarget::Mute => {
2021                        track.lock().midi_learn_mute = Some(binding.clone());
2022                    }
2023                    crate::message::TrackMidiLearnTarget::Solo => {
2024                        track.lock().midi_learn_solo = Some(binding.clone());
2025                    }
2026                    crate::message::TrackMidiLearnTarget::Arm => {
2027                        track.lock().midi_learn_arm = Some(binding.clone());
2028                    }
2029                    crate::message::TrackMidiLearnTarget::InputMonitor => {
2030                        track.lock().midi_learn_input_monitor = Some(binding.clone());
2031                    }
2032                    crate::message::TrackMidiLearnTarget::DiskMonitor => {
2033                        track.lock().midi_learn_disk_monitor = Some(binding.clone());
2034                    }
2035                }
2036                self.pending_midi_learn = None;
2037                self.notify_clients(Ok(Action::TrackSetMidiLearnBinding {
2038                    track_name: track_name.clone(),
2039                    target,
2040                    binding: Some(binding),
2041                }))
2042                .await;
2043            } else {
2044                self.pending_midi_learn = None;
2045            }
2046        }
2047        if let Some(target) = self.pending_global_midi_learn.take() {
2048            let binding = crate::message::MidiLearnBinding {
2049                device: Some(device.to_string()),
2050                channel,
2051                cc,
2052            };
2053            let conflicts =
2054                self.midi_learn_slot_conflicts(&binding, Some(MidiLearnSlot::Global(target)));
2055            if !conflicts.is_empty() {
2056                self.notify_clients(Err(format!(
2057                    "Global MIDI learn conflict for {:?}: {}",
2058                    target,
2059                    conflicts.join(", ")
2060                )))
2061                .await;
2062                return;
2063            }
2064            match target {
2065                crate::message::GlobalMidiLearnTarget::PlayPause => {
2066                    self.global_midi_learn_play_pause = Some(binding.clone());
2067                }
2068                crate::message::GlobalMidiLearnTarget::Stop => {
2069                    self.global_midi_learn_stop = Some(binding.clone());
2070                }
2071                crate::message::GlobalMidiLearnTarget::RecordToggle => {
2072                    self.global_midi_learn_record_toggle = Some(binding.clone());
2073                }
2074            }
2075            self.notify_clients(Ok(Action::SetGlobalMidiLearnBinding {
2076                target,
2077                binding: Some(binding),
2078            }))
2079            .await;
2080        }
2081
2082        let mut mapped_actions = Vec::<Action>::new();
2083        for (track_name, track) in self.state.lock().tracks.iter() {
2084            let t = track.lock();
2085            if let Some(binding) = t.midi_learn_volume.as_ref() {
2086                let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2087                if device_matches && binding.channel == channel && binding.cc == cc {
2088                    let level = -90.0 + (value as f32 / 127.0) * 110.0;
2089                    mapped_actions.push(Action::TrackLevel(track_name.clone(), level));
2090                }
2091            }
2092            if let Some(binding) = t.midi_learn_balance.as_ref() {
2093                let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2094                if device_matches && binding.channel == channel && binding.cc == cc {
2095                    let balance = (value as f32 / 127.0) * 2.0 - 1.0;
2096                    mapped_actions.push(Action::TrackBalance(track_name.clone(), balance));
2097                }
2098            }
2099            if let Some(binding) = t.midi_learn_mute.as_ref() {
2100                let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2101                if device_matches && binding.channel == channel && binding.cc == cc {
2102                    let wanted = value >= 64;
2103                    if t.muted != wanted {
2104                        mapped_actions.push(Action::TrackToggleMute(track_name.clone()));
2105                    }
2106                }
2107            }
2108            if let Some(binding) = t.midi_learn_solo.as_ref() {
2109                let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2110                if device_matches && binding.channel == channel && binding.cc == cc {
2111                    let wanted = value >= 64;
2112                    if t.soloed != wanted {
2113                        mapped_actions.push(Action::TrackToggleSolo(track_name.clone()));
2114                    }
2115                }
2116            }
2117            if let Some(binding) = t.midi_learn_arm.as_ref() {
2118                let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2119                if device_matches && binding.channel == channel && binding.cc == cc {
2120                    let wanted = value >= 64;
2121                    if t.armed != wanted {
2122                        mapped_actions.push(Action::TrackToggleArm(track_name.clone()));
2123                    }
2124                }
2125            }
2126            if let Some(binding) = t.midi_learn_input_monitor.as_ref() {
2127                let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2128                if device_matches && binding.channel == channel && binding.cc == cc {
2129                    let wanted = value >= 64;
2130                    if t.input_monitor != wanted {
2131                        mapped_actions.push(Action::TrackToggleInputMonitor(track_name.clone()));
2132                    }
2133                }
2134            }
2135            if let Some(binding) = t.midi_learn_disk_monitor.as_ref() {
2136                let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
2137                if device_matches && binding.channel == channel && binding.cc == cc {
2138                    let wanted = value >= 64;
2139                    if t.disk_monitor != wanted {
2140                        mapped_actions.push(Action::TrackToggleDiskMonitor(track_name.clone()));
2141                    }
2142                }
2143            }
2144        }
2145        let device_matches =
2146            |binding: &crate::message::MidiLearnBinding| binding.device.as_deref() == Some(device);
2147        let mut mapped_global_actions = Vec::<Action>::new();
2148        if let Some(binding) = self.global_midi_learn_play_pause.as_ref()
2149            && device_matches(binding)
2150            && binding.channel == channel
2151            && binding.cc == cc
2152            && rising
2153        {
2154            mapped_global_actions.push(if self.playing {
2155                Action::Stop
2156            } else {
2157                Action::Play
2158            });
2159        }
2160        if let Some(binding) = self.global_midi_learn_stop.as_ref()
2161            && device_matches(binding)
2162            && binding.channel == channel
2163            && binding.cc == cc
2164            && rising
2165            && self.playing
2166        {
2167            mapped_global_actions.push(Action::Stop);
2168        }
2169        if let Some(binding) = self.global_midi_learn_record_toggle.as_ref()
2170            && device_matches(binding)
2171            && binding.channel == channel
2172            && binding.cc == cc
2173            && rising
2174        {
2175            mapped_global_actions.push(Action::SetRecordEnabled(!self.record_enabled));
2176        }
2177        for action in mapped_actions {
2178            match action {
2179                Action::TrackLevel(ref track_name, level) => {
2180                    if let Some(track) = self.state.lock().tracks.get(track_name) {
2181                        track.lock().set_level(level);
2182                        self.notify_clients(Ok(Action::TrackLevel(track_name.clone(), level)))
2183                            .await;
2184                    }
2185                }
2186                Action::TrackBalance(ref track_name, balance) => {
2187                    if let Some(track) = self.state.lock().tracks.get(track_name) {
2188                        track.lock().set_balance(balance);
2189                        self.notify_clients(Ok(Action::TrackBalance(track_name.clone(), balance)))
2190                            .await;
2191                    }
2192                }
2193                Action::TrackToggleMute(ref track_name) => {
2194                    if let Some(track) = self.state.lock().tracks.get(track_name) {
2195                        track.lock().mute();
2196                        self.notify_clients(Ok(Action::TrackToggleMute(track_name.clone())))
2197                            .await;
2198                    }
2199                }
2200                Action::TrackTogglePhase(ref track_name) => {
2201                    if let Some(track) = self.state.lock().tracks.get(track_name) {
2202                        track.lock().invert_phase();
2203                        self.notify_clients(Ok(Action::TrackTogglePhase(track_name.clone())))
2204                            .await;
2205                    }
2206                }
2207                Action::TrackToggleSolo(ref track_name) => {
2208                    if let Some(track) = self.state.lock().tracks.get(track_name) {
2209                        track.lock().solo();
2210                        self.notify_clients(Ok(Action::TrackToggleSolo(track_name.clone())))
2211                            .await;
2212                    }
2213                }
2214                Action::TrackToggleMaster(ref track_name) => {
2215                    if let Some(track) = self.state.lock().tracks.get(track_name) {
2216                        let blocked = {
2217                            let t = track.lock();
2218                            t.vca_master.is_some() || !self.vca_followers(track_name).is_empty()
2219                        };
2220                        if blocked {
2221                            self.notify_clients(Err(format!(
2222                                "Track '{}' cannot be promoted to Master while part of a VCA group",
2223                                track_name
2224                            )))
2225                            .await;
2226                            continue;
2227                        }
2228                        track.lock().toggle_master();
2229                        self.notify_clients(Ok(Action::TrackToggleMaster(track_name.clone())))
2230                            .await;
2231                    }
2232                }
2233                Action::TrackToggleArm(ref track_name) => {
2234                    if let Some(track) = self.state.lock().tracks.get(track_name) {
2235                        track.lock().arm();
2236                        self.notify_clients(Ok(Action::TrackToggleArm(track_name.clone())))
2237                            .await;
2238                    }
2239                }
2240                Action::TrackToggleInputMonitor(ref track_name) => {
2241                    if let Some(track) = self.state.lock().tracks.get(track_name) {
2242                        track.lock().toggle_input_monitor();
2243                        self.notify_clients(Ok(Action::TrackToggleInputMonitor(
2244                            track_name.clone(),
2245                        )))
2246                        .await;
2247                    }
2248                }
2249                Action::TrackToggleDiskMonitor(ref track_name) => {
2250                    if let Some(track) = self.state.lock().tracks.get(track_name) {
2251                        track.lock().toggle_disk_monitor();
2252                        self.notify_clients(Ok(Action::TrackToggleDiskMonitor(track_name.clone())))
2253                            .await;
2254                    }
2255                }
2256                _ => {}
2257            }
2258        }
2259        for action in mapped_global_actions {
2260            self.handle_request_inner(action, false).await;
2261        }
2262    }
2263
2264    fn vca_followers(&self, master_name: &str) -> Vec<String> {
2265        self.state
2266            .lock()
2267            .tracks
2268            .iter()
2269            .filter_map(|(name, track)| {
2270                if track.lock().vca_master.as_deref() == Some(master_name) {
2271                    Some(name.clone())
2272                } else {
2273                    None
2274                }
2275            })
2276            .collect()
2277    }
2278
2279    fn upstream_audio_track_names(
2280        &self,
2281        seeds: &std::collections::HashSet<String>,
2282    ) -> std::collections::HashSet<String> {
2283        let state = self.state.lock();
2284        let mut output_to_track: std::collections::HashMap<
2285            *const crate::audio::io::AudioIO,
2286            String,
2287        > = std::collections::HashMap::new();
2288        for (name, track) in &state.tracks {
2289            let t = track.lock();
2290            for out in &t.audio.outs {
2291                output_to_track.insert(std::sync::Arc::as_ptr(out), name.clone());
2292            }
2293        }
2294        let mut upstream = std::collections::HashSet::new();
2295        let mut to_process: Vec<String> = seeds.iter().cloned().collect();
2296        let mut processed = std::collections::HashSet::new();
2297        while let Some(target_name) = to_process.pop() {
2298            if !processed.insert(target_name.clone()) {
2299                continue;
2300            }
2301            if let Some(target_track) = state.tracks.get(&target_name) {
2302                let tt = target_track.lock();
2303                for input in &tt.audio.ins {
2304                    for conn in input.connections.lock().iter() {
2305                        let conn_ptr = std::sync::Arc::as_ptr(conn);
2306                        if let Some(source_name) = output_to_track.get(&conn_ptr)
2307                            && source_name != &target_name
2308                            && !seeds.contains(source_name)
2309                        {
2310                            upstream.insert(source_name.clone());
2311                            to_process.push(source_name.clone());
2312                        }
2313                    }
2314                }
2315            }
2316        }
2317        upstream
2318    }
2319
2320    fn is_track_in_soloed_folder(
2321        &self,
2322        track: &Track,
2323        tracks: &std::collections::HashMap<String, Arc<UnsafeMutex<Box<Track>>>>,
2324    ) -> bool {
2325        let mut current = track.parent_track.as_deref();
2326        while let Some(parent_name) = current {
2327            if let Some(parent) = tracks.get(parent_name) {
2328                let p = parent.lock();
2329                if p.soloed {
2330                    return true;
2331                }
2332                current = p.parent_track.as_deref();
2333            } else {
2334                break;
2335            }
2336        }
2337        false
2338    }
2339
2340    fn folder_has_soloed_descendant(
2341        &self,
2342        folder_name: &str,
2343        tracks: &std::collections::HashMap<String, Arc<UnsafeMutex<Box<Track>>>>,
2344    ) -> bool {
2345        for track in tracks.values() {
2346            let t = track.lock();
2347            if !t.soloed {
2348                continue;
2349            }
2350            let mut current = t.parent_track.as_deref();
2351            while let Some(parent_name) = current {
2352                if parent_name == folder_name {
2353                    return true;
2354                }
2355                if let Some(parent) = tracks.get(parent_name) {
2356                    current = parent.lock().parent_track.as_deref();
2357                } else {
2358                    break;
2359                }
2360            }
2361        }
2362        false
2363    }
2364
2365    fn refresh_realtime_infection(&self) {
2366        let state = self.state.lock();
2367        let live_seeds: std::collections::HashSet<String> = state
2368            .tracks
2369            .iter()
2370            .filter_map(|(name, track)| {
2371                let t = track.lock();
2372                if t.armed && t.input_monitor {
2373                    Some(name.clone())
2374                } else {
2375                    None
2376                }
2377            })
2378            .collect();
2379        let mut output_owner: std::collections::HashMap<*const crate::audio::io::AudioIO, String> =
2380            std::collections::HashMap::new();
2381        for (name, track) in state.tracks.iter() {
2382            let t = track.lock();
2383            for out in &t.audio.outs {
2384                output_owner.insert(std::sync::Arc::as_ptr(out), name.clone());
2385            }
2386        }
2387
2388        let mut infected = live_seeds.clone();
2389        let mut mixed_nodes = std::collections::HashSet::new();
2390        loop {
2391            let mut changed = false;
2392            for (name, track) in state.tracks.iter() {
2393                let t = track.lock();
2394                let mut upstream_owners = std::collections::HashSet::new();
2395                for input in &t.audio.ins {
2396                    for conn in input.connections.lock().iter() {
2397                        if let Some(owner) = output_owner.get(&std::sync::Arc::as_ptr(conn)) {
2398                            upstream_owners.insert(owner.clone());
2399                        }
2400                    }
2401                }
2402                if upstream_owners.is_empty() {
2403                    continue;
2404                }
2405                let has_realtime = upstream_owners
2406                    .iter()
2407                    .any(|owner| infected.contains(owner) || live_seeds.contains(owner));
2408                let has_playback = upstream_owners
2409                    .iter()
2410                    .any(|owner| !infected.contains(owner) && !live_seeds.contains(owner));
2411                if has_realtime && has_playback {
2412                    mixed_nodes.insert(name.clone());
2413                }
2414                if has_realtime && infected.insert(name.clone()) {
2415                    changed = true;
2416                }
2417            }
2418            if !changed {
2419                break;
2420            }
2421        }
2422
2423        for (name, track) in state.tracks.iter() {
2424            let forced = infected.contains(name) && !live_seeds.contains(name);
2425            let t = track.lock();
2426            t.set_shared_realtime_mixed(mixed_nodes.contains(name));
2427            t.set_force_realtime_domain(forced);
2428        }
2429    }
2430
2431    fn apply_mute_solo_policy(&mut self) {
2432        let mut newly_disabled_tracks = Vec::new();
2433        {
2434            let tracks = &self.state.lock().tracks;
2435            let soloed: std::collections::HashSet<String> = tracks
2436                .iter()
2437                .filter_map(|(name, t)| {
2438                    if t.lock().soloed {
2439                        Some(name.clone())
2440                    } else {
2441                        None
2442                    }
2443                })
2444                .collect();
2445            let any_soloed = !soloed.is_empty();
2446            let upstream = if any_soloed {
2447                self.upstream_audio_track_names(&soloed)
2448            } else {
2449                std::collections::HashSet::new()
2450            };
2451            for track in tracks.values() {
2452                let t = track.lock();
2453                let was_enabled = t.output_enabled;
2454                let in_soloed_folder = self.is_track_in_soloed_folder(t, tracks);
2455                let folder_with_soloed_child =
2456                    t.is_folder && self.folder_has_soloed_descendant(&t.name, tracks);
2457                let enabled = if t.is_master {
2458                    !t.muted
2459                } else if any_soloed {
2460                    (t.soloed
2461                        || upstream.contains(&t.name)
2462                        || in_soloed_folder
2463                        || folder_with_soloed_child)
2464                        && !t.muted
2465                } else {
2466                    !t.muted
2467                };
2468                t.set_output_enabled(enabled);
2469                if was_enabled && !enabled {
2470                    newly_disabled_tracks.push(t.name.clone());
2471                }
2472            }
2473        }
2474        let mut note_off_events = Vec::new();
2475        for track_name in newly_disabled_tracks {
2476            note_off_events.extend(self.note_off_events_for_track(&track_name));
2477        }
2478        if !note_off_events.is_empty() {
2479            self.pending_hw_midi_out_events_by_device
2480                .extend(note_off_events);
2481        }
2482    }
2483
2484    fn sanitize_file_stem(name: &str) -> String {
2485        let mut out = String::with_capacity(name.len());
2486        for c in name.chars() {
2487            if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
2488                out.push(c);
2489            } else {
2490                out.push('_');
2491            }
2492        }
2493        if out.is_empty() {
2494            "track".to_string()
2495        } else {
2496            out
2497        }
2498    }
2499
2500    fn next_recording_file_name(track_name: &str) -> String {
2501        let ts = SystemTime::now()
2502            .duration_since(UNIX_EPOCH)
2503            .map(|d| d.as_secs())
2504            .unwrap_or(0);
2505        format!("{}_{}.wav", Self::sanitize_file_stem(track_name), ts)
2506    }
2507
2508    fn next_midi_recording_file_name(track_name: &str) -> String {
2509        let ts = SystemTime::now()
2510            .duration_since(UNIX_EPOCH)
2511            .map(|d| d.as_secs())
2512            .unwrap_or(0);
2513        format!("{}_{}.mid", Self::sanitize_file_stem(track_name), ts)
2514    }
2515
2516    fn append_recorded_cycle(&mut self) {
2517        if !self.playing || !self.record_enabled {
2518            return;
2519        }
2520        for (name, track_handle) in &self.state.lock().tracks {
2521            let track = track_handle.lock();
2522            if !track.armed {
2523                continue;
2524            }
2525            let audio_channels = track.record_tap_outs.len();
2526            let audio_frames = track
2527                .record_tap_outs
2528                .first()
2529                .map(|ch| ch.len())
2530                .unwrap_or(0);
2531            let frames = audio_frames.max(self.current_cycle_samples());
2532            if frames == 0 {
2533                continue;
2534            }
2535            let segments = self.recording_segments_for_cycle(frames);
2536            for (segment_start, segment_end, frame_offset) in segments {
2537                let segment_len = segment_end.saturating_sub(segment_start);
2538                if segment_len == 0 {
2539                    continue;
2540                }
2541
2542                if audio_channels > 0 && audio_frames > 0 {
2543                    let audio_entry =
2544                        self.audio_recordings
2545                            .entry(name.clone())
2546                            .or_insert_with(|| RecordingSession {
2547                                start_sample: segment_start,
2548                                samples: Vec::with_capacity(segment_len * audio_channels * 2),
2549                                channels: audio_channels,
2550                                file_name: Self::next_recording_file_name(name),
2551                            });
2552                    if audio_entry.channels != audio_channels {
2553                        continue;
2554                    }
2555                    if let Some(entry) = self.audio_recordings.get_mut(name.as_str()) {
2556                        let from = frame_offset.min(audio_frames);
2557                        let to = frame_offset.saturating_add(segment_len).min(audio_frames);
2558                        for frame in from..to {
2559                            for ch in 0..audio_channels {
2560                                entry.samples.push(track.record_tap_outs[ch][frame]);
2561                            }
2562                        }
2563                    }
2564                }
2565
2566                let entry = self.midi_recordings.entry(name.clone()).or_insert_with(|| {
2567                    MidiRecordingSession {
2568                        start_sample: segment_start,
2569                        events: Vec::new(),
2570                        file_name: Self::next_midi_recording_file_name(name),
2571                    }
2572                });
2573                let from = frame_offset;
2574                let to = frame_offset.saturating_add(segment_len);
2575                for event in &track.record_tap_midi_in {
2576                    let frame = event.frame as usize;
2577                    if frame < from || frame >= to {
2578                        continue;
2579                    }
2580                    let abs_sample = segment_start as u64 + (frame - from) as u64;
2581                    entry.events.push((abs_sample, event.data.clone()));
2582                }
2583
2584                if self.punch_enabled
2585                    && let Some((_, punch_end)) = self.punch_range_samples
2586                    && segment_end == punch_end
2587                {
2588                    if let Some(done) = self.audio_recordings.remove(name.as_str()) {
2589                        self.completed_audio_recordings.push((name.clone(), done));
2590                    }
2591                    if let Some(done) = self.midi_recordings.remove(name.as_str()) {
2592                        self.completed_midi_recordings.push((name.clone(), done));
2593                    }
2594                } else if self.loop_enabled
2595                    && let Some((_, loop_end)) = self.loop_range_samples
2596                    && segment_end == loop_end
2597                {
2598                    if let Some(done) = self.audio_recordings.remove(name.as_str()) {
2599                        self.completed_audio_recordings.push((name.clone(), done));
2600                    }
2601                    if let Some(done) = self.midi_recordings.remove(name.as_str()) {
2602                        self.completed_midi_recordings.push((name.clone(), done));
2603                    }
2604                }
2605            }
2606        }
2607    }
2608
2609    async fn flush_completed_recordings(&mut self) {
2610        if self.completed_audio_recordings.is_empty() && self.completed_midi_recordings.is_empty() {
2611            return;
2612        }
2613        let Some(audio_dir) = self.session_audio_dir() else {
2614            self.completed_audio_recordings.clear();
2615            self.completed_midi_recordings.clear();
2616            return;
2617        };
2618        let Some(midi_dir) = self.session_midi_dir() else {
2619            self.completed_audio_recordings.clear();
2620            self.completed_midi_recordings.clear();
2621            return;
2622        };
2623        if std::fs::create_dir_all(&audio_dir).is_err()
2624            || std::fs::create_dir_all(&midi_dir).is_err()
2625        {
2626            self.completed_audio_recordings.clear();
2627            self.completed_midi_recordings.clear();
2628            return;
2629        }
2630        let rate = self
2631            .hw_driver
2632            .as_ref()
2633            .map(|o| o.lock().sample_rate())
2634            .unwrap_or(48_000);
2635        let completed_audio = std::mem::take(&mut self.completed_audio_recordings);
2636        for (track_name, rec) in completed_audio {
2637            self.flush_recording_entry(&audio_dir, rate, track_name, rec)
2638                .await;
2639        }
2640        let completed_midi = std::mem::take(&mut self.completed_midi_recordings);
2641        for (track_name, rec) in completed_midi {
2642            self.flush_midi_recording_entry(&midi_dir, rate as u32, track_name, rec)
2643                .await;
2644        }
2645    }
2646
2647    async fn flush_recordings(&mut self) {
2648        let Some(audio_dir) = self.session_audio_dir() else {
2649            if !self.audio_recordings.is_empty()
2650                || !self.midi_recordings.is_empty()
2651                || !self.completed_audio_recordings.is_empty()
2652                || !self.completed_midi_recordings.is_empty()
2653            {
2654                self.notify_clients(Err("Recording stopped: session path is not set".to_string()))
2655                    .await;
2656            }
2657            self.audio_recordings.clear();
2658            self.midi_recordings.clear();
2659            self.completed_audio_recordings.clear();
2660            self.completed_midi_recordings.clear();
2661            return;
2662        };
2663        if std::fs::create_dir_all(&audio_dir).is_err() {
2664            self.notify_clients(Err(format!(
2665                "Recording stopped: failed to create audio directory {}",
2666                audio_dir.display()
2667            )))
2668            .await;
2669            self.audio_recordings.clear();
2670            self.midi_recordings.clear();
2671            self.completed_audio_recordings.clear();
2672            self.completed_midi_recordings.clear();
2673            return;
2674        }
2675        let Some(midi_dir) = self.session_midi_dir() else {
2676            self.audio_recordings.clear();
2677            self.midi_recordings.clear();
2678            self.completed_audio_recordings.clear();
2679            self.completed_midi_recordings.clear();
2680            return;
2681        };
2682        if std::fs::create_dir_all(&midi_dir).is_err() {
2683            self.audio_recordings.clear();
2684            self.midi_recordings.clear();
2685            self.completed_audio_recordings.clear();
2686            self.completed_midi_recordings.clear();
2687            return;
2688        }
2689        let rate = self
2690            .hw_driver
2691            .as_ref()
2692            .map(|o| o.lock().sample_rate())
2693            .unwrap_or(48_000);
2694        let completed_audio = std::mem::take(&mut self.completed_audio_recordings);
2695        for (track_name, rec) in completed_audio {
2696            self.flush_recording_entry(&audio_dir, rate, track_name, rec)
2697                .await;
2698        }
2699        let completed_midi = std::mem::take(&mut self.completed_midi_recordings);
2700        for (track_name, rec) in completed_midi {
2701            self.flush_midi_recording_entry(&midi_dir, rate as u32, track_name, rec)
2702                .await;
2703        }
2704        let recordings = std::mem::take(&mut self.audio_recordings);
2705        for (track_name, rec) in recordings {
2706            self.flush_recording_entry(&audio_dir, rate, track_name, rec)
2707                .await;
2708        }
2709        let midi_recordings = std::mem::take(&mut self.midi_recordings);
2710        for (track_name, rec) in midi_recordings {
2711            self.flush_midi_recording_entry(&midi_dir, rate as u32, track_name, rec)
2712                .await;
2713        }
2714    }
2715
2716    async fn flush_recording_entry(
2717        &mut self,
2718        audio_dir: &Path,
2719        rate: i32,
2720        track_name: String,
2721        rec: RecordingSession,
2722    ) {
2723        if rec.samples.is_empty() || rec.channels == 0 {
2724            return;
2725        }
2726        let file_path = audio_dir.join(&rec.file_name);
2727        let write_result =
2728            crate::audio_codec::write_wav_f32(&file_path, &rec.samples, rec.channels, rate as u32);
2729        if let Err(e) = write_result {
2730            self.notify_clients(Err(format!(
2731                "Failed to write recording {}: {}",
2732                file_path.display(),
2733                e
2734            )))
2735            .await;
2736            return;
2737        }
2738        let length = rec.samples.len() / rec.channels;
2739        let clip_rel_name = format!("audio/{}", rec.file_name);
2740        let clip = AudioClip::new(
2741            clip_rel_name.clone(),
2742            rec.start_sample,
2743            rec.start_sample.saturating_add(length.max(1)),
2744        );
2745        let (audio_ins, audio_outs) = if let Some(track) = self.state.lock().tracks.get(&track_name)
2746        {
2747            let track = track.lock();
2748            let audio_ins = track.audio.ins.len();
2749            let audio_outs = track.audio.outs.len();
2750            track.audio.clips.push(clip.clone());
2751            (audio_ins, audio_outs)
2752        } else {
2753            (0, 0)
2754        };
2755        self.notify_clients(Ok(Action::AddClip {
2756            name: clip_rel_name,
2757            track_name: track_name.clone(),
2758            start: rec.start_sample,
2759            length,
2760            offset: 0,
2761            input_channel: 0,
2762            muted: false,
2763            peaks_file: None,
2764            kind: Kind::Audio,
2765            fade_enabled: clip.fade_enabled,
2766            fade_in_samples: clip.fade_in_samples,
2767            fade_out_samples: clip.fade_out_samples,
2768            source_name: None,
2769            source_offset: None,
2770            source_length: None,
2771            preview_name: None,
2772            pitch_correction_points: vec![],
2773            pitch_correction_frame_likeness: None,
2774            pitch_correction_inertia_ms: None,
2775            pitch_correction_formant_compensation: None,
2776            plugin_graph_json: Some(Self::default_clip_plugin_graph_json(audio_ins, audio_outs)),
2777        }))
2778        .await;
2779        if let Some(track) = self.state.lock().tracks.get(&track_name).cloned() {
2780            tokio::task::spawn_blocking(move || {
2781                track.lock().preload_clips();
2782                tracing::debug!("Preloaded clips for track '{}' after recording", track_name);
2783            });
2784        }
2785    }
2786
2787    async fn flush_track_recording(&mut self, track_name: &str) {
2788        let Some(audio_dir) = self.session_audio_dir() else {
2789            self.audio_recordings.remove(track_name);
2790            self.midi_recordings.remove(track_name);
2791            self.completed_audio_recordings
2792                .retain(|(name, _)| name != track_name);
2793            self.completed_midi_recordings
2794                .retain(|(name, _)| name != track_name);
2795            return;
2796        };
2797        let Some(midi_dir) = self.session_midi_dir() else {
2798            self.audio_recordings.remove(track_name);
2799            self.midi_recordings.remove(track_name);
2800            self.completed_audio_recordings
2801                .retain(|(name, _)| name != track_name);
2802            self.completed_midi_recordings
2803                .retain(|(name, _)| name != track_name);
2804            return;
2805        };
2806        if std::fs::create_dir_all(&audio_dir).is_err()
2807            || std::fs::create_dir_all(&midi_dir).is_err()
2808        {
2809            return;
2810        }
2811        let rate = self
2812            .hw_driver
2813            .as_ref()
2814            .map(|o| o.lock().sample_rate())
2815            .unwrap_or(48_000);
2816        let mut i = 0;
2817        while i < self.completed_audio_recordings.len() {
2818            if self.completed_audio_recordings[i].0 == track_name {
2819                let (name, rec) = self.completed_audio_recordings.remove(i);
2820                self.flush_recording_entry(&audio_dir, rate, name, rec)
2821                    .await;
2822            } else {
2823                i += 1;
2824            }
2825        }
2826        let mut j = 0;
2827        while j < self.completed_midi_recordings.len() {
2828            if self.completed_midi_recordings[j].0 == track_name {
2829                let (name, rec) = self.completed_midi_recordings.remove(j);
2830                self.flush_midi_recording_entry(&midi_dir, rate as u32, name, rec)
2831                    .await;
2832            } else {
2833                j += 1;
2834            }
2835        }
2836
2837        let Some(rec) = self.audio_recordings.remove(track_name) else {
2838            if let Some(mrec) = self.midi_recordings.remove(track_name) {
2839                self.flush_midi_recording_entry(
2840                    &midi_dir,
2841                    rate as u32,
2842                    track_name.to_string(),
2843                    mrec,
2844                )
2845                .await;
2846            }
2847            return;
2848        };
2849        self.flush_recording_entry(&audio_dir, rate, track_name.to_string(), rec)
2850            .await;
2851        if let Some(mrec) = self.midi_recordings.remove(track_name) {
2852            self.flush_midi_recording_entry(&midi_dir, rate as u32, track_name.to_string(), mrec)
2853                .await;
2854        }
2855    }
2856
2857    async fn flush_midi_recording_entry(
2858        &mut self,
2859        midi_dir: &Path,
2860        sample_rate: u32,
2861        track_name: String,
2862        mut rec: MidiRecordingSession,
2863    ) {
2864        if rec.events.is_empty() {
2865            return;
2866        }
2867        rec.events.sort_by_key(|(sample, _)| *sample);
2868        let clip_rel_name = format!("midi/{}", rec.file_name);
2869        let clip_len_samples = rec
2870            .events
2871            .last()
2872            .map(|(s, _)| s.saturating_sub(rec.start_sample as u64) as usize + 1)
2873            .unwrap_or(1);
2874
2875        for (sample, _) in &mut rec.events {
2876            *sample = sample.saturating_sub(rec.start_sample as u64);
2877        }
2878        let path = midi_dir.join(&rec.file_name);
2879        if let Err(e) = Self::write_midi_file(&path, sample_rate, &rec.events) {
2880            self.notify_clients(Err(format!(
2881                "Failed to write MIDI recording {}: {}",
2882                path.display(),
2883                e
2884            )))
2885            .await;
2886            return;
2887        }
2888        let mut clip = MIDIClip::new(
2889            clip_rel_name.clone(),
2890            rec.start_sample,
2891            rec.start_sample.saturating_add(clip_len_samples.max(1)),
2892        );
2893        clip.offset = 0;
2894        if let Some(track) = self.state.lock().tracks.get(&track_name) {
2895            track.lock().midi.clips.push(clip);
2896        }
2897        self.notify_clients(Ok(Action::AddClip {
2898            name: clip_rel_name,
2899            track_name: track_name.clone(),
2900            start: rec.start_sample,
2901            length: clip_len_samples,
2902            offset: 0,
2903            input_channel: 0,
2904            muted: false,
2905            peaks_file: None,
2906            kind: Kind::MIDI,
2907            fade_enabled: true,
2908            fade_in_samples: 240,
2909            fade_out_samples: 240,
2910            source_name: None,
2911            source_offset: None,
2912            source_length: None,
2913            preview_name: None,
2914            pitch_correction_points: vec![],
2915            pitch_correction_frame_likeness: None,
2916            pitch_correction_inertia_ms: None,
2917            pitch_correction_formant_compensation: None,
2918            plugin_graph_json: None,
2919        }))
2920        .await;
2921        if let Some(track) = self.state.lock().tracks.get(&track_name).cloned() {
2922            tokio::task::spawn_blocking(move || {
2923                track.lock().preload_clips();
2924                tracing::debug!(
2925                    "Preloaded clips for track '{}' after MIDI recording",
2926                    track_name
2927                );
2928            });
2929        }
2930    }
2931
2932    fn write_midi_file(
2933        path: &Path,
2934        sample_rate: u32,
2935        events: &[(u64, Vec<u8>)],
2936    ) -> Result<(), String> {
2937        let ppq: u16 = 480;
2938        let ticks_per_second: u64 = 960;
2939        let arena = Arena::new();
2940        let mut track_events: Vec<TrackEvent<'_>> = vec![TrackEvent {
2941            delta: u28::new(0),
2942            kind: TrackEventKind::Meta(MetaMessage::Tempo(u24::new(500_000))),
2943        }];
2944        let mut prev_ticks = 0_u64;
2945        for (sample, data) in events {
2946            let ticks = sample.saturating_mul(ticks_per_second) / sample_rate.max(1) as u64;
2947            let delta = ticks.saturating_sub(prev_ticks).min(u32::MAX as u64) as u32;
2948            prev_ticks = ticks;
2949            let Ok(live) = LiveEvent::parse(data) else {
2950                continue;
2951            };
2952            let kind = live.as_track_event(&arena);
2953            track_events.push(TrackEvent {
2954                delta: u28::new(delta),
2955                kind,
2956            });
2957        }
2958        track_events.push(TrackEvent {
2959            delta: u28::new(0),
2960            kind: TrackEventKind::Meta(MetaMessage::EndOfTrack),
2961        });
2962
2963        let smf = Smf {
2964            header: Header::new(Format::SingleTrack, Timing::Metrical(u15::new(ppq))),
2965            tracks: vec![track_events],
2966        };
2967        let mut file = File::create(path).map_err(|e| e.to_string())?;
2968        smf.write_std(&mut file).map_err(|e| e.to_string())
2969    }
2970
2971    pub async fn init(&mut self) {
2972        let max_threads = num_cpus::get();
2973        let realtime_count = if max_threads > 1 { 1 } else { max_threads };
2974        for id in 0..max_threads {
2975            let class = if id < realtime_count {
2976                WorkerClass::Realtime
2977            } else {
2978                WorkerClass::Refill
2979            };
2980            let priority = match class {
2981                WorkerClass::Realtime => 20,
2982                WorkerClass::Refill => 8,
2983            };
2984            let (tx, rx) = channel::<Message>(32);
2985            let tx_thread = self.tx.clone();
2986            let handler = tokio::spawn(async move {
2987                let wrk = Worker::new(id, rx, tx_thread, priority);
2988                wrk.await.work().await;
2989            });
2990            self.worker_classes.push(class);
2991            self.workers.push(WorkerData::new(tx.clone(), handler));
2992        }
2993    }
2994
2995    async fn notify_clients(&mut self, action: Result<Action, String>) {
2996        self.clients.retain(|client| !client.is_closed());
2997        for client in &self.clients {
2998            client
2999                .send(Message::Response(action.clone()))
3000                .await
3001                .expect("Error sending response to client");
3002        }
3003    }
3004
3005    fn set_osc_enabled_with<F>(&mut self, enabled: bool, start_server: F) -> Result<(), String>
3006    where
3007        F: FnOnce(Sender<Message>) -> Result<OscServer, String>,
3008    {
3009        if enabled {
3010            if self.osc_server.is_none() {
3011                self.osc_server = Some(start_server(self.tx.clone())?);
3012            }
3013        } else if let Some(mut server) = self.osc_server.take() {
3014            server.stop();
3015        }
3016        Ok(())
3017    }
3018
3019    fn track_handle_by_name(&self, track_name: &str) -> Option<Arc<UnsafeMutex<Box<Track>>>> {
3020        self.state.lock().tracks.get(track_name).cloned()
3021    }
3022
3023    fn track_handle_or_err(
3024        &self,
3025        track_name: &str,
3026    ) -> Result<Arc<UnsafeMutex<Box<Track>>>, String> {
3027        self.track_handle_by_name(track_name)
3028            .ok_or_else(|| format!("Track not found: {track_name}"))
3029    }
3030
3031    fn add_clip_to_track(&self, request: ClipAddRequest<'_>) {
3032        if let Some(track) = self.state.lock().tracks.get(request.track_name) {
3033            let track = track.lock();
3034            if track.is_master {
3035                return;
3036            }
3037            match request.kind {
3038                Kind::Audio => {
3039                    let mut clip = AudioClip::new(
3040                        request.name.to_string(),
3041                        request.start,
3042                        request.start.saturating_add(request.length.max(1)),
3043                    );
3044                    clip.offset = request.offset;
3045                    let max_lane = track.audio.ins.len().saturating_sub(1);
3046                    clip.input_channel = request.input_channel.min(max_lane);
3047                    clip.muted = request.muted;
3048                    clip.peaks_file = request.peaks_file;
3049                    clip.fade_enabled = request.fade_enabled;
3050                    clip.fade_in_samples = request.fade_in_samples;
3051                    clip.fade_out_samples = request.fade_out_samples;
3052                    clip.pitch_correction_preview_name = request.preview_name;
3053                    clip.pitch_correction_source_name = request.source_name;
3054                    clip.pitch_correction_source_offset = request.source_offset;
3055                    clip.pitch_correction_source_length = request.source_length;
3056                    clip.pitch_correction_points = request.pitch_correction_points;
3057                    clip.pitch_correction_frame_likeness = request.pitch_correction_frame_likeness;
3058                    clip.pitch_correction_inertia_ms = request.pitch_correction_inertia_ms;
3059                    clip.pitch_correction_formant_compensation =
3060                        request.pitch_correction_formant_compensation;
3061                    clip.plugin_graph_json = request.plugin_graph_json;
3062                    track.audio.clips.push(clip);
3063                    #[cfg(unix)]
3064                    track.clip_pitch_shifters.clear();
3065                }
3066                Kind::MIDI => {
3067                    let mut clip = MIDIClip::new(
3068                        request.name.to_string(),
3069                        request.start,
3070                        request.start.saturating_add(request.length.max(1)),
3071                    );
3072                    clip.offset = request.offset;
3073                    let max_lane = track.midi.ins.len().saturating_sub(1);
3074                    clip.input_channel = request.input_channel.min(max_lane);
3075                    clip.muted = request.muted;
3076                    track.midi.clips.push(clip);
3077                }
3078            }
3079        }
3080    }
3081
3082    fn audio_clip_from_data(data: &crate::message::AudioClipData) -> AudioClip {
3083        let mut clip = AudioClip::new(
3084            data.name.clone(),
3085            data.start,
3086            data.start.saturating_add(data.length.max(1)),
3087        );
3088        clip.offset = data.offset;
3089        clip.input_channel = data.input_channel;
3090        clip.muted = data.muted;
3091        clip.peaks_file = data.peaks_file.clone();
3092        clip.fade_enabled = data.fade_enabled;
3093        clip.fade_in_samples = data.fade_in_samples;
3094        clip.fade_out_samples = data.fade_out_samples;
3095        clip.pitch_correction_preview_name = data.preview_name.clone();
3096        clip.pitch_correction_source_name = data.source_name.clone();
3097        clip.pitch_correction_source_offset = data.source_offset;
3098        clip.pitch_correction_source_length = data.source_length;
3099        clip.pitch_correction_points = data.pitch_correction_points.clone();
3100        clip.pitch_correction_frame_likeness = data.pitch_correction_frame_likeness;
3101        clip.pitch_correction_inertia_ms = data.pitch_correction_inertia_ms;
3102        clip.pitch_correction_formant_compensation = data.pitch_correction_formant_compensation;
3103        clip.plugin_graph_json = data.plugin_graph_json.clone();
3104        clip.grouped_clips = data
3105            .grouped_clips
3106            .iter()
3107            .map(Self::audio_clip_from_data)
3108            .collect();
3109        for child in &mut clip.grouped_clips {
3110            child.fade_enabled = false;
3111            child.fade_in_samples = 0;
3112            child.fade_out_samples = 0;
3113        }
3114        clip
3115    }
3116
3117    fn midi_clip_from_data(data: &crate::message::MidiClipData) -> MIDIClip {
3118        let mut clip = MIDIClip::new(
3119            data.name.clone(),
3120            data.start,
3121            data.start.saturating_add(data.length.max(1)),
3122        );
3123        clip.offset = data.offset;
3124        clip.input_channel = data.input_channel;
3125        clip.muted = data.muted;
3126        clip.grouped_clips = data
3127            .grouped_clips
3128            .iter()
3129            .map(Self::midi_clip_from_data)
3130            .collect();
3131        clip
3132    }
3133
3134    fn add_grouped_clip_to_track(
3135        &self,
3136        track_name: &str,
3137        kind: Kind,
3138        audio_clip: Option<crate::message::AudioClipData>,
3139        midi_clip: Option<crate::message::MidiClipData>,
3140    ) {
3141        if let Some(track) = self.state.lock().tracks.get(track_name) {
3142            let track = track.lock();
3143            if track.is_master {
3144                return;
3145            }
3146            match kind {
3147                Kind::Audio => {
3148                    if let Some(mut clip) = audio_clip.map(|clip| Self::audio_clip_from_data(&clip))
3149                    {
3150                        let max_lane = track.audio.ins.len().saturating_sub(1);
3151                        clip.input_channel = clip.input_channel.min(max_lane);
3152                        track.audio.clips.push(clip);
3153                        #[cfg(unix)]
3154                        track.clip_pitch_shifters.clear();
3155                    }
3156                }
3157                Kind::MIDI => {
3158                    if let Some(mut clip) = midi_clip.map(|clip| Self::midi_clip_from_data(&clip)) {
3159                        let max_lane = track.midi.ins.len().saturating_sub(1);
3160                        clip.input_channel = clip.input_channel.min(max_lane);
3161                        track.midi.clips.push(clip);
3162                    }
3163                }
3164            }
3165        }
3166    }
3167
3168    fn remove_clips_from_track(&self, track_name: &str, kind: Kind, clip_indices: &[usize]) {
3169        if let Some(track) = self.state.lock().tracks.get(track_name) {
3170            let track = track.lock();
3171            let mut indices = clip_indices.to_vec();
3172            indices.sort_unstable();
3173            indices.dedup();
3174            match kind {
3175                Kind::Audio => {
3176                    for idx in indices.into_iter().rev() {
3177                        if idx < track.audio.clips.len() {
3178                            track.audio.clips.remove(idx);
3179                        }
3180                    }
3181                    #[cfg(unix)]
3182                    track.clip_pitch_shifters.clear();
3183                }
3184                Kind::MIDI => {
3185                    for idx in indices.into_iter().rev() {
3186                        if idx < track.midi.clips.len() {
3187                            track.midi.clips.remove(idx);
3188                        }
3189                    }
3190                }
3191            }
3192        }
3193    }
3194
3195    fn rename_clip_references(
3196        &self,
3197        track_name: &str,
3198        kind: Kind,
3199        clip_index: usize,
3200        new_name: &str,
3201    ) {
3202        let Some(track) = self.state.lock().tracks.get(track_name) else {
3203            return;
3204        };
3205        let track = track.lock();
3206        let old_name = match kind {
3207            Kind::Audio => {
3208                if clip_index >= track.audio.clips.len() {
3209                    return;
3210                }
3211                track.audio.clips[clip_index].name.clone()
3212            }
3213            Kind::MIDI => {
3214                if clip_index >= track.midi.clips.len() {
3215                    return;
3216                }
3217                track.midi.clips[clip_index].name.clone()
3218            }
3219        };
3220
3221        let new_file_name = match kind {
3222            Kind::Audio => format!("audio/{}.wav", new_name),
3223            Kind::MIDI => {
3224                let ext = std::path::Path::new(&old_name)
3225                    .extension()
3226                    .and_then(|e| e.to_str())
3227                    .map(|s| s.to_ascii_lowercase())
3228                    .filter(|e| e == "mid" || e == "midi")
3229                    .unwrap_or_else(|| "mid".to_string());
3230                format!("midi/{}.{}", new_name, ext)
3231            }
3232        };
3233        let _ = track;
3234
3235        for (_, other_track) in self.state.lock().tracks.iter() {
3236            let other_track = other_track.lock();
3237            match kind {
3238                Kind::Audio => {
3239                    for clip in &mut other_track.audio.clips {
3240                        if clip.name == old_name {
3241                            clip.name = new_file_name.clone();
3242                        }
3243                        if clip.pitch_correction_source_name.as_deref() == Some(old_name.as_str()) {
3244                            clip.pitch_correction_source_name = Some(new_file_name.clone());
3245                        }
3246                    }
3247                }
3248                Kind::MIDI => {
3249                    for clip in &mut other_track.midi.clips {
3250                        if clip.name == old_name {
3251                            clip.name = new_file_name.clone();
3252                        }
3253                    }
3254                }
3255            }
3256        }
3257    }
3258
3259    fn set_clip_fade(
3260        &self,
3261        track_name: &str,
3262        clip_index: usize,
3263        kind: Kind,
3264        fade_enabled: bool,
3265        fade_in_samples: usize,
3266        fade_out_samples: usize,
3267    ) {
3268        let Some(track) = self.state.lock().tracks.get(track_name) else {
3269            return;
3270        };
3271        let track = track.lock();
3272        match kind {
3273            Kind::Audio => {
3274                if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3275                    clip.fade_enabled = fade_enabled;
3276                    clip.fade_in_samples = fade_in_samples;
3277                    clip.fade_out_samples = fade_out_samples;
3278                }
3279            }
3280            Kind::MIDI => {}
3281        }
3282    }
3283
3284    fn set_clip_bounds(
3285        &self,
3286        track_name: &str,
3287        clip_index: usize,
3288        kind: Kind,
3289        start: usize,
3290        length: usize,
3291        offset: usize,
3292    ) {
3293        let Some(track) = self.state.lock().tracks.get(track_name) else {
3294            return;
3295        };
3296        let track = track.lock();
3297        match kind {
3298            Kind::Audio => {
3299                if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3300                    clip.start = start;
3301                    clip.end = start.saturating_add(length.max(1));
3302                    clip.offset = offset;
3303                    clip.pitch_correction_preview_name = None;
3304                    clip.pitch_correction_source_name = None;
3305                    clip.pitch_correction_source_offset = None;
3306                    clip.pitch_correction_source_length = None;
3307                    clip.pitch_correction_points.clear();
3308                    clip.pitch_correction_frame_likeness = None;
3309                    clip.pitch_correction_inertia_ms = None;
3310                    clip.pitch_correction_formant_compensation = None;
3311                }
3312                #[cfg(unix)]
3313                track.clip_pitch_shifters.clear();
3314            }
3315            Kind::MIDI => {
3316                if let Some(clip) = track.midi.clips.get_mut(clip_index) {
3317                    clip.start = start;
3318                    clip.end = start.saturating_add(length.max(1));
3319                    clip.offset = offset;
3320                }
3321            }
3322        }
3323    }
3324
3325    fn set_clip_source_name(&self, track_name: &str, clip_index: usize, kind: Kind, name: String) {
3326        let Some(track) = self.state.lock().tracks.get(track_name) else {
3327            return;
3328        };
3329        let track = track.lock();
3330        match kind {
3331            Kind::Audio => {
3332                if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3333                    clip.name = name;
3334                }
3335                #[cfg(unix)]
3336                track.clip_pitch_shifters.clear();
3337            }
3338            Kind::MIDI => {
3339                if let Some(clip) = track.midi.clips.get_mut(clip_index) {
3340                    clip.name = name;
3341                }
3342            }
3343        }
3344    }
3345
3346    fn set_clip_muted(&self, track_name: &str, clip_index: usize, kind: Kind, muted: bool) {
3347        let Some(track) = self.state.lock().tracks.get(track_name) else {
3348            return;
3349        };
3350        let track = track.lock();
3351        match kind {
3352            Kind::Audio => {
3353                if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3354                    clip.muted = muted;
3355                }
3356            }
3357            Kind::MIDI => {
3358                if let Some(clip) = track.midi.clips.get_mut(clip_index) {
3359                    clip.muted = muted;
3360                }
3361            }
3362        }
3363    }
3364
3365    #[allow(clippy::too_many_arguments)]
3366    fn set_clip_pitch_correction(
3367        &self,
3368        track_name: &str,
3369        clip_index: usize,
3370        preview_name: Option<String>,
3371        source_name: Option<String>,
3372        source_offset: Option<usize>,
3373        source_length: Option<usize>,
3374        pitch_correction_points: Vec<crate::message::PitchCorrectionPointData>,
3375        pitch_correction_frame_likeness: Option<f32>,
3376        pitch_correction_inertia_ms: Option<u16>,
3377        pitch_correction_formant_compensation: Option<bool>,
3378    ) {
3379        if let Some(track) = self.state.lock().tracks.get(track_name) {
3380            let track = track.lock();
3381            if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3382                clip.pitch_correction_preview_name = preview_name;
3383                clip.pitch_correction_source_name = source_name;
3384                clip.pitch_correction_source_offset = source_offset;
3385                clip.pitch_correction_source_length = source_length;
3386                clip.pitch_correction_points = pitch_correction_points;
3387                clip.pitch_correction_frame_likeness = pitch_correction_frame_likeness;
3388                clip.pitch_correction_inertia_ms = pitch_correction_inertia_ms;
3389                clip.pitch_correction_formant_compensation = pitch_correction_formant_compensation;
3390            }
3391            #[cfg(unix)]
3392            track.clip_pitch_shifters.clear();
3393        }
3394    }
3395
3396    async fn request_hw_cycle(&mut self) {
3397        if self.awaiting_hwfinished {
3398            return;
3399        }
3400        self.apply_hw_out_gain_and_meter().await;
3401        if let Some(worker) = &self.hw_worker {
3402            if !self.pending_hw_midi_out_events_by_device.is_empty() {
3403                let out_events = std::mem::take(&mut self.pending_hw_midi_out_events_by_device);
3404                if let Err(e) = worker.tx.send(Message::HWMidiOutEvents(out_events)).await {
3405                    error!("Error sending HWMidiOutEvents {e}");
3406                }
3407            }
3408            match worker.tx.send(Message::TracksFinished).await {
3409                Ok(_) => {
3410                    self.awaiting_hwfinished = true;
3411                }
3412                Err(e) => {
3413                    error!("Error sending TracksFinished {e}");
3414                }
3415            }
3416        }
3417    }
3418
3419    async fn clear_hw_midi_output_state(&mut self, send_panic: bool) {
3420        self.pending_hw_midi_out_events.clear();
3421        self.pending_hw_midi_out_events_by_device.clear();
3422        {
3423            let state = self.state.lock();
3424            for track in state.tracks.values() {
3425                track.lock().take_hw_midi_out_events();
3426            }
3427        }
3428
3429        let panic_events = if send_panic {
3430            self.note_off_events_for_all_active_tracks()
3431        } else {
3432            vec![]
3433        };
3434
3435        if let Some(worker) = &self.hw_worker {
3436            if let Err(e) = worker.tx.send(Message::ClearHWMidiOutEvents).await {
3437                error!("Error clearing pending HWMidiOutEvents {e}");
3438            }
3439            if !panic_events.is_empty()
3440                && let Err(e) = worker.tx.send(Message::HWMidiOutEvents(panic_events)).await
3441            {
3442                error!("Error sending transport restart MIDI panic events {e}");
3443            }
3444        } else if !panic_events.is_empty() {
3445            self.pending_hw_midi_out_events_by_device
3446                .extend(panic_events);
3447        }
3448    }
3449
3450    fn invalidate_track_cycle_state(&mut self) {
3451        self.track_process_epoch = self.track_process_epoch.saturating_add(1);
3452        self.track_processing_started_at.clear();
3453        let state = self.state.lock();
3454        for track in state.tracks.values() {
3455            let t = track.lock();
3456            t.audio.finished = false;
3457            t.audio.processing = false;
3458        }
3459    }
3460
3461    fn force_stalled_track_completions(&mut self) {
3462        let now = Instant::now();
3463        let state = self.state.lock();
3464        for (track_name, track) in state.tracks.iter() {
3465            let started = self.track_processing_started_at.get(track_name).copied();
3466            let Some(started) = started else {
3467                continue;
3468            };
3469            if now.duration_since(started) < Self::TRACK_PROCESS_TIMEOUT {
3470                continue;
3471            }
3472            let t = track.lock();
3473            if t.audio.finished || !t.audio.processing {
3474                self.track_processing_started_at.remove(track_name);
3475                continue;
3476            }
3477            for out in &t.audio.outs {
3478                let out_buf = out.buffer.lock();
3479                out_buf.fill(0.0);
3480                *out.finished.lock() = true;
3481            }
3482            t.audio.processing = false;
3483            t.audio.finished = true;
3484            self.track_processing_started_at.remove(track_name);
3485            tracing::warn!(
3486                "Track '{}' exceeded process timeout ({} ms); forcing silent completion for cycle",
3487                track_name,
3488                Self::TRACK_PROCESS_TIMEOUT.as_millis()
3489            );
3490        }
3491    }
3492
3493    fn should_publish_hw_out_meters(&mut self) -> bool {
3494        let now = Instant::now();
3495        match self.last_hw_out_meter_publish {
3496            Some(last) if now.duration_since(last) < Self::METER_PUBLISH_INTERVAL => false,
3497            _ => {
3498                self.last_hw_out_meter_publish = Some(now);
3499                true
3500            }
3501        }
3502    }
3503
3504    fn should_publish_track_meters(&mut self) -> bool {
3505        let now = Instant::now();
3506        match self.last_track_meter_publish {
3507            Some(last) if now.duration_since(last) < Self::METER_PUBLISH_INTERVAL => false,
3508            _ => {
3509                self.last_track_meter_publish = Some(now);
3510                true
3511            }
3512        }
3513    }
3514
3515    fn should_publish_hw_out_linear(&mut self, peaks_linear: &[f32]) -> bool {
3516        #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
3517        {
3518            self.hw_out_meter_publish_phase = !self.hw_out_meter_publish_phase;
3519            if !self.hw_out_meter_publish_phase {
3520                return false;
3521            }
3522            let changed = if self.last_hw_out_meter_linear.len() != peaks_linear.len() {
3523                true
3524            } else {
3525                self.last_hw_out_meter_linear
3526                    .iter()
3527                    .zip(peaks_linear.iter())
3528                    .any(|(prev, next)| (prev - next).abs() >= Self::HW_OUT_METER_LINEAR_EPSILON)
3529            };
3530            if !changed {
3531                return false;
3532            }
3533            self.last_hw_out_meter_linear.clear();
3534            self.last_hw_out_meter_linear
3535                .extend_from_slice(peaks_linear);
3536            true
3537        }
3538        #[cfg(not(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd")))]
3539        {
3540            let _ = peaks_linear;
3541            false
3542        }
3543    }
3544
3545    async fn maybe_notify_hw_out_meter(&mut self, _meter_db: Vec<f32>) {
3546        {}
3547    }
3548
3549    fn collect_changed_track_meters(
3550        &mut self,
3551        _tracks: &[(String, Arc<UnsafeMutex<Box<Track>>>)],
3552    ) -> Vec<(String, Vec<f32>)> {
3553        Vec::new()
3554    }
3555
3556    async fn apply_hw_out_gain_and_meter(&mut self) {
3557        let gain = if self.hw_out_muted {
3558            0.0
3559        } else {
3560            10.0_f32.powf(self.hw_out_level_db / 20.0)
3561        };
3562        let should_notify_interval = self.should_publish_hw_out_meters();
3563        if let Some(oss) = self.hw_driver.clone() {
3564            let hw = oss.lock();
3565            hw.set_output_gain_balance(gain, self.hw_out_balance);
3566            if !should_notify_interval {
3567                return;
3568            }
3569        } else {
3570            #[cfg(unix)]
3571            {
3572                if let Some(jack) = self.jack_runtime.clone() {
3573                    jack.lock().set_output_gain_linear(gain);
3574                    jack.lock().set_output_balance(self.hw_out_balance);
3575                    if !should_notify_interval {
3576                        return;
3577                    }
3578                } else {
3579                    return;
3580                }
3581            }
3582            #[cfg(not(unix))]
3583            {
3584                return;
3585            }
3586        }
3587        let peaks_linear = if let Some(oss) = self.hw_driver.clone() {
3588            oss.lock().output_meter_linear(gain, self.hw_out_balance)
3589        } else {
3590            #[cfg(unix)]
3591            {
3592                if let Some(jack) = self.jack_runtime.clone() {
3593                    let outs = jack.lock().audio_outs();
3594                    let out_count = outs.len();
3595                    let b = if out_count == 2 {
3596                        self.hw_out_balance.clamp(-1.0, 1.0)
3597                    } else {
3598                        0.0
3599                    };
3600                    let mut meters_linear = Vec::with_capacity(out_count);
3601                    for (channel_idx, channel) in outs.iter().enumerate() {
3602                        let balance_gain = if out_count == 2 {
3603                            if channel_idx == 0 {
3604                                (1.0 - b).clamp(0.0, 1.0)
3605                            } else {
3606                                (1.0 + b).clamp(0.0, 1.0)
3607                            }
3608                        } else {
3609                            1.0
3610                        };
3611                        let buf = channel.buffer.lock();
3612                        let peak = crate::simd::peak_abs(buf) * gain * balance_gain;
3613                        meters_linear.push(peak);
3614                    }
3615                    meters_linear
3616                } else {
3617                    return;
3618                }
3619            }
3620            #[cfg(not(unix))]
3621            {
3622                return;
3623            }
3624        };
3625        if self.hw_out_peak_hold_linear.len() != peaks_linear.len() {
3626            self.hw_out_peak_hold_linear.resize(peaks_linear.len(), 0.0);
3627        }
3628        let mut held_peaks = Vec::with_capacity(peaks_linear.len());
3629        for (idx, peak_now) in peaks_linear.iter().copied().enumerate() {
3630            let held = self.hw_out_peak_hold_linear[idx] * 0.92;
3631            let next = peak_now.max(held);
3632            self.hw_out_peak_hold_linear[idx] = next;
3633            held_peaks.push(next);
3634        }
3635        let should_notify =
3636            should_notify_interval && self.should_publish_hw_out_linear(&held_peaks);
3637        let meter_db: Vec<f32> = held_peaks
3638            .into_iter()
3639            .map(Self::meter_linear_to_db)
3640            .collect();
3641        self.latest_hw_out_meter_db = Arc::new(meter_db.clone());
3642        if should_notify {
3643            self.maybe_notify_hw_out_meter(meter_db).await;
3644        }
3645    }
3646
3647    fn preload_track_clips_spawn(&self) {
3648        let tracks: Vec<_> = self.state.lock().tracks.values().cloned().collect();
3649        for track in tracks {
3650            tokio::task::spawn_blocking(move || {
3651                track.lock().preload_clips();
3652            });
3653        }
3654    }
3655
3656    async fn preload_track_clips(&self) {
3657        let tracks: Vec<_> = self.state.lock().tracks.values().cloned().collect();
3658        if tracks.is_empty() {
3659            return;
3660        }
3661        let mut handles = Vec::with_capacity(tracks.len());
3662        for track in tracks {
3663            handles.push(tokio::task::spawn_blocking(move || {
3664                track.lock().preload_clips();
3665            }));
3666        }
3667        for handle in handles {
3668            if let Err(e) = handle.await {
3669                tracing::warn!("Clip preload task panicked: {e}");
3670            }
3671        }
3672    }
3673
3674    async fn send_tracks(&mut self) -> bool {
3675        if !self.playing {
3676            return false;
3677        }
3678        self.refresh_realtime_infection();
3679        let mut cycle_underflows = 0usize;
3680        {
3681            let state = self.state.lock();
3682            for track in state.tracks.values() {
3683                cycle_underflows =
3684                    cycle_underflows.saturating_add(track.lock().take_hybrid_underflow_delta());
3685            }
3686        }
3687        if cycle_underflows > 0 {
3688            self.refill_budget_per_pass = (self.refill_budget_per_pass + 1).min(8);
3689        } else {
3690            self.refill_budget_per_pass = self.refill_budget_per_pass.saturating_sub(1).max(1);
3691        }
3692        self.force_stalled_track_completions();
3693        let mut finished = true;
3694        let mut dispatched = 0;
3695        let mut refill_dispatched = 0usize;
3696        let mut realtime_fallback_dispatched = 0usize;
3697        loop {
3698            let next_track = {
3699                let state = self.state.lock();
3700                let mut next_realtime = None;
3701                let mut next_playback = None;
3702                for track in state.tracks.values() {
3703                    let t = track.lock();
3704                    if t.audio.finished {
3705                        continue;
3706                    }
3707                    let needs_refill_event = t.hybrid_needs_refill();
3708                    if self.hybrid_enabled
3709                        && !t.is_realtime_domain()
3710                        && !needs_refill_event
3711                        && t.try_consume_hybrid_playback_cycle()
3712                    {
3713                        continue;
3714                    }
3715                    finished = false;
3716                    if t.audio.processing || !t.audio.ready() {
3717                        continue;
3718                    }
3719                    if t.is_realtime_domain() {
3720                        if next_realtime.is_none() {
3721                            next_realtime = Some(track.clone());
3722                        }
3723                    } else if next_playback.is_none() {
3724                        next_playback = Some(track.clone());
3725                    }
3726                }
3727                if next_realtime.is_none()
3728                    && next_playback.is_some()
3729                    && refill_dispatched >= self.refill_budget_per_pass
3730                {
3731                    self.refill_budget_throttle_count =
3732                        self.refill_budget_throttle_count.saturating_add(1);
3733                }
3734                next_realtime.or(next_playback)
3735            };
3736
3737            let Some(track) = next_track else {
3738                if dispatched > 0 {
3739                    tracing::info!("send_tracks dispatched {} tracks", dispatched);
3740                }
3741                return finished;
3742            };
3743            let worker_class = {
3744                let t = track.lock();
3745                if t.is_realtime_domain() {
3746                    WorkerClass::Realtime
3747                } else {
3748                    WorkerClass::Refill
3749                }
3750            };
3751            let worker_index = if !self.hybrid_enabled {
3752                // When hybrid buffering is disabled, any track can use any worker.
3753                self.take_ready_worker_index(WorkerClass::Realtime)
3754                    .or_else(|| self.take_ready_worker_index(WorkerClass::Refill))
3755            } else if let Some(index) = self.take_ready_worker_index(worker_class) {
3756                Some(index)
3757            } else if matches!(worker_class, WorkerClass::Realtime)
3758                && self.realtime_fallback_enabled
3759                && realtime_fallback_dispatched < self.realtime_fallback_budget_per_pass
3760            {
3761                self.take_ready_worker_index(WorkerClass::Refill)
3762            } else {
3763                None
3764            };
3765            let Some(worker_index) = worker_index else {
3766                self.force_stalled_track_completions();
3767                if dispatched > 0 {
3768                    tracing::info!(
3769                        "send_tracks dispatched {} tracks (no more workers)",
3770                        dispatched
3771                    );
3772                }
3773                return false;
3774            };
3775
3776            let t = track.lock();
3777            if t.audio.finished || t.audio.processing || !t.audio.ready() {
3778                continue;
3779            }
3780            if self.hybrid_enabled && matches!(worker_class, WorkerClass::Refill) {
3781                // Consume wakeup only when we are actually dispatching refill work.
3782                let _ = t.hybrid_take_refill_wakeup();
3783            }
3784            dispatched += 1;
3785            if matches!(worker_class, WorkerClass::Refill) {
3786                refill_dispatched = refill_dispatched.saturating_add(1);
3787            } else if !matches!(
3788                self.worker_classes
3789                    .get(worker_index)
3790                    .copied()
3791                    .unwrap_or(WorkerClass::Realtime),
3792                WorkerClass::Realtime
3793            ) {
3794                realtime_fallback_dispatched = realtime_fallback_dispatched.saturating_add(1);
3795                self.realtime_fallback_dispatch_count =
3796                    self.realtime_fallback_dispatch_count.saturating_add(1);
3797            }
3798            t.set_transport_sample(self.transport_sample);
3799            t.set_loop_config(self.loop_enabled, self.loop_range_samples);
3800            t.set_transport_timing(self.tempo_bpm, self.tsig_num, self.tsig_denom);
3801            if self.hybrid_enabled {
3802                let low_watermark = if self.hybrid_low_watermark_frames > 0 {
3803                    self.hybrid_low_watermark_frames
3804                } else {
3805                    self.current_cycle_samples().saturating_mul(4).max(1)
3806                };
3807                let realtime_frames = if self.hybrid_realtime_frames > 0 {
3808                    self.hybrid_realtime_frames
3809                } else {
3810                    self.current_cycle_samples().max(1)
3811                };
3812                let playback_frames = if self.hybrid_playback_frames > 0 {
3813                    self.hybrid_playback_frames
3814                } else {
3815                    self.current_cycle_samples().max(1)
3816                };
3817                t.configure_hybrid_timing(realtime_frames, low_watermark, playback_frames);
3818            }
3819            t.process_epoch = self.track_process_epoch;
3820
3821            t.set_clip_playback_enabled(self.clip_playback_enabled && self.playing);
3822
3823            t.set_record_tap_enabled(self.playing && self.record_enabled);
3824            t.audio.processing = true;
3825            self.track_processing_started_at
3826                .insert(t.name.clone(), Instant::now());
3827            let worker = &self.workers[worker_index];
3828            if let Err(e) = worker.tx.send(Message::ProcessTrack(track.clone())).await {
3829                t.audio.processing = false;
3830                self.track_processing_started_at.remove(&t.name);
3831                self.notify_clients(Err(format!("Failed to send track to worker: {}", e)))
3832                    .await;
3833            }
3834        }
3835    }
3836
3837    async fn on_all_tracks_finished(&mut self) {
3838        if self.transport_restart_pending {
3839            let state = self.state.lock();
3840            for track in state.tracks.values() {
3841                track.lock().take_hw_midi_out_events();
3842            }
3843        } else if self.hw_worker.is_some() {
3844            self.active_hw_notes_cycle_start = self.active_hw_notes_by_track.clone();
3845            let mut out_events = self.collect_hw_midi_output_events_by_device();
3846            if self.loop_enabled
3847                && let Some((_, loop_end)) = self.loop_range_samples
3848            {
3849                let cycle_end = self
3850                    .transport_sample
3851                    .saturating_add(self.current_cycle_samples());
3852                if self.transport_sample < loop_end && cycle_end > loop_end {
3853                    let wrap_frame = loop_end
3854                        .saturating_sub(self.transport_sample)
3855                        .min(self.current_cycle_samples())
3856                        as u32;
3857                    out_events.extend(self.note_off_events_for_active_snapshot(
3858                        &self.active_hw_notes_cycle_start,
3859                        wrap_frame,
3860                    ));
3861                    out_events.sort_by(|a, b| {
3862                        a.event
3863                            .frame
3864                            .cmp(&b.event.frame)
3865                            .then_with(|| a.device.cmp(&b.device))
3866                    });
3867                }
3868            }
3869            self.pending_hw_midi_out_events_by_device.extend(out_events);
3870        } else {
3871            self.pending_hw_midi_out_events = self.collect_hw_midi_output_events();
3872        }
3873        self.request_hw_cycle().await;
3874    }
3875
3876    fn take_ready_worker_index(&mut self, class: WorkerClass) -> Option<usize> {
3877        let queue = match class {
3878            WorkerClass::Realtime => &mut self.ready_realtime_workers,
3879            WorkerClass::Refill => &mut self.ready_refill_workers,
3880        };
3881        while !queue.is_empty() {
3882            let worker_index = queue.remove(0);
3883            if worker_index < self.workers.len() {
3884                return Some(worker_index);
3885            }
3886        }
3887        None
3888    }
3889
3890    fn push_ready_worker(&mut self, worker_index: usize) {
3891        match self
3892            .worker_classes
3893            .get(worker_index)
3894            .copied()
3895            .unwrap_or(WorkerClass::Refill)
3896        {
3897            WorkerClass::Realtime => self.ready_realtime_workers.push(worker_index),
3898            WorkerClass::Refill => self.ready_refill_workers.push(worker_index),
3899        }
3900    }
3901
3902    async fn publish_track_meters(&mut self) {
3903        if !self.should_publish_track_meters() {
3904            return;
3905        }
3906        let tracks: Vec<(String, Arc<UnsafeMutex<Box<Track>>>)> = self
3907            .state
3908            .lock()
3909            .tracks
3910            .iter()
3911            .map(|(name, track)| (name.clone(), track.clone()))
3912            .collect();
3913        let mut snapshot = Vec::with_capacity(tracks.len());
3914        for (name, track) in &tracks {
3915            let linear = self
3916                .track_meter_linear_by_track
3917                .get(name)
3918                .cloned()
3919                .unwrap_or_else(|| track.lock().output_meter_linear());
3920            let output_db = linear
3921                .iter()
3922                .copied()
3923                .map(Self::meter_linear_to_db)
3924                .collect::<Vec<_>>();
3925            snapshot.push((name.clone(), output_db));
3926        }
3927        self.latest_track_meter_snapshot = Arc::new(snapshot);
3928        let meters = self.collect_changed_track_meters(&tracks);
3929        for (track_name, output_db) in meters {
3930            self.notify_clients(Ok(Action::TrackMeters {
3931                track_name,
3932                output_db,
3933            }))
3934            .await;
3935        }
3936    }
3937
3938    fn reset_meters_after_stop(&mut self) {
3939        self.last_hw_out_meter_publish = None;
3940        self.last_track_meter_publish = None;
3941        self.hw_out_peak_hold_linear.fill(0.0);
3942        #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
3943        {
3944            self.last_hw_out_meter_linear.clear();
3945        }
3946        let hw_channels = self.latest_hw_out_meter_db.len();
3947        self.latest_hw_out_meter_db = Arc::new(vec![-90.0; hw_channels]);
3948
3949        let tracks: Vec<(String, Arc<UnsafeMutex<Box<Track>>>)> = self
3950            .state
3951            .lock()
3952            .tracks
3953            .iter()
3954            .map(|(name, track)| (name.clone(), track.clone()))
3955            .collect();
3956        self.track_meter_linear_by_track.clear();
3957        let mut snapshot = Vec::with_capacity(tracks.len());
3958        for (name, track) in tracks {
3959            let t = track.lock();
3960            t.clear_output_meters();
3961            let width = t.output_meter_linear().len();
3962            let zero_linear = vec![0.0; width];
3963            self.track_meter_linear_by_track
3964                .insert(name.clone(), zero_linear);
3965            snapshot.push((name, vec![-90.0; width]));
3966        }
3967        self.latest_track_meter_snapshot = Arc::new(snapshot);
3968    }
3969
3970    pub fn check_if_leads_to_kind(
3971        &self,
3972        kind: Kind,
3973        current_track_name: &str,
3974        target_track_name: &str,
3975    ) -> bool {
3976        routing::would_create_cycle(
3977            &target_track_name.to_string(),
3978            &current_track_name.to_string(),
3979            |track_name| self.connected_neighbors(kind, track_name),
3980        )
3981    }
3982
3983    fn connected_neighbors(&self, kind: Kind, current_track_name: &str) -> Vec<String> {
3984        let state = self.state.lock();
3985        let mut found_neighbors = Vec::new();
3986
3987        if let Some(current_track_handle) = state.tracks.get(current_track_name) {
3988            let current_track = current_track_handle.lock();
3989
3990            match kind {
3991                Kind::Audio => {
3992                    for out_port in &current_track.audio.outs {
3993                        let conns = out_port.connections.lock();
3994                        for conn in conns.iter() {
3995                            for (name, next_track_handle) in &state.tracks {
3996                                let next_track = next_track_handle.lock();
3997                                let is_connected =
3998                                    next_track.audio.ins.iter().any(|ins_port| {
3999                                        Arc::ptr_eq(&ins_port.buffer, &conn.buffer)
4000                                    });
4001
4002                                if is_connected {
4003                                    found_neighbors.push(name.clone());
4004                                }
4005                            }
4006                        }
4007                    }
4008                }
4009                Kind::MIDI => {
4010                    for out_port in &current_track.midi.outs {
4011                        let conns = out_port.lock().connections.clone();
4012                        for conn in conns.iter() {
4013                            for (name, next_track_handle) in &state.tracks {
4014                                let next_track = next_track_handle.lock();
4015                                let is_connected = next_track
4016                                    .midi
4017                                    .ins
4018                                    .iter()
4019                                    .any(|ins_port| Arc::ptr_eq(ins_port, conn));
4020
4021                                if is_connected {
4022                                    found_neighbors.push(name.clone());
4023                                }
4024                            }
4025                        }
4026                    }
4027                }
4028            }
4029        }
4030        found_neighbors
4031    }
4032
4033    async fn handle_request(&mut self, a: Action) {
4034        match a {
4035            Action::Undo => {
4036                let actions = match self.history.undo() {
4037                    Some(actions) => actions,
4038                    None => {
4039                        self.notify_clients(Ok(Action::Undo)).await;
4040                        self.notify_clients(Ok(Action::HistoryState {
4041                            dirty: self.history.is_dirty(),
4042                        }))
4043                        .await;
4044                        return;
4045                    }
4046                };
4047
4048                let was_suspended = self.history_suspended;
4049                self.history_suspended = true;
4050                for action in actions {
4051                    self.handle_request_inner(action, false).await;
4052                }
4053                self.history_suspended = was_suspended;
4054                self.notify_clients(Ok(Action::Undo)).await;
4055                self.notify_clients(Ok(Action::HistoryState {
4056                    dirty: self.history.is_dirty(),
4057                }))
4058                .await;
4059            }
4060            Action::Redo => {
4061                let actions = match self.history.redo() {
4062                    Some(actions) => actions,
4063                    None => {
4064                        self.notify_clients(Ok(Action::Redo)).await;
4065                        self.notify_clients(Ok(Action::HistoryState {
4066                            dirty: self.history.is_dirty(),
4067                        }))
4068                        .await;
4069                        return;
4070                    }
4071                };
4072
4073                let was_suspended = self.history_suspended;
4074                self.history_suspended = true;
4075                for action in actions {
4076                    self.handle_request_inner(action, false).await;
4077                }
4078                self.history_suspended = was_suspended;
4079                self.notify_clients(Ok(Action::Redo)).await;
4080                self.notify_clients(Ok(Action::HistoryState {
4081                    dirty: self.history.is_dirty(),
4082                }))
4083                .await;
4084            }
4085            Action::ApplyGroupedActions(actions) => {
4086                self.handle_request_inner(Action::BeginHistoryGroup, true)
4087                    .await;
4088                for action in actions {
4089                    self.handle_request_inner(action, true).await;
4090                }
4091                self.handle_request_inner(Action::EndHistoryGroup, true)
4092                    .await;
4093            }
4094            other => {
4095                self.handle_request_inner(other, true).await;
4096            }
4097        }
4098    }
4099
4100    async fn handle_request_inner(&mut self, action_to_process: Action, record_history: bool) {
4101        let a = action_to_process.clone();
4102        let suppress_timing_history = self.playing
4103            && matches!(
4104                &action_to_process,
4105                Action::SetTempo(_) | Action::SetTimeSignature { .. }
4106            );
4107        let mut extra_inverse_actions: Vec<Action> = Vec::new();
4108        if record_history
4109            && !self.history_suspended
4110            && let Action::RemoveTrack(ref track_name) = action_to_process
4111        {
4112            for route in self
4113                .midi_hw_in_routes
4114                .iter()
4115                .filter(|route| &route.to_track == track_name)
4116            {
4117                extra_inverse_actions.push(Action::Connect {
4118                    from_track: format!("midi:hw:in:{}", route.device),
4119                    from_port: 0,
4120                    to_track: route.to_track.clone(),
4121                    to_port: route.to_port,
4122                    kind: Kind::MIDI,
4123                });
4124            }
4125            for route in self
4126                .midi_hw_out_routes
4127                .iter()
4128                .filter(|route| &route.from_track == track_name)
4129            {
4130                extra_inverse_actions.push(Action::Connect {
4131                    from_track: route.from_track.clone(),
4132                    from_port: route.from_port,
4133                    to_track: format!("midi:hw:out:{}", route.device),
4134                    to_port: 0,
4135                    kind: Kind::MIDI,
4136                });
4137            }
4138        }
4139        if record_history
4140            && !self.history_suspended
4141            && matches!(action_to_process, Action::ClearAllMidiLearnBindings)
4142        {
4143            if let Some(binding) = self.global_midi_learn_play_pause.clone() {
4144                extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
4145                    target: crate::message::GlobalMidiLearnTarget::PlayPause,
4146                    binding: Some(binding),
4147                });
4148            }
4149            if let Some(binding) = self.global_midi_learn_stop.clone() {
4150                extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
4151                    target: crate::message::GlobalMidiLearnTarget::Stop,
4152                    binding: Some(binding),
4153                });
4154            }
4155            if let Some(binding) = self.global_midi_learn_record_toggle.clone() {
4156                extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
4157                    target: crate::message::GlobalMidiLearnTarget::RecordToggle,
4158                    binding: Some(binding),
4159                });
4160            }
4161        }
4162        let mut inverse_actions = if record_history
4163            && !suppress_timing_history
4164            && should_record(&action_to_process)
4165            && !self.history_suspended
4166        {
4167            let state = self.state.lock();
4168            create_inverse_actions(&action_to_process, state).map(|mut actions| {
4169                actions.extend(extra_inverse_actions);
4170                actions
4171            })
4172        } else {
4173            None
4174        };
4175        if record_history && !suppress_timing_history && !self.history_suspended {
4176            match &action_to_process {
4177                Action::SetTempo(_) => {
4178                    inverse_actions = Some(vec![Action::SetTempo(self.tempo_bpm)]);
4179                }
4180                Action::SetLoopEnabled(_) => {
4181                    inverse_actions = Some(vec![Action::SetLoopEnabled(self.loop_enabled)]);
4182                }
4183                Action::SetLoopRange(_) => {
4184                    inverse_actions = Some(vec![
4185                        Action::SetLoopRange(self.loop_range_samples),
4186                        Action::SetLoopEnabled(self.loop_enabled),
4187                    ]);
4188                }
4189                Action::SetPunchEnabled(_) => {
4190                    inverse_actions = Some(vec![Action::SetPunchEnabled(self.punch_enabled)]);
4191                }
4192                Action::SetPunchRange(_) => {
4193                    inverse_actions = Some(vec![
4194                        Action::SetPunchRange(self.punch_range_samples),
4195                        Action::SetPunchEnabled(self.punch_enabled),
4196                    ]);
4197                }
4198                Action::SetMetronomeEnabled(_) => {
4199                    inverse_actions =
4200                        Some(vec![Action::SetMetronomeEnabled(self.metronome_enabled)]);
4201                }
4202                Action::SetTimeSignature { .. } => {
4203                    inverse_actions = Some(vec![Action::SetTimeSignature {
4204                        numerator: self.tsig_num,
4205                        denominator: self.tsig_denom,
4206                    }]);
4207                }
4208                Action::SetClipPlaybackEnabled(_) => {
4209                    inverse_actions = Some(vec![Action::SetClipPlaybackEnabled(
4210                        self.clip_playback_enabled,
4211                    )]);
4212                }
4213                Action::SetRecordEnabled(_) => {
4214                    inverse_actions = Some(vec![Action::SetRecordEnabled(self.record_enabled)]);
4215                }
4216                Action::SetGlobalMidiLearnBinding { target, .. } => {
4217                    let binding = match target {
4218                        crate::message::GlobalMidiLearnTarget::PlayPause => {
4219                            self.global_midi_learn_play_pause.clone()
4220                        }
4221                        crate::message::GlobalMidiLearnTarget::Stop => {
4222                            self.global_midi_learn_stop.clone()
4223                        }
4224                        crate::message::GlobalMidiLearnTarget::RecordToggle => {
4225                            self.global_midi_learn_record_toggle.clone()
4226                        }
4227                    };
4228                    inverse_actions = Some(vec![Action::SetGlobalMidiLearnBinding {
4229                        target: *target,
4230                        binding,
4231                    }]);
4232                }
4233                _ => {}
4234            }
4235        }
4236
4237        match action_to_process {
4238            Action::Play => {
4239                tracing::info!(
4240                    "Action::Play pressed, transport_sample={}",
4241                    self.transport_sample
4242                );
4243                self.playing = true;
4244                self.transport_restart_pending = true;
4245                self.invalidate_track_cycle_state();
4246                if let Some(driver) = self.hw_driver.as_mut() {
4247                    driver.lock().set_playing(true);
4248                }
4249                #[cfg(unix)]
4250                if let Some(jack) = &self.jack_runtime
4251                    && let Err(e) = jack.lock().transport_start()
4252                {
4253                    self.notify_clients(Err(e)).await;
4254                }
4255                self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
4256                    .await;
4257                self.preload_track_clips().await;
4258                let send_result = self.send_tracks().await;
4259                tracing::info!("send_tracks after Play returned finished={}", send_result);
4260                if !self.awaiting_hwfinished
4261                    && !self.handling_hwfinished
4262                    && send_result
4263                    && self.hw_worker.is_some()
4264                {
4265                    self.transport_restart_pending = false;
4266                    self.request_hw_cycle().await;
4267                }
4268            }
4269            Action::Pause => {
4270                self.clip_playback_enabled = false;
4271                for track in self.state.lock().tracks.values() {
4272                    track.lock().set_clip_playback_enabled(false);
4273                }
4274                if !self.playing {
4275                    self.playing = true;
4276                    self.transport_restart_pending = true;
4277                    self.invalidate_track_cycle_state();
4278                    if let Some(driver) = self.hw_driver.as_mut() {
4279                        driver.lock().set_playing(true);
4280                    }
4281                    #[cfg(unix)]
4282                    if let Some(jack) = &self.jack_runtime
4283                        && let Err(e) = jack.lock().transport_start()
4284                    {
4285                        self.notify_clients(Err(e)).await;
4286                    }
4287                    self.preload_track_clips().await;
4288                    if !self.awaiting_hwfinished
4289                        && !self.handling_hwfinished
4290                        && self.send_tracks().await
4291                        && self.hw_worker.is_some()
4292                    {
4293                        self.transport_restart_pending = false;
4294                        self.request_hw_cycle().await;
4295                    }
4296                }
4297                self.notify_clients(Ok(Action::Pause)).await;
4298                self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
4299                    .await;
4300            }
4301            Action::Stop => {
4302                self.playing = false;
4303                self.transport_panic_flush_pending = false;
4304                self.transport_restart_pending = false;
4305                self.invalidate_track_cycle_state();
4306                if let Some(driver) = self.hw_driver.as_mut() {
4307                    driver.lock().set_playing(false);
4308                }
4309                #[cfg(unix)]
4310                if let Some(jack) = &self.jack_runtime
4311                    && let Err(e) = jack.lock().transport_stop()
4312                {
4313                    self.notify_clients(Err(e)).await;
4314                }
4315                let panic_events = self.note_off_events_for_all_active_tracks();
4316                if let Some(worker) = &self.hw_worker {
4317                    if !panic_events.is_empty()
4318                        && let Err(e) = worker.tx.send(Message::HWMidiOutEvents(panic_events)).await
4319                    {
4320                        error!("Error sending stop MIDI panic events {e}");
4321                    }
4322                } else {
4323                    self.pending_hw_midi_out_events_by_device
4324                        .extend(panic_events);
4325                }
4326                self.reset_meters_after_stop();
4327                self.flush_recordings().await;
4328                self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
4329                    .await;
4330            }
4331            Action::JumpToEnd => {
4332                self.transport_sample = self.normalize_transport_sample(self.session_end_sample());
4333                self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
4334                    .await;
4335            }
4336            Action::Panic => {
4337                let panic_events = self.panic_events_for_all_hw_midi_outputs();
4338                if let Some(worker) = &self.hw_worker {
4339                    if !panic_events.is_empty() {
4340                        if let Err(e) = worker.tx.send(Message::ClearHWMidiOutEvents).await {
4341                            error!("Error clearing HW MIDI queue for panic {e}");
4342                        }
4343                        self.midi_hub
4344                            .lock()
4345                            .write_events_blocking(&panic_events, Duration::from_millis(250));
4346                    }
4347                } else if !panic_events.is_empty() {
4348                    self.pending_hw_midi_out_events_by_device
4349                        .extend(panic_events);
4350                }
4351            }
4352            Action::SetClipPlaybackEnabled(enabled) => {
4353                self.clip_playback_enabled = enabled;
4354                for track in self.state.lock().tracks.values() {
4355                    track.lock().set_clip_playback_enabled(enabled);
4356                }
4357            }
4358            Action::TransportPosition(sample) => {
4359                self.transport_sample = self.normalize_transport_sample(sample);
4360                #[cfg(unix)]
4361                if let Some(jack) = &self.jack_runtime
4362                    && let Err(e) = jack.lock().transport_locate(self.transport_sample)
4363                {
4364                    self.notify_clients(Err(e)).await;
4365                }
4366                if self.playing {
4367                    self.transport_restart_pending = true;
4368                    self.invalidate_track_cycle_state();
4369                    self.transport_panic_flush_pending = self.hw_worker.is_some();
4370                    self.clear_hw_midi_output_state(true).await;
4371                    if !self.awaiting_hwfinished && !self.handling_hwfinished {
4372                        if self.hw_worker.is_some() {
4373                            self.request_hw_cycle().await;
4374                        } else if self.send_tracks().await {
4375                            self.transport_restart_pending = false;
4376                            self.request_hw_cycle().await;
4377                        }
4378                    }
4379                }
4380            }
4381            Action::SetLoopEnabled(enabled) => {
4382                self.loop_enabled = enabled && self.loop_range_samples.is_some();
4383            }
4384            Action::SetLoopRange(range) => {
4385                self.loop_range_samples = range.and_then(|(start, end)| {
4386                    if end > start {
4387                        Some((start, end))
4388                    } else {
4389                        None
4390                    }
4391                });
4392                self.loop_enabled = self.loop_range_samples.is_some();
4393                if self.loop_enabled
4394                    && let Some((loop_start, loop_end)) = self.loop_range_samples
4395                    && self.transport_sample >= loop_end
4396                {
4397                    self.transport_sample = loop_start;
4398                    self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
4399                        .await;
4400                }
4401            }
4402            Action::SetPunchEnabled(enabled) => {
4403                self.punch_enabled = enabled && self.punch_range_samples.is_some();
4404            }
4405            Action::SetPunchRange(range) => {
4406                self.punch_range_samples = range.and_then(|(start, end)| {
4407                    if end > start {
4408                        Some((start, end))
4409                    } else {
4410                        None
4411                    }
4412                });
4413                self.punch_enabled = self.punch_range_samples.is_some();
4414            }
4415            Action::SetMetronomeEnabled(enabled) => {
4416                self.metronome_enabled = enabled;
4417                if enabled {
4418                    self.ensure_metronome_track().await;
4419                }
4420                if let Some(track) = self.state.lock().tracks.get(Self::METRONOME_TRACK).cloned() {
4421                    track.lock().set_metronome_enabled(enabled);
4422                }
4423            }
4424            Action::SetTempo(bpm) => {
4425                self.tempo_bpm = bpm.max(1.0);
4426            }
4427            Action::SetTimeSignature {
4428                numerator,
4429                denominator,
4430            } => {
4431                self.tsig_num = numerator.max(1);
4432                self.tsig_denom = denominator.max(1);
4433            }
4434            Action::SetOscEnabled(enabled) => {
4435                if let Err(err) = self.set_osc_enabled_with(enabled, OscServer::start) {
4436                    self.notify_clients(Err(err)).await;
4437                }
4438            }
4439            Action::SetRecordEnabled(enabled) => {
4440                self.record_enabled = enabled;
4441                if !enabled {
4442                    if self.awaiting_hwfinished {
4443                        self.append_recorded_cycle();
4444                    }
4445                    self.flush_recordings().await;
4446                } else if self.session_dir.is_none() {
4447                    self.notify_clients(Err(
4448                        "Recording enabled but session path is not set".to_string()
4449                    ))
4450                    .await;
4451                }
4452            }
4453            Action::BeginHistoryGroup if self.history_group.is_none() => {
4454                self.history_group = Some(UndoEntry {
4455                    forward_actions: vec![],
4456                    inverse_actions: vec![],
4457                });
4458            }
4459            Action::EndHistoryGroup => {
4460                if let Some(mut group) = self.history_group.take()
4461                    && !group.forward_actions.is_empty()
4462                    && !group.inverse_actions.is_empty()
4463                {
4464                    let mut add_tracks = Vec::new();
4465                    let mut connections = Vec::new();
4466                    let mut rest = Vec::new();
4467                    for action in group.inverse_actions {
4468                        if matches!(action, Action::AddTrack { .. }) {
4469                            add_tracks.push(action);
4470                        } else if matches!(action, Action::Connect { .. }) {
4471                            connections.push(action);
4472                        } else {
4473                            rest.push(action);
4474                        }
4475                    }
4476                    group.inverse_actions = add_tracks;
4477                    group.inverse_actions.extend(rest);
4478                    group.inverse_actions.extend(connections);
4479                    self.history.record(group);
4480                }
4481            }
4482            Action::SetSessionPath(ref path) => {
4483                self.session_dir = Some(Path::new(path).to_path_buf());
4484                self.ensure_session_subdirs();
4485                #[cfg(all(unix, not(target_os = "macos")))]
4486                let _lv2_dir = self.session_plugins_dir();
4487                for track in self.state.lock().tracks.values() {
4488                    track.lock().set_session_base_dir(self.session_dir.clone());
4489                }
4490            }
4491            Action::MarkHistorySavePoint => {
4492                self.history.mark_save_point();
4493                self.notify_clients(Ok(Action::HistoryState {
4494                    dirty: self.history.is_dirty(),
4495                }))
4496                .await;
4497            }
4498            Action::ClearHistory => {
4499                self.history.clear();
4500                self.history.mark_save_point();
4501            }
4502            Action::BeginSessionRestore => {
4503                self.history_suspended = true;
4504                self.history.clear();
4505            }
4506            Action::EndSessionRestore => {
4507                self.history.clear();
4508                self.history_suspended = false;
4509                self.preload_track_clips_spawn();
4510            }
4511            Action::Quit => {
4512                self.flush_recordings().await;
4513                self.ready_realtime_workers.clear();
4514                self.ready_refill_workers.clear();
4515                while !self.workers.is_empty() {
4516                    let worker = self.workers.remove(0);
4517                    worker
4518                        .tx
4519                        .send(Message::Request(a.clone()))
4520                        .await
4521                        .expect("Failed sending quit message to worker");
4522                    worker
4523                        .handle
4524                        .await
4525                        .expect("Failed waiting for worker to quit");
4526                }
4527
4528                if let Some(worker) = self.hw_worker.take() {
4529                    let mut panic_events = self.note_off_events_for_all_active_tracks();
4530                    panic_events.extend(self.panic_events_for_all_hw_midi_outputs());
4531                    if !panic_events.is_empty() {
4532                        if let Err(e) = worker.tx.send(Message::ClearHWMidiOutEvents).await {
4533                            error!("Error clearing HW MIDI queue during quit {e}");
4534                        }
4535                        self.midi_hub
4536                            .lock()
4537                            .write_events_blocking(&panic_events, Duration::from_millis(250));
4538                    }
4539                    worker
4540                        .tx
4541                        .send(Message::Request(a.clone()))
4542                        .await
4543                        .expect("Failed sending quit message to HW worker");
4544                    worker
4545                        .handle
4546                        .await
4547                        .expect("Failed waiting for HW worker to quit");
4548                }
4549                #[cfg(unix)]
4550                {
4551                    self.jack_runtime = None;
4552                }
4553            }
4554            Action::AddTrack {
4555                ref name,
4556                audio_ins,
4557                midi_ins,
4558                audio_outs,
4559                midi_outs,
4560            } => {
4561                let tracks = &mut self.state.lock().tracks;
4562                if tracks.contains_key(name) {
4563                    self.notify_clients(Err(format!("Track {} already exists", name)))
4564                        .await;
4565                    return;
4566                }
4567                let maybe_hw = if let Some(oss) = &self.hw_driver {
4568                    let hw = oss.lock();
4569                    Some((hw.cycle_samples(), hw.sample_rate() as f64))
4570                } else {
4571                    #[cfg(unix)]
4572                    if let Some(jack) = &self.jack_runtime {
4573                        let j = jack.lock();
4574                        Some((j.buffer_size, j.sample_rate as f64))
4575                    } else {
4576                        None
4577                    }
4578                    #[cfg(not(unix))]
4579                    None
4580                };
4581
4582                if let Some((chsamples, sample_rate)) = maybe_hw {
4583                    tracks.insert(
4584                        name.clone(),
4585                        Arc::new(UnsafeMutex::new(Box::new(Track::new(
4586                            name.clone(),
4587                            audio_ins,
4588                            audio_outs,
4589                            midi_ins,
4590                            midi_outs,
4591                            chsamples,
4592                            sample_rate,
4593                        )))),
4594                    );
4595                    if let Some(track) = tracks.get(name) {
4596                        let t = track.lock();
4597                        t.ensure_default_audio_passthrough();
4598                        t.ensure_default_midi_passthrough();
4599                        t.set_clip_playback_enabled(self.clip_playback_enabled);
4600                        t.set_transport_timing(self.tempo_bpm, self.tsig_num, self.tsig_denom);
4601                        t.set_session_base_dir(self.session_dir.clone());
4602                        t.set_hybrid_enabled(self.hybrid_enabled);
4603                    }
4604                } else {
4605                    self.notify_clients(Err(
4606                        "Engine needs to open audio device before adding audio track".to_string(),
4607                    ))
4608                    .await;
4609                }
4610            }
4611            Action::TrackAddAudioInput(ref name) => {
4612                let track = match self.track_handle_or_err(name) {
4613                    Ok(track) => track,
4614                    Err(e) => {
4615                        self.notify_clients(Err(e)).await;
4616                        return;
4617                    }
4618                };
4619                if let Err(e) = track.lock().add_audio_input() {
4620                    self.notify_clients(Err(e)).await;
4621                    return;
4622                }
4623            }
4624            Action::TrackAddAudioOutput(ref name) => {
4625                let track = match self.track_handle_or_err(name) {
4626                    Ok(track) => track,
4627                    Err(e) => {
4628                        self.notify_clients(Err(e)).await;
4629                        return;
4630                    }
4631                };
4632                if let Err(e) = track.lock().add_audio_output() {
4633                    self.notify_clients(Err(e)).await;
4634                    return;
4635                }
4636            }
4637            Action::TrackRemoveAudioInput(ref name) => {
4638                let track = match self.track_handle_or_err(name) {
4639                    Ok(track) => track,
4640                    Err(e) => {
4641                        self.notify_clients(Err(e)).await;
4642                        return;
4643                    }
4644                };
4645                if let Err(e) = track.lock().remove_audio_input() {
4646                    self.notify_clients(Err(e)).await;
4647                    return;
4648                }
4649            }
4650            Action::TrackRemoveAudioOutput(ref name) => {
4651                let track = match self.track_handle_or_err(name) {
4652                    Ok(track) => track,
4653                    Err(e) => {
4654                        self.notify_clients(Err(e)).await;
4655                        return;
4656                    }
4657                };
4658                let (hw_outputs, track_inputs) = {
4659                    let state = self.state.lock();
4660                    let hw_outputs = self.all_hw_output_audio_ports();
4661                    let track_inputs = state
4662                        .tracks
4663                        .iter()
4664                        .filter(|(track_name, _)| *track_name != name)
4665                        .flat_map(|(_, handle)| handle.lock().audio.ins.clone())
4666                        .collect::<Vec<_>>();
4667                    (hw_outputs, track_inputs)
4668                };
4669                if let Err(e) = track.lock().remove_audio_output(&hw_outputs, &track_inputs) {
4670                    self.notify_clients(Err(e)).await;
4671                    return;
4672                }
4673            }
4674            Action::RenameTrack {
4675                ref old_name,
4676                ref new_name,
4677            } => {
4678                if self.state.lock().tracks.contains_key(new_name) {
4679                    self.notify_clients(Err(format!("Track '{}' already exists", new_name)))
4680                        .await;
4681                    return;
4682                }
4683
4684                let Some(track) = self.state.lock().tracks.remove(old_name) else {
4685                    self.notify_clients(Err(format!("Track '{}' not found", old_name)))
4686                        .await;
4687                    return;
4688                };
4689
4690                track.lock().name = new_name.clone();
4691                self.state.lock().tracks.insert(new_name.clone(), track);
4692                for other in self.state.lock().tracks.values() {
4693                    let other = other.lock();
4694                    if other.vca_master.as_deref() == Some(old_name.as_str()) {
4695                        other.set_vca_master(Some(new_name.clone()));
4696                    }
4697                    if other.parent_track.as_deref() == Some(old_name.as_str()) {
4698                        other.parent_track = Some(new_name.clone());
4699                    }
4700                }
4701
4702                if let Some(recording) = self.audio_recordings.remove(old_name) {
4703                    self.audio_recordings.insert(new_name.clone(), recording);
4704                }
4705                if let Some(recording) = self.midi_recordings.remove(old_name) {
4706                    self.midi_recordings.insert(new_name.clone(), recording);
4707                }
4708
4709                for route in &mut self.midi_hw_in_routes {
4710                    if route.to_track == *old_name {
4711                        route.to_track = new_name.clone();
4712                    }
4713                }
4714                for route in &mut self.midi_hw_out_routes {
4715                    if route.from_track == *old_name {
4716                        route.from_track = new_name.clone();
4717                    }
4718                }
4719                if let Some((armed_track, target, device)) = self.pending_midi_learn.clone()
4720                    && armed_track == *old_name
4721                {
4722                    self.pending_midi_learn = Some((new_name.clone(), target, device));
4723                }
4724
4725                self.notify_clients(Ok(Action::RenameTrack {
4726                    old_name: old_name.clone(),
4727                    new_name: new_name.clone(),
4728                }))
4729                .await;
4730            }
4731            Action::RemoveTrack(ref name) => {
4732                // Clean up folder children before removing the track
4733                let children: Vec<String> = {
4734                    let state = self.state.lock();
4735                    state
4736                        .tracks
4737                        .iter()
4738                        .filter_map(|(n, t)| {
4739                            if t.lock().parent_track.as_deref() == Some(name.as_str()) {
4740                                Some(n.clone())
4741                            } else {
4742                                None
4743                            }
4744                        })
4745                        .collect()
4746                };
4747                if let Some(removed_track) = self.state.lock().tracks.get(name).cloned() {
4748                    for child_name in children {
4749                        if let Some(child) = self.state.lock().tracks.get(&child_name).cloned() {
4750                            let removed = removed_track.lock();
4751                            child.lock().disconnect_outputs_from_parent(removed);
4752                            child.lock().parent_track = None;
4753                        }
4754                    }
4755                }
4756                self.state.lock().tracks.remove(name);
4757                self.audio_recordings.remove(name);
4758                self.midi_recordings.remove(name);
4759                self.midi_hw_in_routes.retain(|r| r.to_track != *name);
4760                self.midi_hw_out_routes.retain(|r| r.from_track != *name);
4761                if self
4762                    .pending_midi_learn
4763                    .as_ref()
4764                    .is_some_and(|(track_name, _, _)| track_name == name)
4765                {
4766                    self.pending_midi_learn = None;
4767                }
4768                for track in self.state.lock().tracks.values() {
4769                    let track = track.lock();
4770                    if track.vca_master.as_deref() == Some(name.as_str()) {
4771                        track.set_vca_master(None);
4772                    }
4773                }
4774            }
4775            Action::TrackLevel(ref name, level) => {
4776                if name == "hw:out" {
4777                    self.hw_out_level_db = level;
4778                } else if let Some(track) = self.state.lock().tracks.get(name) {
4779                    let previous = track.lock().level();
4780                    track.lock().set_level(level);
4781                    let delta = level - previous;
4782                    if delta.abs() > f32::EPSILON {
4783                        for follower_name in self.vca_followers(name) {
4784                            if let Some(follower) = self.state.lock().tracks.get(&follower_name) {
4785                                let next = (follower.lock().level() + delta).clamp(-90.0, 20.0);
4786                                follower.lock().set_level(next);
4787                                self.notify_clients(Ok(Action::TrackLevel(
4788                                    follower_name.clone(),
4789                                    next,
4790                                )))
4791                                .await;
4792                            }
4793                        }
4794                    }
4795                }
4796            }
4797            Action::TrackBalance(ref name, balance) => {
4798                if name == "hw:out" {
4799                    self.hw_out_balance = balance.clamp(-1.0, 1.0);
4800                } else if let Some(track) = self.state.lock().tracks.get(name) {
4801                    track.lock().set_balance(balance);
4802                }
4803            }
4804            Action::TrackAutomationLevel(ref name, level) => {
4805                if let Some(track) = self.state.lock().tracks.get(name) {
4806                    let previous = track.lock().level();
4807                    track.lock().set_level(level);
4808                    let delta = level - previous;
4809                    if delta.abs() > f32::EPSILON {
4810                        for follower_name in self.vca_followers(name) {
4811                            if let Some(follower) = self.state.lock().tracks.get(&follower_name) {
4812                                let next = (follower.lock().level() + delta).clamp(-90.0, 20.0);
4813                                follower.lock().set_level(next);
4814                                self.notify_clients(Ok(Action::TrackAutomationLevel(
4815                                    follower_name.clone(),
4816                                    next,
4817                                )))
4818                                .await;
4819                            }
4820                        }
4821                    }
4822                }
4823            }
4824            Action::TrackAutomationBalance(ref name, balance) => {
4825                if let Some(track) = self.state.lock().tracks.get(name) {
4826                    track.lock().set_balance(balance);
4827                }
4828            }
4829            Action::TrackAutomationMute(ref name, muted) => {
4830                if let Some(track) = self.state.lock().tracks.get(name) {
4831                    track.lock().set_muted(muted);
4832                    for follower_name in self.vca_followers(name) {
4833                        if let Some(follower) = self.state.lock().tracks.get(&follower_name) {
4834                            follower.lock().set_muted(muted);
4835                            self.notify_clients(Ok(Action::TrackAutomationMute(
4836                                follower_name.clone(),
4837                                muted,
4838                            )))
4839                            .await;
4840                        }
4841                    }
4842                }
4843            }
4844            Action::RequestMeterSnapshot => {
4845                self.notify_clients(Ok(Action::MeterSnapshot {
4846                    hw_out_db: self.latest_hw_out_meter_db.clone(),
4847                    track_meters: self.latest_track_meter_snapshot.clone(),
4848                }))
4849                .await;
4850                return;
4851            }
4852            Action::TrackMeters { .. } => {}
4853            Action::MeterSnapshot { .. } => {}
4854            Action::TrackToggleArm(ref name) => {
4855                if self.reject_if_track_frozen(name, "arming/disarming").await {
4856                    return;
4857                }
4858                if let Some(track) = self.state.lock().tracks.get(name).cloned() {
4859                    track.lock().arm();
4860                    if !track.lock().armed && self.audio_recordings.contains_key(name) {
4861                        self.flush_track_recording(name).await;
4862                    }
4863                }
4864            }
4865            Action::TrackToggleMute(ref name) => {
4866                if name == "hw:out" {
4867                    self.hw_out_muted = !self.hw_out_muted;
4868                } else if let Some(track) = self.state.lock().tracks.get(name) {
4869                    track.lock().mute();
4870                    let muted = track.lock().muted;
4871                    for follower_name in self.vca_followers(name) {
4872                        if let Some(follower) = self.state.lock().tracks.get(&follower_name)
4873                            && follower.lock().muted != muted
4874                        {
4875                            follower.lock().set_muted(muted);
4876                            self.notify_clients(Ok(Action::TrackToggleMute(follower_name.clone())))
4877                                .await;
4878                        }
4879                    }
4880                }
4881            }
4882            Action::TrackTogglePhase(ref name) => {
4883                if let Some(track) = self.state.lock().tracks.get(name) {
4884                    track.lock().invert_phase();
4885                }
4886            }
4887            Action::TrackToggleSolo(ref name) => {
4888                if name == "hw:out" {
4889                    return;
4890                }
4891                if let Some(track) = self.state.lock().tracks.get(name) {
4892                    track.lock().solo();
4893                    let soloed = track.lock().soloed;
4894                    for follower_name in self.vca_followers(name) {
4895                        if let Some(follower) = self.state.lock().tracks.get(&follower_name)
4896                            && follower.lock().soloed != soloed
4897                        {
4898                            follower.lock().solo();
4899                            self.notify_clients(Ok(Action::TrackToggleSolo(follower_name.clone())))
4900                                .await;
4901                        }
4902                    }
4903                }
4904            }
4905            Action::TrackToggleMaster(ref name) => {
4906                if let Some(track) = self.state.lock().tracks.get(name) {
4907                    let blocked = {
4908                        let t = track.lock();
4909                        t.vca_master.is_some() || !self.vca_followers(name).is_empty()
4910                    };
4911                    if blocked {
4912                        self.notify_clients(Err(format!(
4913                            "Track '{}' cannot be promoted to Master while part of a VCA group",
4914                            name
4915                        )))
4916                        .await;
4917                        return;
4918                    }
4919                    track.lock().toggle_master();
4920                }
4921            }
4922            Action::TrackToggleInputMonitor(ref name) => {
4923                if let Some(track) = self.state.lock().tracks.get(name) {
4924                    track.lock().toggle_input_monitor();
4925                }
4926            }
4927            Action::TrackToggleDiskMonitor(ref name) => {
4928                if let Some(track) = self.state.lock().tracks.get(name) {
4929                    track.lock().toggle_disk_monitor();
4930                }
4931            }
4932            Action::TrackSetColor {
4933                ref track_name,
4934                color,
4935            } => {
4936                if let Some(track) = self.state.lock().tracks.get(track_name) {
4937                    track.lock().color = color;
4938                }
4939            }
4940            Action::TrackArmMidiLearn {
4941                ref track_name,
4942                target,
4943            } => {
4944                if let Err(e) = self.track_handle_or_err(track_name) {
4945                    self.notify_clients(Err(e)).await;
4946                    return;
4947                }
4948                self.pending_midi_learn = Some((track_name.clone(), target, None));
4949            }
4950            Action::GlobalArmMidiLearn { target } => {
4951                self.pending_global_midi_learn = Some(target);
4952            }
4953            Action::TrackSetMidiLearnBinding {
4954                ref track_name,
4955                target,
4956                ref binding,
4957            } => {
4958                if let Some(binding) = binding.as_ref() {
4959                    let conflicts = self.midi_learn_slot_conflicts(
4960                        binding,
4961                        Some(MidiLearnSlot::Track(track_name.clone(), target)),
4962                    );
4963                    if !conflicts.is_empty() {
4964                        self.notify_clients(Err(format!(
4965                            "MIDI learn conflict for '{}' {:?}: {}",
4966                            track_name,
4967                            target,
4968                            conflicts.join(", ")
4969                        )))
4970                        .await;
4971                        return;
4972                    }
4973                }
4974                let track = match self.track_handle_or_err(track_name) {
4975                    Ok(track) => track,
4976                    Err(e) => {
4977                        self.notify_clients(Err(e)).await;
4978                        return;
4979                    }
4980                };
4981                match target {
4982                    crate::message::TrackMidiLearnTarget::Volume => {
4983                        track.lock().midi_learn_volume = binding.clone();
4984                    }
4985                    crate::message::TrackMidiLearnTarget::Balance => {
4986                        track.lock().midi_learn_balance = binding.clone();
4987                    }
4988                    crate::message::TrackMidiLearnTarget::Mute => {
4989                        track.lock().midi_learn_mute = binding.clone();
4990                    }
4991                    crate::message::TrackMidiLearnTarget::Solo => {
4992                        track.lock().midi_learn_solo = binding.clone();
4993                    }
4994                    crate::message::TrackMidiLearnTarget::Arm => {
4995                        track.lock().midi_learn_arm = binding.clone();
4996                    }
4997                    crate::message::TrackMidiLearnTarget::InputMonitor => {
4998                        track.lock().midi_learn_input_monitor = binding.clone();
4999                    }
5000                    crate::message::TrackMidiLearnTarget::DiskMonitor => {
5001                        track.lock().midi_learn_disk_monitor = binding.clone();
5002                    }
5003                }
5004            }
5005            Action::SetGlobalMidiLearnBinding {
5006                target,
5007                ref binding,
5008            } => {
5009                if let Some(binding) = binding.as_ref() {
5010                    let conflicts = self
5011                        .midi_learn_slot_conflicts(binding, Some(MidiLearnSlot::Global(target)));
5012                    if !conflicts.is_empty() {
5013                        self.notify_clients(Err(format!(
5014                            "Global MIDI learn conflict for {:?}: {}",
5015                            target,
5016                            conflicts.join(", ")
5017                        )))
5018                        .await;
5019                        return;
5020                    }
5021                }
5022                match target {
5023                    crate::message::GlobalMidiLearnTarget::PlayPause => {
5024                        self.global_midi_learn_play_pause = binding.clone();
5025                    }
5026                    crate::message::GlobalMidiLearnTarget::Stop => {
5027                        self.global_midi_learn_stop = binding.clone();
5028                    }
5029                    crate::message::GlobalMidiLearnTarget::RecordToggle => {
5030                        self.global_midi_learn_record_toggle = binding.clone();
5031                    }
5032                }
5033            }
5034            Action::TrackSetVcaMaster {
5035                ref track_name,
5036                ref master_track,
5037            } => {
5038                let track = match self.track_handle_or_err(track_name) {
5039                    Ok(track) => track,
5040                    Err(e) => {
5041                        self.notify_clients(Err(e)).await;
5042                        return;
5043                    }
5044                };
5045                if track.lock().is_master {
5046                    self.notify_clients(Err(format!(
5047                        "Master track '{}' cannot be part of a VCA group",
5048                        track_name
5049                    )))
5050                    .await;
5051                    return;
5052                }
5053                if let Some(master_name) = master_track
5054                    && master_name == track_name
5055                {
5056                    self.notify_clients(Err("Track cannot be its own VCA master".to_string()))
5057                        .await;
5058                    return;
5059                }
5060                if let Some(master_name) = master_track
5061                    && let Some(master) = self.state.lock().tracks.get(master_name)
5062                    && master.lock().is_master
5063                {
5064                    self.notify_clients(Err(format!(
5065                        "Track '{}' cannot be grouped to Master track '{}'",
5066                        track_name, master_name
5067                    )))
5068                    .await;
5069                    return;
5070                }
5071                track.lock().set_vca_master(master_track.clone());
5072            }
5073            Action::TrackSetFolder {
5074                ref track_name,
5075                is_folder,
5076            } => {
5077                let track = match self.track_handle_or_err(track_name) {
5078                    Ok(track) => track,
5079                    Err(e) => {
5080                        self.notify_clients(Err(e)).await;
5081                        return;
5082                    }
5083                };
5084                track.lock().is_folder = is_folder;
5085                self.notify_clients(Ok(Action::TrackSetFolder {
5086                    track_name: track_name.clone(),
5087                    is_folder,
5088                }))
5089                .await;
5090            }
5091            Action::TrackSetParent {
5092                ref track_name,
5093                ref parent_name,
5094            } => {
5095                let track = match self.track_handle_or_err(track_name) {
5096                    Ok(track) => track,
5097                    Err(e) => {
5098                        self.notify_clients(Err(e)).await;
5099                        return;
5100                    }
5101                };
5102                if parent_name.as_deref() == Some(track_name.as_str()) {
5103                    self.notify_clients(Err("Track cannot be its own parent".to_string()))
5104                        .await;
5105                    return;
5106                }
5107                // Get old parent and disconnect
5108                let old_parent = {
5109                    let t = track.lock();
5110                    t.parent_track.clone()
5111                };
5112                if let Some(ref old) = old_parent
5113                    && let Some(old_track_arc) = self.state.lock().tracks.get(old).cloned()
5114                {
5115                    let old_track = old_track_arc.lock();
5116                    track.lock().disconnect_outputs_from_parent(old_track);
5117                }
5118                // Connect to new parent
5119                if let Some(new_parent) = parent_name
5120                    && let Some(parent_track_arc) =
5121                        self.state.lock().tracks.get(new_parent).cloned()
5122                {
5123                    let parent_track = parent_track_arc.lock();
5124                    track.lock().connect_outputs_to_parent(parent_track);
5125                }
5126                track.lock().parent_track = parent_name.clone();
5127                self.notify_clients(Ok(Action::TrackSetParent {
5128                    track_name: track_name.clone(),
5129                    parent_name: parent_name.clone(),
5130                }))
5131                .await;
5132            }
5133            Action::TrackToggleFolder { ref track_name } => {
5134                let track = match self.track_handle_or_err(track_name) {
5135                    Ok(track) => track,
5136                    Err(e) => {
5137                        self.notify_clients(Err(e)).await;
5138                        return;
5139                    }
5140                };
5141                {
5142                    let t = track.lock();
5143                    t.folder_open = !t.folder_open;
5144                }
5145                self.notify_clients(Ok(Action::TrackToggleFolder {
5146                    track_name: track_name.clone(),
5147                }))
5148                .await;
5149                // Also notify with the new open state for GUI convenience
5150                self.notify_clients(Ok(Action::TrackSetFolder {
5151                    track_name: track_name.clone(),
5152                    is_folder: track.lock().is_folder,
5153                }))
5154                .await;
5155            }
5156            Action::TrackSetMidiLaneChannel {
5157                ref track_name,
5158                lane,
5159                channel,
5160            } => {
5161                let track = match self.track_handle_or_err(track_name) {
5162                    Ok(track) => track,
5163                    Err(e) => {
5164                        self.notify_clients(Err(e)).await;
5165                        return;
5166                    }
5167                };
5168                track.lock().set_midi_lane_channel(lane, channel);
5169            }
5170            Action::TrackSetFrozen {
5171                ref track_name,
5172                frozen,
5173            } => {
5174                let track = match self.track_handle_or_err(track_name) {
5175                    Ok(track) => track,
5176                    Err(e) => {
5177                        self.notify_clients(Err(e)).await;
5178                        return;
5179                    }
5180                };
5181                track.lock().set_frozen(frozen);
5182            }
5183            Action::TrackOfflineBounce {
5184                track_name,
5185                output_path,
5186                start_sample,
5187                length_samples,
5188                automation_lanes,
5189                apply_fader,
5190            } => {
5191                if self.offline_bounce_jobs.contains_key(&track_name) {
5192                    self.notify_clients(Err(format!(
5193                        "Offline bounce for track '{}' is already in progress",
5194                        track_name
5195                    )))
5196                    .await;
5197                    return;
5198                }
5199                if let Err(e) = self.track_handle_or_err(&track_name) {
5200                    self.notify_clients(Err(e)).await;
5201                    return;
5202                }
5203                if length_samples == 0 {
5204                    self.notify_clients(Err(format!(
5205                        "Track '{}' has no renderable content for offline bounce",
5206                        track_name
5207                    )))
5208                    .await;
5209                    return;
5210                }
5211                let Some(worker_index) = self.take_ready_worker_index(WorkerClass::Refill) else {
5212                    self.pending_requests
5213                        .push_front(Action::TrackOfflineBounce {
5214                            track_name,
5215                            output_path,
5216                            start_sample,
5217                            length_samples,
5218                            automation_lanes,
5219                            apply_fader,
5220                        });
5221                    return;
5222                };
5223                let cancel = Arc::new(AtomicBool::new(false));
5224                self.offline_bounce_jobs.insert(
5225                    track_name.clone(),
5226                    OfflineBounceJob {
5227                        cancel: cancel.clone(),
5228                    },
5229                );
5230                let track_name_clone = track_name.clone();
5231                let worker = &self.workers[worker_index];
5232                let job = crate::message::OfflineBounceWork {
5233                    state: self.state.clone(),
5234                    track_name,
5235                    output_path,
5236                    start_sample,
5237                    length_samples,
5238                    tempo_bpm: self.tempo_bpm,
5239                    tsig_num: self.tsig_num,
5240                    tsig_denom: self.tsig_denom,
5241                    automation_lanes,
5242                    cancel,
5243                    apply_fader,
5244                };
5245                if let Err(e) = worker.tx.send(Message::ProcessOfflineBounce(job)).await {
5246                    self.offline_bounce_jobs.remove(&track_name_clone);
5247                    self.notify_clients(Err(format!("Failed to schedule offline bounce: {e}")))
5248                        .await;
5249                }
5250                return;
5251            }
5252            Action::TrackOfflineBounceCancel { .. } => {}
5253            Action::TrackOfflineBounceCancelAll => {}
5254            Action::TrackOfflineBounceCanceled { .. } => {}
5255            Action::TrackOfflineBounceProgress { .. } => {}
5256            Action::PianoKey {
5257                ref track_name,
5258                note,
5259                velocity,
5260                on,
5261            } => {
5262                if let Some(track) = self.state.lock().tracks.get(track_name) {
5263                    let status = if on { 0x90 } else { 0x80 };
5264                    let event = MidiEvent::new(0, vec![status, note.min(127), velocity.min(127)]);
5265                    track.lock().push_hw_midi_events(&[event]);
5266                }
5267            }
5268            Action::ModifyMidiNotes { .. }
5269            | Action::ModifyMidiControllers { .. }
5270            | Action::DeleteMidiControllers { .. }
5271            | Action::InsertMidiControllers { .. }
5272            | Action::DeleteMidiNotes { .. }
5273            | Action::InsertMidiNotes { .. } => {
5274                if let Err(e) = self.apply_midi_edit_action(&action_to_process) {
5275                    self.notify_clients(Err(e)).await;
5276                    return;
5277                }
5278            }
5279            Action::SetMidiSysExEvents { .. } => {
5280                if let Err(e) = self.apply_midi_edit_action(&action_to_process) {
5281                    self.notify_clients(Err(e)).await;
5282                    return;
5283                }
5284            }
5285            Action::TrackClearDefaultPassthrough { ref track_name } => {
5286                if self
5287                    .reject_if_track_frozen(track_name, "plugin graph editing")
5288                    .await
5289                {
5290                    return;
5291                }
5292                let track = match self.track_handle_or_err(track_name) {
5293                    Ok(track) => track,
5294                    Err(e) => {
5295                        self.notify_clients(Err(e)).await;
5296                        return;
5297                    }
5298                };
5299                track.lock().clear_default_passthrough();
5300            }
5301            #[cfg(all(unix, not(target_os = "macos")))]
5302            Action::ClipSetLv2PluginState { ref track_name, .. } => {
5303                self.notify_clients(Err(format!(
5304                    "Track '{}': clip LV2 plugin state changes are not supported",
5305                    track_name
5306                )))
5307                .await;
5308            }
5309            Action::TrackGetClapNoteNames { ref track_name } => {
5310                let track = match self.track_handle_or_err(track_name) {
5311                    Ok(track) => track,
5312                    Err(e) => {
5313                        self.notify_clients(Err(e)).await;
5314                        return;
5315                    }
5316                };
5317                let note_names = track.lock().get_clap_note_names();
5318                self.notify_clients(Ok(Action::TrackClapNoteNames {
5319                    track_name: track_name.clone(),
5320                    note_names,
5321                }))
5322                .await;
5323            }
5324            Action::TrackGetPluginGraph { ref track_name } => {
5325                let track = match self.track_handle_or_err(track_name) {
5326                    Ok(track) => track,
5327                    Err(e) => {
5328                        self.notify_clients(Err(e)).await;
5329                        return;
5330                    }
5331                };
5332                let (plugins, connections) = {
5333                    let track = track.lock();
5334                    (
5335                        track.plugin_graph_plugins(),
5336                        track.plugin_graph_connections(),
5337                    )
5338                };
5339                self.notify_clients(Ok(Action::TrackPluginGraph {
5340                    track_name: track_name.clone(),
5341                    plugins,
5342                    connections,
5343                }))
5344                .await;
5345                return;
5346            }
5347            Action::TrackPluginGraph { .. } => {}
5348            Action::TrackConnectPluginAudio {
5349                ref track_name,
5350                ref from_node,
5351                from_port,
5352                ref to_node,
5353                to_port,
5354            } => {
5355                if self
5356                    .reject_if_track_frozen(track_name, "plugin routing changes")
5357                    .await
5358                {
5359                    return;
5360                }
5361                let track = match self.track_handle_or_err(track_name) {
5362                    Ok(track) => track,
5363                    Err(e) => {
5364                        self.notify_clients(Err(e)).await;
5365                        return;
5366                    }
5367                };
5368                if let Err(e) = track.lock().connect_plugin_audio(
5369                    from_node.clone(),
5370                    from_port,
5371                    to_node.clone(),
5372                    to_port,
5373                ) {
5374                    self.notify_clients(Err(e)).await;
5375                    return;
5376                }
5377            }
5378            Action::TrackConnectPluginMidi {
5379                ref track_name,
5380                ref from_node,
5381                from_port,
5382                ref to_node,
5383                to_port,
5384            } => {
5385                if self
5386                    .reject_if_track_frozen(track_name, "plugin routing changes")
5387                    .await
5388                {
5389                    return;
5390                }
5391                let track = match self.track_handle_or_err(track_name) {
5392                    Ok(track) => track,
5393                    Err(e) => {
5394                        self.notify_clients(Err(e)).await;
5395                        return;
5396                    }
5397                };
5398                if let Err(e) = track.lock().connect_plugin_midi(
5399                    from_node.clone(),
5400                    from_port,
5401                    to_node.clone(),
5402                    to_port,
5403                ) {
5404                    self.notify_clients(Err(e)).await;
5405                    return;
5406                }
5407            }
5408            Action::TrackDisconnectPluginAudio {
5409                ref track_name,
5410                ref from_node,
5411                from_port,
5412                ref to_node,
5413                to_port,
5414            } => {
5415                if self
5416                    .reject_if_track_frozen(track_name, "plugin routing changes")
5417                    .await
5418                {
5419                    return;
5420                }
5421                let track = match self.track_handle_or_err(track_name) {
5422                    Ok(track) => track,
5423                    Err(e) => {
5424                        self.notify_clients(Err(e)).await;
5425                        return;
5426                    }
5427                };
5428                if let Err(e) = track.lock().disconnect_plugin_audio(
5429                    from_node.clone(),
5430                    from_port,
5431                    to_node.clone(),
5432                    to_port,
5433                ) {
5434                    self.notify_clients(Err(e)).await;
5435                    return;
5436                }
5437            }
5438            Action::TrackDisconnectPluginMidi {
5439                ref track_name,
5440                ref from_node,
5441                from_port,
5442                ref to_node,
5443                to_port,
5444            } => {
5445                if self
5446                    .reject_if_track_frozen(track_name, "plugin routing changes")
5447                    .await
5448                {
5449                    return;
5450                }
5451                let track = match self.track_handle_or_err(track_name) {
5452                    Ok(track) => track,
5453                    Err(e) => {
5454                        self.notify_clients(Err(e)).await;
5455                        return;
5456                    }
5457                };
5458                if let Err(e) = track.lock().disconnect_plugin_midi(
5459                    from_node.clone(),
5460                    from_port,
5461                    to_node.clone(),
5462                    to_port,
5463                ) {
5464                    self.notify_clients(Err(e)).await;
5465                    return;
5466                }
5467            }
5468            #[cfg(all(unix, not(target_os = "macos")))]
5469            Action::ListLv2Plugins => {
5470                match crate::plugins::scan_plugins::<crate::plugins::types::Lv2PluginInfo>("lv2") {
5471                    Ok(plugins) => {
5472                        self.notify_clients(Ok(Action::Lv2Plugins(plugins))).await;
5473                    }
5474                    Err(e) => {
5475                        self.notify_clients(Err(e)).await;
5476                    }
5477                }
5478                return;
5479            }
5480            #[cfg(all(unix, not(target_os = "macos")))]
5481            Action::Lv2Plugins(_) => {}
5482            Action::ListVst3Plugins => {
5483                match crate::plugins::scan_plugins::<crate::plugins::types::Vst3PluginInfo>("vst3")
5484                {
5485                    Ok(plugins) => {
5486                        self.notify_clients(Ok(Action::Vst3Plugins(plugins))).await;
5487                    }
5488                    Err(e) => {
5489                        self.notify_clients(Err(e)).await;
5490                    }
5491                }
5492                return;
5493            }
5494            Action::Vst3Plugins(_) => {}
5495            Action::ListClapPlugins => {
5496                match crate::plugins::scan_plugins::<crate::plugins::types::ClapPluginInfo>("clap")
5497                {
5498                    Ok(plugins) => {
5499                        self.notify_clients(Ok(Action::ClapPlugins(plugins))).await;
5500                    }
5501                    Err(e) => {
5502                        self.notify_clients(Err(e)).await;
5503                    }
5504                }
5505                return;
5506            }
5507            Action::ListClapPluginsWithCapabilities => {
5508                match crate::plugins::scan_plugins::<crate::plugins::types::ClapPluginInfo>("clap")
5509                {
5510                    Ok(plugins) => {
5511                        self.notify_clients(Ok(Action::ClapPlugins(plugins))).await;
5512                    }
5513                    Err(e) => {
5514                        self.notify_clients(Err(e)).await;
5515                    }
5516                }
5517                return;
5518            }
5519            Action::ClapPlugins(_) => {}
5520            Action::TrackLoadClapPlugin {
5521                ref track_name,
5522                ref plugin_path,
5523                instance_id,
5524            } => {
5525                if self
5526                    .reject_if_track_frozen(track_name, "CLAP plugin loading")
5527                    .await
5528                {
5529                    return;
5530                }
5531                let track = match self.track_handle_or_err(track_name) {
5532                    Ok(track) => track,
5533                    Err(e) => {
5534                        self.notify_clients(Err(e)).await;
5535                        return;
5536                    }
5537                };
5538                let track = track.lock();
5539                if track.audio.processing {
5540                    self.notify_clients(Err(format!(
5541                        "Track '{}' is currently processing audio; stop playback before loading CLAP plugins",
5542                        track_name
5543                    )))
5544                    .await;
5545                    return;
5546                }
5547                if let Err(e) = track.load_clap_plugin(plugin_path, instance_id) {
5548                    self.notify_clients(Err(e)).await;
5549                    return;
5550                }
5551            }
5552            Action::TrackUnloadClapPlugin {
5553                ref track_name,
5554                ref plugin_path,
5555            } => {
5556                if self
5557                    .reject_if_track_frozen(track_name, "CLAP plugin unloading")
5558                    .await
5559                {
5560                    return;
5561                }
5562                let track = match self.track_handle_or_err(track_name) {
5563                    Ok(track) => track,
5564                    Err(e) => {
5565                        self.notify_clients(Err(e)).await;
5566                        return;
5567                    }
5568                };
5569                let track = track.lock();
5570                if track.audio.processing {
5571                    self.notify_clients(Err(format!(
5572                        "Track '{}' is currently processing audio; stop playback before unloading CLAP plugins",
5573                        track_name
5574                    )))
5575                    .await;
5576                    return;
5577                }
5578                if let Err(e) = track.unload_clap_plugin(plugin_path) {
5579                    self.notify_clients(Err(e)).await;
5580                    return;
5581                }
5582            }
5583            Action::TrackUnloadClapPluginInstance {
5584                ref track_name,
5585                instance_id,
5586            } => {
5587                if self
5588                    .reject_if_track_frozen(track_name, "CLAP plugin unloading")
5589                    .await
5590                {
5591                    return;
5592                }
5593                let track = match self.track_handle_or_err(track_name) {
5594                    Ok(track) => track,
5595                    Err(e) => {
5596                        self.notify_clients(Err(e)).await;
5597                        return;
5598                    }
5599                };
5600                let track = track.lock();
5601                if track.audio.processing {
5602                    self.notify_clients(Err(format!(
5603                        "Track '{}' is currently processing audio; stop playback before unloading CLAP plugins",
5604                        track_name
5605                    )))
5606                    .await;
5607                    return;
5608                }
5609                if let Err(e) = track.unload_clap_plugin_instance(instance_id) {
5610                    self.notify_clients(Err(e)).await;
5611                    return;
5612                }
5613            }
5614            Action::TrackShowClapGui {
5615                ref track_name,
5616                instance_id,
5617            } => {
5618                let track = match self.track_handle_or_err(track_name) {
5619                    Ok(track) => track,
5620                    Err(e) => {
5621                        self.notify_clients(Err(e)).await;
5622                        return;
5623                    }
5624                };
5625                if let Err(e) = track.lock().show_clap_gui(instance_id) {
5626                    self.notify_clients(Err(e)).await;
5627                    return;
5628                }
5629            }
5630            Action::TrackLoadVst3Plugin {
5631                ref track_name,
5632                ref plugin_path,
5633                instance_id,
5634            } => {
5635                if self
5636                    .reject_if_track_frozen(track_name, "VST3 plugin loading")
5637                    .await
5638                {
5639                    return;
5640                }
5641                let track = match self.track_handle_or_err(track_name) {
5642                    Ok(track) => track,
5643                    Err(e) => {
5644                        self.notify_clients(Err(e)).await;
5645                        return;
5646                    }
5647                };
5648                let track = track.lock();
5649                if track.audio.processing {
5650                    self.notify_clients(Err(format!(
5651                        "Track '{}' is currently processing audio; stop playback before loading VST3 plugins",
5652                        track_name
5653                    )))
5654                    .await;
5655                    return;
5656                }
5657                if let Err(e) = track.load_vst3_plugin(plugin_path, instance_id) {
5658                    self.notify_clients(Err(e)).await;
5659                    return;
5660                }
5661            }
5662            Action::TrackUnloadVst3Plugin {
5663                ref track_name,
5664                ref plugin_path,
5665            } => {
5666                if self
5667                    .reject_if_track_frozen(track_name, "VST3 plugin unloading")
5668                    .await
5669                {
5670                    return;
5671                }
5672                let track = match self.track_handle_or_err(track_name) {
5673                    Ok(track) => track,
5674                    Err(e) => {
5675                        self.notify_clients(Err(e)).await;
5676                        return;
5677                    }
5678                };
5679                let track = track.lock();
5680                if track.audio.processing {
5681                    self.notify_clients(Err(format!(
5682                        "Track '{}' is currently processing audio; stop playback before unloading VST3 plugins",
5683                        track_name
5684                    )))
5685                    .await;
5686                    return;
5687                }
5688                if let Err(e) = track.unload_vst3_plugin(plugin_path) {
5689                    self.notify_clients(Err(e)).await;
5690                    return;
5691                }
5692            }
5693            Action::TrackUnloadVst3PluginInstance {
5694                ref track_name,
5695                instance_id,
5696            } => {
5697                if self
5698                    .reject_if_track_frozen(track_name, "VST3 plugin unloading")
5699                    .await
5700                {
5701                    return;
5702                }
5703                let track = match self.track_handle_or_err(track_name) {
5704                    Ok(track) => track,
5705                    Err(e) => {
5706                        self.notify_clients(Err(e)).await;
5707                        return;
5708                    }
5709                };
5710                let track = track.lock();
5711                if track.audio.processing {
5712                    self.notify_clients(Err(format!(
5713                        "Track '{}' is currently processing audio; stop playback before unloading VST3 plugins",
5714                        track_name
5715                    )))
5716                    .await;
5717                    return;
5718                }
5719                if let Err(e) = track.unload_vst3_plugin_instance(instance_id) {
5720                    self.notify_clients(Err(e)).await;
5721                    return;
5722                }
5723            }
5724            Action::TrackShowVst3Gui {
5725                ref track_name,
5726                instance_id,
5727            } => {
5728                let track = match self.track_handle_or_err(track_name) {
5729                    Ok(track) => track,
5730                    Err(e) => {
5731                        self.notify_clients(Err(e)).await;
5732                        return;
5733                    }
5734                };
5735                if let Err(e) = track.lock().show_vst3_gui(instance_id) {
5736                    self.notify_clients(Err(e)).await;
5737                    return;
5738                }
5739            }
5740            #[cfg(all(unix, not(target_os = "macos")))]
5741            Action::TrackLoadLv2Plugin {
5742                ref track_name,
5743                ref plugin_uri,
5744                instance_id,
5745            } => {
5746                if self
5747                    .reject_if_track_frozen(track_name, "LV2 plugin loading")
5748                    .await
5749                {
5750                    return;
5751                }
5752                let track = match self.track_handle_or_err(track_name) {
5753                    Ok(track) => track,
5754                    Err(e) => {
5755                        self.notify_clients(Err(e)).await;
5756                        return;
5757                    }
5758                };
5759                let track = track.lock();
5760                if track.audio.processing {
5761                    self.notify_clients(Err(format!(
5762                        "Track '{}' is currently processing audio; stop playback before loading LV2 plugins",
5763                        track_name
5764                    )))
5765                    .await;
5766                    return;
5767                }
5768                if let Err(e) = track.load_lv2_plugin(plugin_uri, instance_id) {
5769                    self.notify_clients(Err(e)).await;
5770                    return;
5771                }
5772            }
5773            #[cfg(all(unix, not(target_os = "macos")))]
5774            Action::TrackUnloadLv2Plugin {
5775                ref track_name,
5776                ref plugin_uri,
5777            } => {
5778                if self
5779                    .reject_if_track_frozen(track_name, "LV2 plugin unloading")
5780                    .await
5781                {
5782                    return;
5783                }
5784                let track = match self.track_handle_or_err(track_name) {
5785                    Ok(track) => track,
5786                    Err(e) => {
5787                        self.notify_clients(Err(e)).await;
5788                        return;
5789                    }
5790                };
5791                let track = track.lock();
5792                if track.audio.processing {
5793                    self.notify_clients(Err(format!(
5794                        "Track '{}' is currently processing audio; stop playback before unloading LV2 plugins",
5795                        track_name
5796                    )))
5797                    .await;
5798                    return;
5799                }
5800                if let Err(e) = track.unload_lv2_plugin(plugin_uri) {
5801                    self.notify_clients(Err(e)).await;
5802                    return;
5803                }
5804            }
5805            #[cfg(all(unix, not(target_os = "macos")))]
5806            Action::TrackUnloadLv2PluginInstance {
5807                ref track_name,
5808                instance_id,
5809            } => {
5810                if self
5811                    .reject_if_track_frozen(track_name, "LV2 plugin unloading")
5812                    .await
5813                {
5814                    return;
5815                }
5816                let track = match self.track_handle_or_err(track_name) {
5817                    Ok(track) => track,
5818                    Err(e) => {
5819                        self.notify_clients(Err(e)).await;
5820                        return;
5821                    }
5822                };
5823                let track = track.lock();
5824                if track.audio.processing {
5825                    self.notify_clients(Err(format!(
5826                        "Track '{}' is currently processing audio; stop playback before unloading LV2 plugins",
5827                        track_name
5828                    )))
5829                    .await;
5830                    return;
5831                }
5832                if let Err(e) = track.unload_lv2_plugin_instance(instance_id) {
5833                    self.notify_clients(Err(e)).await;
5834                    return;
5835                }
5836            }
5837            #[cfg(all(unix, not(target_os = "macos")))]
5838            Action::TrackShowLv2Gui {
5839                ref track_name,
5840                instance_id,
5841            } => {
5842                let track = match self.track_handle_or_err(track_name) {
5843                    Ok(track) => track,
5844                    Err(e) => {
5845                        self.notify_clients(Err(e)).await;
5846                        return;
5847                    }
5848                };
5849                if let Err(e) = track.lock().show_lv2_gui(instance_id) {
5850                    self.notify_clients(Err(e)).await;
5851                    return;
5852                }
5853            }
5854            Action::TrackSetClapParameter {
5855                ref track_name,
5856                instance_id,
5857                param_id,
5858                value,
5859            } => {
5860                if self
5861                    .reject_if_track_frozen(track_name, "CLAP parameter changes")
5862                    .await
5863                {
5864                    return;
5865                }
5866                match self.track_handle_or_err(track_name) {
5867                    Ok(track) => {
5868                        if let Err(e) =
5869                            track
5870                                .lock()
5871                                .set_clap_parameter(instance_id, param_id, value)
5872                        {
5873                            self.notify_clients(Err(e)).await;
5874                            return;
5875                        }
5876                        self.notify_clients(Ok(a.clone())).await;
5877                    }
5878                    Err(e) => {
5879                        self.notify_clients(Err(e)).await;
5880                    }
5881                }
5882            }
5883            Action::ClipSetClapParameter {
5884                ref track_name,
5885                clip_idx,
5886                instance_id,
5887                param_id,
5888                value,
5889            } => {
5890                if self
5891                    .reject_if_track_frozen(track_name, "CLAP parameter changes")
5892                    .await
5893                {
5894                    return;
5895                }
5896                match self.track_handle_or_err(track_name) {
5897                    Ok(track) => {
5898                        if let Err(e) = track.lock().clip_set_clap_parameter(
5899                            clip_idx,
5900                            instance_id,
5901                            param_id,
5902                            value,
5903                        ) {
5904                            self.notify_clients(Err(e)).await;
5905                            return;
5906                        }
5907                        self.notify_clients(Ok(a.clone())).await;
5908                    }
5909                    Err(e) => {
5910                        self.notify_clients(Err(e)).await;
5911                    }
5912                }
5913            }
5914            Action::TrackSetClapParameterAt {
5915                ref track_name,
5916                instance_id,
5917                param_id,
5918                value,
5919                frame,
5920            } => {
5921                if self
5922                    .reject_if_track_frozen(track_name, "CLAP parameter changes")
5923                    .await
5924                {
5925                    return;
5926                }
5927                match self.track_handle_or_err(track_name) {
5928                    Ok(track) => {
5929                        if let Err(e) =
5930                            track
5931                                .lock()
5932                                .set_clap_parameter_at(instance_id, param_id, value, frame)
5933                        {
5934                            self.notify_clients(Err(e)).await;
5935                            return;
5936                        }
5937                        self.notify_clients(Ok(a.clone())).await;
5938                    }
5939                    Err(e) => {
5940                        self.notify_clients(Err(e)).await;
5941                    }
5942                }
5943            }
5944            Action::TrackBeginClapParameterEdit {
5945                ref track_name,
5946                instance_id,
5947                param_id,
5948                frame,
5949            } => {
5950                if self
5951                    .reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
5952                    .await
5953                {
5954                    return;
5955                }
5956                match self.track_handle_or_err(track_name) {
5957                    Ok(track) => {
5958                        if let Err(e) =
5959                            track
5960                                .lock()
5961                                .begin_clap_parameter_edit(instance_id, param_id, frame)
5962                        {
5963                            self.notify_clients(Err(e)).await;
5964                            return;
5965                        }
5966                        self.notify_clients(Ok(a.clone())).await;
5967                    }
5968                    Err(e) => {
5969                        self.notify_clients(Err(e)).await;
5970                    }
5971                }
5972            }
5973            Action::TrackEndClapParameterEdit {
5974                ref track_name,
5975                instance_id,
5976                param_id,
5977                frame,
5978            } => {
5979                if self
5980                    .reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
5981                    .await
5982                {
5983                    return;
5984                }
5985                match self.track_handle_or_err(track_name) {
5986                    Ok(track) => {
5987                        if let Err(e) =
5988                            track
5989                                .lock()
5990                                .end_clap_parameter_edit(instance_id, param_id, frame)
5991                        {
5992                            self.notify_clients(Err(e)).await;
5993                            return;
5994                        }
5995                        self.notify_clients(Ok(a.clone())).await;
5996                    }
5997                    Err(e) => {
5998                        self.notify_clients(Err(e)).await;
5999                    }
6000                }
6001            }
6002            Action::TrackGetClapParameters {
6003                ref track_name,
6004                instance_id,
6005            } => match self.track_handle_or_err(track_name) {
6006                Ok(track) => match track.lock().get_clap_parameters(instance_id) {
6007                    Ok(parameters) => {
6008                        self.notify_clients(Ok(Action::TrackClapParameters {
6009                            track_name: track_name.clone(),
6010                            instance_id,
6011                            parameters,
6012                        }))
6013                        .await;
6014                    }
6015                    Err(e) => {
6016                        self.notify_clients(Err(e)).await;
6017                    }
6018                },
6019                Err(e) => {
6020                    self.notify_clients(Err(e)).await;
6021                }
6022            },
6023            Action::TrackClapParameters { .. } => {}
6024            Action::TrackClapSnapshotState {
6025                ref track_name,
6026                instance_id,
6027            } => match self.track_handle_or_err(track_name) {
6028                Ok(track) => {
6029                    let plugin_path = track
6030                        .lock()
6031                        .clap_plugins
6032                        .iter()
6033                        .find(|instance| instance.id == instance_id)
6034                        .map(|instance| instance.processor.lock().path().to_string())
6035                        .unwrap_or_default();
6036                    match track.lock().clap_snapshot_state(instance_id) {
6037                        Ok(state) => {
6038                            self.notify_clients(Ok(Action::TrackClapStateSnapshot {
6039                                track_name: track_name.clone(),
6040                                instance_id,
6041                                plugin_path,
6042                                state,
6043                            }))
6044                            .await;
6045                        }
6046                        Err(e) => {
6047                            self.notify_clients(Err(e)).await;
6048                        }
6049                    }
6050                }
6051                Err(e) => {
6052                    self.notify_clients(Err(e)).await;
6053                }
6054            },
6055            Action::ClipClapSnapshotState {
6056                ref track_name,
6057                clip_idx,
6058                instance_id,
6059            } => match self.track_handle_or_err(track_name) {
6060                Ok(track) => match track.lock().clip_clap_snapshot_state(clip_idx, instance_id) {
6061                    Ok((plugin_path, state)) => {
6062                        self.notify_clients(Ok(Action::ClipClapStateSnapshot {
6063                            track_name: track_name.clone(),
6064                            clip_idx,
6065                            instance_id,
6066                            plugin_path,
6067                            state,
6068                        }))
6069                        .await;
6070                    }
6071                    Err(e) => {
6072                        self.notify_clients(Err(e)).await;
6073                    }
6074                },
6075                Err(e) => {
6076                    self.notify_clients(Err(e)).await;
6077                }
6078            },
6079            Action::TrackClapStateSnapshot { .. } => {}
6080            Action::ClipClapStateSnapshot { .. } => {}
6081            Action::TrackClapRestoreState {
6082                ref track_name,
6083                instance_id,
6084                ref state,
6085            } => {
6086                if self
6087                    .reject_if_track_frozen(track_name, "CLAP state restore")
6088                    .await
6089                {
6090                    return;
6091                }
6092                let track = match self.track_handle_or_err(track_name) {
6093                    Ok(track) => track,
6094                    Err(e) => {
6095                        self.notify_clients(Err(e)).await;
6096                        return;
6097                    }
6098                };
6099                let track = track.lock();
6100                if track.audio.processing {
6101                    self.notify_clients(Err(format!(
6102                        "Track '{}' is currently processing audio; stop playback before restoring CLAP state",
6103                        track_name
6104                    )))
6105                    .await;
6106                    return;
6107                }
6108                if let Err(e) = track.clap_restore_state(instance_id, state) {
6109                    self.notify_clients(Err(e)).await;
6110                    return;
6111                }
6112            }
6113            Action::ClipClapRestoreState {
6114                ref track_name,
6115                clip_idx,
6116                instance_id,
6117                ref state,
6118            } => {
6119                if self
6120                    .reject_if_track_frozen(track_name, "CLAP state restore")
6121                    .await
6122                {
6123                    return;
6124                }
6125                let track = match self.track_handle_or_err(track_name) {
6126                    Ok(track) => track,
6127                    Err(e) => {
6128                        self.notify_clients(Err(e)).await;
6129                        return;
6130                    }
6131                };
6132                let track = track.lock();
6133                if track.audio.processing {
6134                    self.notify_clients(Err(format!(
6135                        "Track '{}' is currently processing audio; stop playback before restoring CLAP state",
6136                        track_name
6137                    )))
6138                    .await;
6139                    return;
6140                }
6141                if let Err(e) = track.clip_clap_restore_state(clip_idx, instance_id, state) {
6142                    self.notify_clients(Err(e)).await;
6143                    return;
6144                }
6145            }
6146            Action::TrackSnapshotAllClapStates { ref track_name } => {
6147                let track = match self.track_handle_or_err(track_name) {
6148                    Ok(track) => track,
6149                    Err(e) => {
6150                        self.notify_clients(Err(e)).await;
6151                        return;
6152                    }
6153                };
6154                for (instance_id, plugin_path, state) in track.lock().clap_snapshot_all_states() {
6155                    self.notify_clients(Ok(Action::TrackClapStateSnapshot {
6156                        track_name: track_name.clone(),
6157                        instance_id,
6158                        plugin_path,
6159                        state,
6160                    }))
6161                    .await;
6162                }
6163                self.notify_clients(Ok(Action::TrackSnapshotAllClapStatesDone {
6164                    track_name: track_name.clone(),
6165                }))
6166                .await;
6167            }
6168            Action::TrackSnapshotAllClapStatesDone { .. } => {}
6169            Action::TrackGetVst3Graph { ref track_name } => {
6170                match self.track_handle_or_err(track_name) {
6171                    Ok(track) => {
6172                        let t = track.lock();
6173                        let plugins = t.vst3_graph_plugins();
6174                        let connections = t.vst3_graph_connections();
6175                        self.notify_clients(Ok(Action::TrackVst3Graph {
6176                            track_name: track_name.clone(),
6177                            plugins,
6178                            connections,
6179                        }))
6180                        .await;
6181                    }
6182                    Err(e) => {
6183                        self.notify_clients(Err(e)).await;
6184                    }
6185                }
6186            }
6187            Action::TrackVst3Graph { .. } => {}
6188            Action::TrackSetVst3Parameter {
6189                ref track_name,
6190                instance_id,
6191                param_id,
6192                value,
6193            } => {
6194                if self
6195                    .reject_if_track_frozen(track_name, "VST3 parameter changes")
6196                    .await
6197                {
6198                    return;
6199                }
6200                match self.track_handle_or_err(track_name) {
6201                    Ok(track) => {
6202                        if let Err(e) =
6203                            track
6204                                .lock()
6205                                .set_vst3_parameter(instance_id, param_id, value)
6206                        {
6207                            self.notify_clients(Err(e)).await;
6208                            return;
6209                        }
6210                        self.notify_clients(Ok(a.clone())).await;
6211                    }
6212                    Err(e) => {
6213                        self.notify_clients(Err(e)).await;
6214                    }
6215                }
6216            }
6217            Action::TrackSetPluginBypassed {
6218                ref track_name,
6219                instance_id,
6220                ref format,
6221                bypassed,
6222            } => match self.track_handle_or_err(track_name) {
6223                Ok(track) => {
6224                    let result = match format.as_str() {
6225                        "CLAP" => track.lock().set_clap_plugin_bypassed(instance_id, bypassed),
6226                        "VST3" => track.lock().set_vst3_plugin_bypassed(instance_id, bypassed),
6227                        #[cfg(all(unix, not(target_os = "macos")))]
6228                        "LV2" => track.lock().set_lv2_plugin_bypassed(instance_id, bypassed),
6229                        _ => Err(format!("Unknown plugin format for bypass: {format}")),
6230                    };
6231                    if let Err(e) = result {
6232                        self.notify_clients(Err(e)).await;
6233                        return;
6234                    }
6235                    self.notify_clients(Ok(a.clone())).await;
6236                }
6237                Err(e) => {
6238                    self.notify_clients(Err(e)).await;
6239                }
6240            },
6241            Action::TrackGetVst3Parameters {
6242                ref track_name,
6243                instance_id,
6244            } => match self.track_handle_or_err(track_name) {
6245                Ok(track) => match track.lock().get_vst3_parameters(instance_id) {
6246                    Ok(parameters) => {
6247                        self.notify_clients(Ok(Action::TrackVst3Parameters {
6248                            track_name: track_name.clone(),
6249                            instance_id,
6250                            parameters,
6251                        }))
6252                        .await;
6253                    }
6254                    Err(e) => {
6255                        self.notify_clients(Err(e)).await;
6256                    }
6257                },
6258                Err(e) => {
6259                    self.notify_clients(Err(e)).await;
6260                }
6261            },
6262            Action::TrackVst3Parameters { .. } => {}
6263            Action::TrackVst3SnapshotState {
6264                ref track_name,
6265                instance_id,
6266            } => match self.track_handle_or_err(track_name) {
6267                Ok(track) => match track.lock().vst3_snapshot_state(instance_id) {
6268                    Ok(state) => {
6269                        self.notify_clients(Ok(Action::TrackVst3StateSnapshot {
6270                            track_name: track_name.clone(),
6271                            instance_id,
6272                            state,
6273                        }))
6274                        .await;
6275                    }
6276                    Err(e) => {
6277                        self.notify_clients(Err(e)).await;
6278                    }
6279                },
6280                Err(e) => {
6281                    self.notify_clients(Err(e)).await;
6282                }
6283            },
6284            Action::ClipVst3SnapshotState {
6285                ref track_name,
6286                clip_idx,
6287                instance_id,
6288            } => match self.track_handle_or_err(track_name) {
6289                Ok(track) => match track.lock().clip_vst3_snapshot_state(clip_idx, instance_id) {
6290                    Ok(state) => {
6291                        self.notify_clients(Ok(Action::ClipVst3StateSnapshot {
6292                            track_name: track_name.clone(),
6293                            clip_idx,
6294                            instance_id,
6295                            state,
6296                        }))
6297                        .await;
6298                    }
6299                    Err(e) => {
6300                        self.notify_clients(Err(e)).await;
6301                    }
6302                },
6303                Err(e) => {
6304                    self.notify_clients(Err(e)).await;
6305                }
6306            },
6307            Action::TrackVst3StateSnapshot { .. } => {}
6308            Action::ClipVst3StateSnapshot { .. } => {}
6309            Action::TrackVst3RestoreState {
6310                ref track_name,
6311                instance_id,
6312                ref state,
6313            } => match self.track_handle_or_err(track_name) {
6314                Ok(track) => {
6315                    if let Err(e) = track.lock().vst3_restore_state(instance_id, state) {
6316                        self.notify_clients(Err(e)).await;
6317                        return;
6318                    }
6319                    self.notify_clients(Ok(a.clone())).await;
6320                }
6321                Err(e) => {
6322                    self.notify_clients(Err(e)).await;
6323                }
6324            },
6325            Action::TrackConnectVst3Audio {
6326                ref track_name,
6327                ref from_node,
6328                from_port,
6329                ref to_node,
6330                to_port,
6331            } => {
6332                if self
6333                    .reject_if_track_frozen(track_name, "VST3 routing changes")
6334                    .await
6335                {
6336                    return;
6337                }
6338                match self.track_handle_or_err(track_name) {
6339                    Ok(track) => {
6340                        if let Err(e) = track
6341                            .lock()
6342                            .connect_vst3_audio(from_node, from_port, to_node, to_port)
6343                        {
6344                            self.notify_clients(Err(e)).await;
6345                            return;
6346                        }
6347                        self.notify_clients(Ok(a.clone())).await;
6348                    }
6349                    Err(e) => {
6350                        self.notify_clients(Err(e)).await;
6351                    }
6352                }
6353            }
6354            Action::TrackDisconnectVst3Audio {
6355                ref track_name,
6356                ref from_node,
6357                from_port,
6358                ref to_node,
6359                to_port,
6360            } => {
6361                if self
6362                    .reject_if_track_frozen(track_name, "VST3 routing changes")
6363                    .await
6364                {
6365                    return;
6366                }
6367                match self.track_handle_or_err(track_name) {
6368                    Ok(track) => {
6369                        if let Err(e) = track
6370                            .lock()
6371                            .disconnect_vst3_audio(from_node, from_port, to_node, to_port)
6372                        {
6373                            self.notify_clients(Err(e)).await;
6374                            return;
6375                        }
6376                        self.notify_clients(Ok(a.clone())).await;
6377                    }
6378                    Err(e) => {
6379                        self.notify_clients(Err(e)).await;
6380                    }
6381                }
6382            }
6383            Action::ClipMove {
6384                ref kind,
6385                ref from,
6386                ref to,
6387                copy,
6388            } => {
6389                if let Some(from_track_handle) = self.state.lock().tracks.get(&from.track_name)
6390                    && let Some(to_track_handle) = self.state.lock().tracks.get(&to.track_name)
6391                {
6392                    let from_track = from_track_handle.lock();
6393                    let to_track = to_track_handle.lock();
6394                    match kind {
6395                        Kind::Audio => {
6396                            if from.clip_index >= from_track.audio.clips.len() {
6397                                self.notify_clients(Err(format!(
6398                                    "Clip index {} is too high, as track {} has only {} clips!",
6399                                    from.clip_index,
6400                                    from_track.name.clone(),
6401                                    from_track.audio.clips.len(),
6402                                )))
6403                                .await;
6404                                return;
6405                            }
6406                            if from_track.audio.ins.len() != to_track.audio.ins.len() {
6407                                self.notify_clients(Err(format!(
6408                                    "Cannot move/copy audio clip from '{}' ({} inputs) to '{}' ({} inputs)",
6409                                    from_track.name,
6410                                    from_track.audio.ins.len(),
6411                                    to_track.name,
6412                                    to_track.audio.ins.len()
6413                                )))
6414                                .await;
6415                                return;
6416                            }
6417                            let clip_copy = from_track.audio.clips[from.clip_index].clone();
6418                            if !copy {
6419                                from_track.audio.clips.remove(from.clip_index);
6420                            }
6421                            let mut clip_copy = clip_copy;
6422                            clip_copy.start = to.sample_offset;
6423                            let max_lane = to_track.audio.ins.len().saturating_sub(1);
6424                            clip_copy.input_channel = to.input_channel.min(max_lane);
6425                            to_track.audio.clips.push(clip_copy);
6426                        }
6427                        Kind::MIDI => {
6428                            if from.clip_index >= from_track.midi.clips.len() {
6429                                self.notify_clients(Err(format!(
6430                                    "Clip index {} is too high, as track {} has only {} clips!",
6431                                    from.clip_index,
6432                                    from_track.name.clone(),
6433                                    from_track.midi.clips.len(),
6434                                )))
6435                                .await;
6436                                return;
6437                            }
6438                            let clip_copy = from_track.midi.clips[from.clip_index].clone();
6439                            if !copy {
6440                                from_track.midi.clips.remove(from.clip_index);
6441                            }
6442                            let mut clip_copy = clip_copy;
6443                            clip_copy.start = to.sample_offset;
6444                            let max_lane = to_track.midi.ins.len().saturating_sub(1);
6445                            clip_copy.input_channel = to.input_channel.min(max_lane);
6446                            to_track.midi.clips.push(clip_copy);
6447                        }
6448                    }
6449                }
6450            }
6451            Action::AddClip {
6452                ref name,
6453                ref track_name,
6454                start,
6455                length,
6456                offset,
6457                input_channel,
6458                muted,
6459                ref peaks_file,
6460                kind,
6461                fade_enabled,
6462                fade_in_samples,
6463                fade_out_samples,
6464                ref source_name,
6465                source_offset,
6466                source_length,
6467                ref preview_name,
6468                ref pitch_correction_points,
6469                pitch_correction_frame_likeness,
6470                pitch_correction_inertia_ms,
6471                pitch_correction_formant_compensation,
6472                ref plugin_graph_json,
6473            } => {
6474                self.add_clip_to_track(ClipAddRequest {
6475                    name,
6476                    track_name,
6477                    start,
6478                    length,
6479                    offset,
6480                    input_channel,
6481                    muted,
6482                    peaks_file: peaks_file.clone(),
6483                    kind,
6484                    fade_enabled,
6485                    fade_in_samples,
6486                    fade_out_samples,
6487                    source_name: source_name.clone(),
6488                    source_offset,
6489                    source_length,
6490                    preview_name: preview_name.clone(),
6491                    pitch_correction_points: pitch_correction_points.clone(),
6492                    pitch_correction_frame_likeness,
6493                    pitch_correction_inertia_ms,
6494                    pitch_correction_formant_compensation,
6495                    plugin_graph_json: plugin_graph_json.clone(),
6496                });
6497                if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
6498                    let track_name = track_name.clone();
6499                    tokio::task::spawn_blocking(move || {
6500                        track.lock().preload_clips();
6501                        tracing::debug!("Preloaded clips for track '{}' after AddClip", track_name);
6502                    });
6503                }
6504            }
6505            Action::AddGroupedClip {
6506                ref track_name,
6507                kind,
6508                ref audio_clip,
6509                ref midi_clip,
6510            } => {
6511                self.add_grouped_clip_to_track(
6512                    track_name,
6513                    kind,
6514                    audio_clip.clone(),
6515                    midi_clip.clone(),
6516                );
6517                if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
6518                    let track_name = track_name.clone();
6519                    tokio::task::spawn_blocking(move || {
6520                        track.lock().preload_clips();
6521                        tracing::debug!(
6522                            "Preloaded clips for track '{}' after AddGroupedClip",
6523                            track_name
6524                        );
6525                    });
6526                }
6527            }
6528            Action::RemoveClip {
6529                ref track_name,
6530                kind,
6531                ref clip_indices,
6532            } => {
6533                self.remove_clips_from_track(track_name, kind, clip_indices);
6534            }
6535            Action::RenameClip {
6536                ref track_name,
6537                kind,
6538                clip_index,
6539                ref new_name,
6540            } => {
6541                self.rename_clip_references(track_name, kind, clip_index, new_name);
6542            }
6543            Action::SetClipSourceName {
6544                ref track_name,
6545                kind,
6546                clip_index,
6547                ref name,
6548            } => {
6549                self.set_clip_source_name(track_name, clip_index, kind, name.clone());
6550            }
6551            Action::SetClipFade {
6552                ref track_name,
6553                clip_index,
6554                kind,
6555                fade_enabled,
6556                fade_in_samples,
6557                fade_out_samples,
6558            } => {
6559                self.set_clip_fade(
6560                    track_name,
6561                    clip_index,
6562                    kind,
6563                    fade_enabled,
6564                    fade_in_samples,
6565                    fade_out_samples,
6566                );
6567            }
6568            Action::SetClipBounds {
6569                ref track_name,
6570                clip_index,
6571                kind,
6572                start,
6573                length,
6574                offset,
6575            } => {
6576                self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
6577            }
6578            Action::SyncClipBounds {
6579                ref track_name,
6580                clip_index,
6581                kind,
6582                start,
6583                length,
6584                offset,
6585            } => {
6586                self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
6587            }
6588            Action::SetClipMuted {
6589                ref track_name,
6590                clip_index,
6591                kind,
6592                muted,
6593            } => {
6594                self.set_clip_muted(track_name, clip_index, kind, muted);
6595            }
6596            Action::SetClipPluginGraphJson {
6597                ref track_name,
6598                clip_index,
6599                ref plugin_graph_json,
6600            } => {
6601                self.set_clip_plugin_graph_json(track_name, clip_index, plugin_graph_json.clone());
6602            }
6603            Action::SetClipPitchCorrection {
6604                ref track_name,
6605                clip_index,
6606                ref preview_name,
6607                ref source_name,
6608                source_offset,
6609                source_length,
6610                ref pitch_correction_points,
6611                pitch_correction_frame_likeness,
6612                pitch_correction_inertia_ms,
6613                pitch_correction_formant_compensation,
6614            } => {
6615                self.set_clip_pitch_correction(
6616                    track_name,
6617                    clip_index,
6618                    preview_name.clone(),
6619                    source_name.clone(),
6620                    source_offset,
6621                    source_length,
6622                    pitch_correction_points.clone(),
6623                    pitch_correction_frame_likeness,
6624                    pitch_correction_inertia_ms,
6625                    pitch_correction_formant_compensation,
6626                );
6627            }
6628            Action::Connect {
6629                ref from_track,
6630                from_port,
6631                ref to_track,
6632                to_port,
6633                kind,
6634            } => {
6635                match kind {
6636                    Kind::Audio => {
6637                        let from_audio_io = if from_track == "hw:in" {
6638                            self.hw_input_audio_port(from_port)
6639                        } else {
6640                            self.state
6641                                .lock()
6642                                .tracks
6643                                .get(from_track)
6644                                .and_then(|t| t.lock().audio.outs.get(from_port).cloned())
6645                        };
6646                        let to_audio_io = if to_track == "hw:out" {
6647                            self.hw_output_audio_port(to_port)
6648                        } else {
6649                            self.state
6650                                .lock()
6651                                .tracks
6652                                .get(to_track)
6653                                .and_then(|t| t.lock().audio.ins.get(to_port).cloned())
6654                        };
6655                        match (from_audio_io, to_audio_io) {
6656                            (Some(source), Some(target)) => {
6657                                if from_track != "hw:in"
6658                                    && to_track != "hw:out"
6659                                    && self.check_if_leads_to_kind(
6660                                        Kind::Audio,
6661                                        to_track,
6662                                        from_track,
6663                                    )
6664                                {
6665                                    self.notify_clients(Err(
6666                                        "Circular routing is not allowed!".into()
6667                                    ))
6668                                    .await;
6669                                    return;
6670                                }
6671                                crate::audio::io::AudioIO::connect(&source, &target);
6672                            }
6673                            (None, _) => {
6674                                self.notify_clients(Err(format!(
6675                                    "Source track '{}' not found",
6676                                    from_track
6677                                )))
6678                                .await;
6679                                return;
6680                            }
6681                            (_, None) => {
6682                                self.notify_clients(Err(format!(
6683                                    "Destination track '{}' not found",
6684                                    to_track
6685                                )))
6686                                .await;
6687                                return;
6688                            }
6689                        }
6690                    }
6691                    Kind::MIDI => {
6692                        let from_hw_in_device = Self::midi_hw_in_device(from_track);
6693                        let to_hw_out_device = Self::midi_hw_out_device(to_track);
6694                        let from_is_invalid_hw = Self::midi_hw_out_device(from_track).is_some();
6695                        let to_is_invalid_hw = Self::midi_hw_in_device(to_track).is_some();
6696
6697                        if from_is_invalid_hw || to_is_invalid_hw {
6698                            self.notify_clients(Err(
6699                                "Invalid MIDI hardware connection direction".to_string()
6700                            ))
6701                            .await;
6702                            return;
6703                        }
6704
6705                        if from_hw_in_device.is_none()
6706                            && to_hw_out_device.is_none()
6707                            && self.check_if_leads_to_kind(Kind::MIDI, to_track, from_track)
6708                        {
6709                            self.notify_clients(Err("Circular routing is not allowed!".into()))
6710                                .await;
6711                            return;
6712                        }
6713
6714                        let state = self.state.lock();
6715                        let from_track_handle = state.tracks.get(from_track);
6716                        let to_track_handle = state.tracks.get(to_track);
6717
6718                        if let (Some(from_device), Some(to_device)) =
6719                            (from_hw_in_device, to_hw_out_device)
6720                        {
6721                            let route = MidiHwThruRoute {
6722                                from_device: from_device.to_string(),
6723                                to_device: to_device.to_string(),
6724                            };
6725                            if !self.midi_hw_thru_routes.iter().any(|r| r == &route) {
6726                                self.midi_hw_thru_routes.push(route);
6727                            }
6728                        } else if let Some(device) = from_hw_in_device {
6729                            if let Some(t_t) = to_track_handle {
6730                                if t_t.lock().midi.ins.get(to_port).is_none() {
6731                                    self.notify_clients(Err(format!(
6732                                        "MIDI input port {} not found on track '{}'",
6733                                        to_port, to_track
6734                                    )))
6735                                    .await;
6736                                    return;
6737                                }
6738                                let route = MidiHwInRoute {
6739                                    device: device.to_string(),
6740                                    to_track: to_track.to_string(),
6741                                    to_port,
6742                                };
6743                                if !self.midi_hw_in_routes.iter().any(|r| r == &route) {
6744                                    self.midi_hw_in_routes.push(route);
6745                                }
6746                            } else {
6747                                self.notify_clients(Err(format!(
6748                                    "MIDI destination track not found: {}",
6749                                    to_track
6750                                )))
6751                                .await;
6752                                return;
6753                            }
6754                        } else if let Some(device) = to_hw_out_device {
6755                            if let Some(f_t) = from_track_handle {
6756                                if f_t.lock().midi.outs.get(from_port).is_none() {
6757                                    self.notify_clients(Err(format!(
6758                                        "MIDI output port {} not found on track '{}'",
6759                                        from_port, from_track
6760                                    )))
6761                                    .await;
6762                                    return;
6763                                }
6764                                let route = MidiHwOutRoute {
6765                                    from_track: from_track.to_string(),
6766                                    from_port,
6767                                    device: device.to_string(),
6768                                };
6769                                if !self.midi_hw_out_routes.iter().any(|r| r == &route) {
6770                                    self.midi_hw_out_routes.push(route);
6771                                }
6772                            } else {
6773                                self.notify_clients(Err(format!(
6774                                    "MIDI source track not found: {}",
6775                                    from_track
6776                                )))
6777                                .await;
6778                                return;
6779                            }
6780                        } else {
6781                            match (from_track_handle, to_track_handle) {
6782                                (Some(f_t), Some(t_t)) => {
6783                                    let to_in_res = t_t.lock().midi.ins.get(to_port).cloned();
6784                                    if let Some(to_in) = to_in_res {
6785                                        let from_track = f_t.lock();
6786                                        if let Err(e) =
6787                                            from_track.midi.connect_out(from_port, to_in)
6788                                        {
6789                                            self.notify_clients(Err(e)).await;
6790                                            return;
6791                                        }
6792                                        from_track.invalidate_midi_route_cache();
6793                                    } else {
6794                                        self.notify_clients(Err(format!(
6795                                            "MIDI input port {} not found on track '{}'",
6796                                            to_port, to_track
6797                                        )))
6798                                        .await;
6799                                        return;
6800                                    }
6801                                }
6802                                _ => {
6803                                    self.notify_clients(Err(format!(
6804                                        "MIDI tracks not found: {} or {}",
6805                                        from_track, to_track
6806                                    )))
6807                                    .await;
6808                                    return;
6809                                }
6810                            }
6811                        }
6812                    }
6813                };
6814            }
6815            Action::Disconnect {
6816                ref from_track,
6817                from_port,
6818                ref to_track,
6819                to_port,
6820                kind,
6821            } => {
6822                if kind == Kind::Audio {
6823                    if let Err(e) = self.disconnect_audio_route_and_notify(a.clone()).await {
6824                        self.notify_clients(Err(e)).await;
6825                    }
6826                } else if kind == Kind::MIDI {
6827                    let from_hw_in_device = Self::midi_hw_in_device(from_track);
6828                    let to_hw_out_device = Self::midi_hw_out_device(to_track);
6829
6830                    if let (Some(from_device), Some(to_device)) =
6831                        (from_hw_in_device, to_hw_out_device)
6832                    {
6833                        let before = self.midi_hw_thru_routes.len();
6834                        self.midi_hw_thru_routes.retain(|r| {
6835                            !(r.from_device == from_device && r.to_device == to_device)
6836                        });
6837                        if self.midi_hw_thru_routes.len() < before {
6838                            self.notify_clients(Ok(a.clone())).await;
6839                        } else {
6840                            self.notify_clients(Err(format!(
6841                                "Disconnect failed: MIDI route not found ({} -> {})",
6842                                from_track, to_track
6843                            )))
6844                            .await;
6845                        }
6846                        return;
6847                    }
6848
6849                    if let Some(device) = from_hw_in_device {
6850                        let before = self.midi_hw_in_routes.len();
6851                        self.midi_hw_in_routes.retain(|r| {
6852                            !(r.device == device && r.to_track == *to_track && r.to_port == to_port)
6853                        });
6854                        if self.midi_hw_in_routes.len() < before {
6855                            self.notify_clients(Ok(a.clone())).await;
6856                        } else {
6857                            self.notify_clients(Err(format!(
6858                                "Disconnect failed: MIDI route not found ({} -> {})",
6859                                from_track, to_track
6860                            )))
6861                            .await;
6862                        }
6863                        return;
6864                    }
6865
6866                    if let Some(device) = to_hw_out_device {
6867                        let before = self.midi_hw_out_routes.len();
6868                        self.midi_hw_out_routes.retain(|r| {
6869                            !(r.from_track == *from_track
6870                                && r.from_port == from_port
6871                                && r.device == device)
6872                        });
6873                        if self.midi_hw_out_routes.len() < before {
6874                            self.notify_clients(Ok(a.clone())).await;
6875                        } else {
6876                            self.notify_clients(Err(format!(
6877                                "Disconnect failed: MIDI route not found ({} -> {})",
6878                                from_track, to_track
6879                            )))
6880                            .await;
6881                        }
6882                        return;
6883                    }
6884
6885                    let state = self.state.lock();
6886                    if let (Some(f_t), Some(t_t)) =
6887                        (state.tracks.get(from_track), state.tracks.get(to_track))
6888                        && let Some(to_in) = t_t.lock().midi.ins.get(to_port).cloned()
6889                    {
6890                        let from_track = f_t.lock();
6891                        if let Err(e) = from_track.midi.disconnect_out(from_port, &to_in) {
6892                            self.notify_clients(Err(e)).await;
6893                        } else {
6894                            from_track.invalidate_midi_route_cache();
6895                            self.notify_clients(Ok(a.clone())).await;
6896                        }
6897                    } else {
6898                        self.notify_clients(Err(format!(
6899                            "Disconnect failed: MIDI ports not found ({} -> {})",
6900                            from_track, to_track
6901                        )))
6902                        .await;
6903                    }
6904                }
6905            }
6906
6907            Action::OpenAudioDevice {
6908                ref device,
6909                ref input_device,
6910                sample_rate_hz,
6911                bits,
6912                exclusive,
6913                period_frames,
6914                realtime_frames,
6915                low_watermark_frames,
6916                nperiods,
6917                sync_mode,
6918                hybrid_enabled,
6919            } => {
6920                #[cfg(unix)]
6921                {
6922                    let request = AudioOpenRequest {
6923                        device,
6924                        input_device: input_device.as_deref(),
6925                        sample_rate_hz,
6926                        bits,
6927                        exclusive,
6928                        period_frames,
6929                        realtime_frames,
6930                        low_watermark_frames,
6931                        nperiods,
6932                        sync_mode,
6933                        hybrid_enabled,
6934                    };
6935                    if self.maybe_open_jack_runtime(request).await.is_some() {
6936                        return;
6937                    }
6938                }
6939                let hw_opts =
6940                    Self::build_hw_options(exclusive, realtime_frames, nperiods, sync_mode);
6941                self.hybrid_playback_frames = period_frames.max(1);
6942                self.hybrid_realtime_frames = realtime_frames.max(1);
6943                self.hybrid_low_watermark_frames = low_watermark_frames.max(1);
6944                self.hybrid_enabled = hybrid_enabled;
6945                let open_result = self
6946                    .open_non_jack_audio_device(
6947                        device,
6948                        input_device.as_deref(),
6949                        sample_rate_hz,
6950                        bits,
6951                        hw_opts,
6952                    )
6953                    .await;
6954                match open_result {
6955                    Ok(()) => {}
6956                    Err(e) => {
6957                        error!("Failed to open audio device: {e}");
6958                        self.notify_clients(Err(e)).await;
6959                        return;
6960                    }
6961                }
6962                {
6963                    let state = self.state.lock();
6964                    for track in state.tracks.values() {
6965                        track.lock().set_hybrid_enabled(hybrid_enabled);
6966                    }
6967                }
6968                self.finalize_open_audio_device().await;
6969            }
6970            Action::JackAddAudioInputPort => {
6971                #[cfg(unix)]
6972                {
6973                    if let Some(jack) = self.jack_runtime.clone() {
6974                        let (input_channels, output_channels, rate) = {
6975                            let jack = jack.lock();
6976                            if let Err(e) = jack.add_audio_input_port() {
6977                                self.notify_clients(Err(e)).await;
6978                                return;
6979                            }
6980                            (
6981                                jack.input_channels(),
6982                                jack.output_channels(),
6983                                jack.sample_rate,
6984                            )
6985                        };
6986                        self.publish_hw_infos(input_channels, output_channels, rate)
6987                            .await;
6988                        self.notify_clients(Ok(a.clone())).await;
6989                    } else {
6990                        self.notify_clients(Err(
6991                            "JACK runtime is not active; open the JACK backend first".to_string(),
6992                        ))
6993                        .await;
6994                    }
6995                }
6996                #[cfg(not(unix))]
6997                {
6998                    self.notify_clients(Err(
6999                        "JACK backend is not available on this platform build".to_string(),
7000                    ))
7001                    .await;
7002                }
7003            }
7004            Action::JackRemoveAudioInputPort(_removed_port) => {
7005                #[cfg(unix)]
7006                {
7007                    let removed_port = _removed_port;
7008                    if let Some(jack) = self.jack_runtime.clone() {
7009                        let (removed_port, removed_io) = {
7010                            let jack = jack.lock();
7011                            let removed_port = Some(removed_port);
7012                            let removed_io =
7013                                removed_port.and_then(|port| jack.input_audio_port(port));
7014                            match (removed_port, removed_io) {
7015                                (Some(port), Some(io)) => (port, io),
7016                                _ => {
7017                                    self.notify_clients(Err(
7018                                        "JACK audio input port index is out of range".to_string(),
7019                                    ))
7020                                    .await;
7021                                    return;
7022                                }
7023                            }
7024                        };
7025                        let reindex_notifications =
7026                            self.reindex_notifications_for_removed_hw_input(removed_port);
7027                        for disconnect in
7028                            self.disconnect_actions_for_removed_hw_input(removed_port, &removed_io)
7029                        {
7030                            if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await
7031                            {
7032                                self.notify_clients(Err(e)).await;
7033                                return;
7034                            }
7035                        }
7036                        let (input_channels, output_channels, rate) = {
7037                            let jack = jack.lock();
7038                            if let Err(e) = jack.remove_audio_input_port(removed_port) {
7039                                self.notify_clients(Err(e)).await;
7040                                return;
7041                            }
7042                            (
7043                                jack.input_channels(),
7044                                jack.output_channels(),
7045                                jack.sample_rate,
7046                            )
7047                        };
7048                        for action in reindex_notifications {
7049                            self.notify_clients(Ok(action)).await;
7050                        }
7051                        self.publish_hw_infos(input_channels, output_channels, rate)
7052                            .await;
7053                        self.notify_clients(Ok(a.clone())).await;
7054                    } else {
7055                        self.notify_clients(Err(
7056                            "JACK runtime is not active; open the JACK backend first".to_string(),
7057                        ))
7058                        .await;
7059                    }
7060                }
7061                #[cfg(not(unix))]
7062                {
7063                    self.notify_clients(Err(
7064                        "JACK backend is not available on this platform build".to_string(),
7065                    ))
7066                    .await;
7067                }
7068            }
7069            Action::JackAddAudioOutputPort => {
7070                #[cfg(unix)]
7071                {
7072                    if let Some(jack) = self.jack_runtime.clone() {
7073                        let (input_channels, output_channels, rate) = {
7074                            let jack = jack.lock();
7075                            if let Err(e) = jack.add_audio_output_port() {
7076                                self.notify_clients(Err(e)).await;
7077                                return;
7078                            }
7079                            (
7080                                jack.input_channels(),
7081                                jack.output_channels(),
7082                                jack.sample_rate,
7083                            )
7084                        };
7085                        self.publish_hw_infos(input_channels, output_channels, rate)
7086                            .await;
7087                        self.notify_clients(Ok(a.clone())).await;
7088                    } else {
7089                        self.notify_clients(Err(
7090                            "JACK runtime is not active; open the JACK backend first".to_string(),
7091                        ))
7092                        .await;
7093                    }
7094                }
7095                #[cfg(not(unix))]
7096                {
7097                    self.notify_clients(Err(
7098                        "JACK backend is not available on this platform build".to_string(),
7099                    ))
7100                    .await;
7101                }
7102            }
7103            Action::JackRemoveAudioOutputPort(_removed_port) => {
7104                #[cfg(unix)]
7105                {
7106                    let removed_port = _removed_port;
7107                    if let Some(jack) = self.jack_runtime.clone() {
7108                        let (removed_port, removed_io) = {
7109                            let jack = jack.lock();
7110                            let removed_port = Some(removed_port);
7111                            let removed_io =
7112                                removed_port.and_then(|port| jack.output_audio_port(port));
7113                            match (removed_port, removed_io) {
7114                                (Some(port), Some(io)) => (port, io),
7115                                _ => {
7116                                    self.notify_clients(Err(
7117                                        "JACK audio output port index is out of range".to_string(),
7118                                    ))
7119                                    .await;
7120                                    return;
7121                                }
7122                            }
7123                        };
7124                        let reindex_notifications =
7125                            self.reindex_notifications_for_removed_hw_output(removed_port);
7126                        for disconnect in
7127                            self.disconnect_actions_for_removed_hw_output(removed_port, &removed_io)
7128                        {
7129                            if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await
7130                            {
7131                                self.notify_clients(Err(e)).await;
7132                                return;
7133                            }
7134                        }
7135                        let (input_channels, output_channels, rate) = {
7136                            let jack = jack.lock();
7137                            if let Err(e) = jack.remove_audio_output_port(removed_port) {
7138                                self.notify_clients(Err(e)).await;
7139                                return;
7140                            }
7141                            (
7142                                jack.input_channels(),
7143                                jack.output_channels(),
7144                                jack.sample_rate,
7145                            )
7146                        };
7147                        for action in reindex_notifications {
7148                            self.notify_clients(Ok(action)).await;
7149                        }
7150                        self.publish_hw_infos(input_channels, output_channels, rate)
7151                            .await;
7152                        self.notify_clients(Ok(a.clone())).await;
7153                    } else {
7154                        self.notify_clients(Err(
7155                            "JACK runtime is not active; open the JACK backend first".to_string(),
7156                        ))
7157                        .await;
7158                    }
7159                }
7160                #[cfg(not(unix))]
7161                {
7162                    self.notify_clients(Err(
7163                        "JACK backend is not available on this platform build".to_string(),
7164                    ))
7165                    .await;
7166                }
7167            }
7168            Action::OpenMidiInputDevice(ref device) => {
7169                let midi_hub = self.midi_hub.lock();
7170                if let Err(e) = midi_hub.open_input(device) {
7171                    self.notify_clients(Err(e)).await;
7172                    return;
7173                }
7174            }
7175            Action::OpenMidiOutputDevice(ref device) => {
7176                let midi_hub = self.midi_hub.lock();
7177                if let Err(e) = midi_hub.open_output(device) {
7178                    self.notify_clients(Err(e)).await;
7179                    return;
7180                }
7181            }
7182            Action::RequestSessionDiagnostics => {
7183                let (
7184                    track_count,
7185                    frozen_track_count,
7186                    audio_clip_count,
7187                    midi_clip_count,
7188                    lv2_instance_count,
7189                    vst3_instance_count,
7190                    clap_instance_count,
7191                ) = {
7192                    let tracks = &self.state.lock().tracks;
7193                    let mut track_count = 0usize;
7194                    let mut frozen_track_count = 0usize;
7195                    let mut audio_clip_count = 0usize;
7196                    let mut midi_clip_count = 0usize;
7197                    #[cfg(all(unix, not(target_os = "macos")))]
7198                    let mut lv2_instance_count = 0usize;
7199                    #[cfg(not(all(unix, not(target_os = "macos"))))]
7200                    let lv2_instance_count = 0usize;
7201                    let mut vst3_instance_count = 0usize;
7202                    let mut clap_instance_count = 0usize;
7203                    for track in tracks.values() {
7204                        let t = track.lock();
7205                        track_count += 1;
7206                        if t.frozen {
7207                            frozen_track_count += 1;
7208                        }
7209                        audio_clip_count += t.audio.clips.len();
7210                        midi_clip_count += t.midi.clips.len();
7211                        #[cfg(all(unix, not(target_os = "macos")))]
7212                        {
7213                            lv2_instance_count += t.lv2_plugins.len();
7214                        }
7215                        vst3_instance_count += t.vst3_plugins.len();
7216                        clap_instance_count += t.clap_plugins.len();
7217                    }
7218                    (
7219                        track_count,
7220                        frozen_track_count,
7221                        audio_clip_count,
7222                        midi_clip_count,
7223                        lv2_instance_count,
7224                        vst3_instance_count,
7225                        clap_instance_count,
7226                    )
7227                };
7228                #[cfg(not(all(unix, not(target_os = "macos"))))]
7229                let _lv2_instance_count = lv2_instance_count;
7230                let pending_hw_midi_events = self.pending_hw_midi_events.len()
7231                    + self
7232                        .pending_hw_midi_events_by_device
7233                        .values()
7234                        .map(std::vec::Vec::len)
7235                        .sum::<usize>();
7236                let sample_rate_hz = if let Some(hw) = &self.hw_driver {
7237                    hw.lock().sample_rate() as usize
7238                } else {
7239                    #[cfg(unix)]
7240                    {
7241                        self.jack_runtime
7242                            .as_ref()
7243                            .map(|j| j.lock().sample_rate)
7244                            .unwrap_or(0)
7245                    }
7246                    #[cfg(not(unix))]
7247                    0
7248                };
7249                let cycle_samples = self.current_cycle_samples();
7250                tracing::info!(
7251                    "Hybrid diagnostics: refill_budget_per_pass={}, refill_budget_throttle_count={}, realtime_fallback_dispatch_count={}, realtime_ready={}, refill_ready={}",
7252                    self.refill_budget_per_pass,
7253                    self.refill_budget_throttle_count,
7254                    self.realtime_fallback_dispatch_count,
7255                    self.ready_realtime_workers.len(),
7256                    self.ready_refill_workers.len()
7257                );
7258                self.notify_clients(Ok(Action::SessionDiagnosticsReport {
7259                    track_count,
7260                    frozen_track_count,
7261                    audio_clip_count,
7262                    midi_clip_count,
7263                    #[cfg(all(unix, not(target_os = "macos")))]
7264                    lv2_instance_count,
7265                    vst3_instance_count,
7266                    clap_instance_count,
7267                    pending_requests: self.pending_requests.len(),
7268                    workers_total: self.workers.len(),
7269                    workers_ready: self.ready_realtime_workers.len()
7270                        + self.ready_refill_workers.len(),
7271                    pending_hw_midi_events,
7272                    playing: self.playing,
7273                    transport_sample: self.transport_sample,
7274                    tempo_bpm: self.tempo_bpm,
7275                    sample_rate_hz,
7276                    cycle_samples,
7277                }))
7278                .await;
7279            }
7280            Action::RequestMidiLearnMappingsReport => {
7281                let mut lines = Vec::<String>::new();
7282                let fmt_binding = |b: &crate::message::MidiLearnBinding| {
7283                    let device = b.device.as_deref().unwrap_or("*");
7284                    format!("{device} CH{} CC{}", b.channel + 1, b.cc)
7285                };
7286                if let Some(b) = self.global_midi_learn_play_pause.as_ref() {
7287                    lines.push(format!("Global PlayPause: {}", fmt_binding(b)));
7288                }
7289                if let Some(b) = self.global_midi_learn_stop.as_ref() {
7290                    lines.push(format!("Global Stop: {}", fmt_binding(b)));
7291                }
7292                if let Some(b) = self.global_midi_learn_record_toggle.as_ref() {
7293                    lines.push(format!("Global RecordToggle: {}", fmt_binding(b)));
7294                }
7295                for (track_name, track) in self.state.lock().tracks.iter() {
7296                    let t = track.lock();
7297                    if let Some(b) = t.midi_learn_volume.as_ref() {
7298                        lines.push(format!("{} Volume: {}", track_name, fmt_binding(b)));
7299                    }
7300                    if let Some(b) = t.midi_learn_balance.as_ref() {
7301                        lines.push(format!("{} Balance: {}", track_name, fmt_binding(b)));
7302                    }
7303                    if let Some(b) = t.midi_learn_mute.as_ref() {
7304                        lines.push(format!("{} Mute: {}", track_name, fmt_binding(b)));
7305                    }
7306                    if let Some(b) = t.midi_learn_solo.as_ref() {
7307                        lines.push(format!("{} Solo: {}", track_name, fmt_binding(b)));
7308                    }
7309                    if let Some(b) = t.midi_learn_arm.as_ref() {
7310                        lines.push(format!("{} Arm: {}", track_name, fmt_binding(b)));
7311                    }
7312                    if let Some(b) = t.midi_learn_input_monitor.as_ref() {
7313                        lines.push(format!("{} InputMonitor: {}", track_name, fmt_binding(b)));
7314                    }
7315                    if let Some(b) = t.midi_learn_disk_monitor.as_ref() {
7316                        lines.push(format!("{} DiskMonitor: {}", track_name, fmt_binding(b)));
7317                    }
7318                }
7319                if lines.is_empty() {
7320                    lines.push("No MIDI learn mappings configured".to_string());
7321                }
7322                self.notify_clients(Ok(Action::MidiLearnMappingsReport { lines }))
7323                    .await;
7324            }
7325            Action::ClearAllMidiLearnBindings => {
7326                self.pending_midi_learn = None;
7327                self.pending_global_midi_learn = None;
7328                self.global_midi_learn_play_pause = None;
7329                self.global_midi_learn_stop = None;
7330                self.global_midi_learn_record_toggle = None;
7331                self.midi_cc_gate.clear();
7332                for track in self.state.lock().tracks.values() {
7333                    let t = track.lock();
7334                    t.midi_learn_volume = None;
7335                    t.midi_learn_balance = None;
7336                    t.midi_learn_mute = None;
7337                    t.midi_learn_solo = None;
7338                    t.midi_learn_arm = None;
7339                    t.midi_learn_input_monitor = None;
7340                    t.midi_learn_disk_monitor = None;
7341                }
7342            }
7343            #[cfg(all(unix, not(target_os = "macos")))]
7344            Action::TrackLv2PluginControls { .. } => {}
7345            #[cfg(all(unix, not(target_os = "macos")))]
7346            Action::ClipLv2PluginControls { .. } => {}
7347            #[cfg(all(unix, not(target_os = "macos")))]
7348            Action::TrackLv2Midnam { .. } => {}
7349            Action::TrackClapNoteNames { .. } => {}
7350            Action::SessionDiagnosticsReport { .. } => {}
7351            Action::MidiLearnMappingsReport { .. } => {}
7352            Action::HWInfo { .. } => {}
7353            Action::HistoryState { .. } => {}
7354            Action::Undo => {}
7355            Action::Redo => {}
7356            Action::ApplyGroupedActions(_) => {}
7357            _ => {}
7358        }
7359
7360        if let Some(inverse) = inverse_actions {
7361            if let Some(group) = self.history_group.as_mut() {
7362                group.forward_actions.push(action_to_process.clone());
7363                group.inverse_actions.splice(0..0, inverse);
7364            } else {
7365                self.history.record(UndoEntry {
7366                    forward_actions: vec![action_to_process.clone()],
7367                    inverse_actions: inverse,
7368                });
7369            }
7370        }
7371
7372        self.notify_clients(Ok(action_to_process)).await;
7373    }
7374
7375    pub async fn work(&mut self) {
7376        while let Some(message) = self.rx.recv().await {
7377            match message {
7378                Message::Ready(id) => self.push_ready_worker(id),
7379                Message::Finished {
7380                    worker_id,
7381                    track_name,
7382                    output_linear,
7383                    process_epoch,
7384                    parameter_updates,
7385                } => {
7386                    self.push_ready_worker(worker_id);
7387                    self.track_processing_started_at.remove(&track_name);
7388                    if process_epoch != self.track_process_epoch {
7389                        if let Some(track) = self.state.lock().tracks.get(&track_name).cloned() {
7390                            let t = track.lock();
7391                            t.audio.finished = false;
7392                            t.audio.processing = false;
7393                        }
7394                        continue;
7395                    }
7396                    self.track_meter_linear_by_track
7397                        .insert(track_name, output_linear);
7398                    for action in parameter_updates {
7399                        self.notify_clients(Ok(action)).await;
7400                    }
7401                    self.force_stalled_track_completions();
7402                    let all_finished = self.send_tracks().await;
7403                    if all_finished {
7404                        self.on_all_tracks_finished().await;
7405                    }
7406                }
7407                Message::Channel(s) => {
7408                    self.clients.push(s);
7409                }
7410
7411                Message::Request(a) => match a {
7412                    Action::TrackOfflineBounceCancel { track_name } => {
7413                        if let Some(job) = self.offline_bounce_jobs.get(&track_name) {
7414                            job.cancel.store(true, Ordering::Relaxed);
7415                        }
7416                    }
7417                    Action::TrackOfflineBounceCancelAll => {
7418                        for job in self.offline_bounce_jobs.values() {
7419                            job.cancel.store(true, Ordering::Relaxed);
7420                        }
7421                    }
7422                    _ if !self.offline_bounce_jobs.is_empty() => {
7423                        self.pending_requests.push_back(a);
7424                    }
7425                    Action::OpenAudioDevice { .. }
7426                    | Action::OpenMidiInputDevice(_)
7427                    | Action::OpenMidiOutputDevice(_)
7428                    | Action::RequestMeterSnapshot
7429                    | Action::Quit
7430                    | Action::Play
7431                    | Action::Pause
7432                    | Action::Stop
7433                    | Action::TransportPosition(_)
7434                    | Action::JumpToEnd
7435                    | Action::SetLoopEnabled(_)
7436                    | Action::SetLoopRange(_)
7437                    | Action::SetPunchEnabled(_)
7438                    | Action::SetPunchRange(_)
7439                    | Action::SetMetronomeEnabled(_)
7440                    | Action::SetTempo(_)
7441                    | Action::SetTimeSignature { .. }
7442                    | Action::SetOscEnabled(_)
7443                    | Action::SetClipPlaybackEnabled(_)
7444                    | Action::SetRecordEnabled(_)
7445                    | Action::SetSessionPath(_)
7446                    | Action::ClearHistory
7447                    | Action::BeginSessionRestore
7448                    | Action::PianoKey { .. }
7449                    | Action::ModifyMidiNotes { .. }
7450                    | Action::ModifyMidiControllers { .. }
7451                    | Action::DeleteMidiControllers { .. }
7452                    | Action::InsertMidiControllers { .. }
7453                    | Action::DeleteMidiNotes { .. }
7454                    | Action::InsertMidiNotes { .. }
7455                    | Action::SetMidiSysExEvents { .. } => {
7456                        self.handle_request(a).await;
7457                    }
7458                    #[cfg(all(unix, not(target_os = "macos")))]
7459                    Action::ListLv2Plugins => {
7460                        self.handle_request(a).await;
7461                    }
7462                    Action::ListVst3Plugins => {
7463                        self.handle_request(a).await;
7464                    }
7465                    Action::ListClapPlugins => {
7466                        self.handle_request(a).await;
7467                    }
7468                    Action::ListClapPluginsWithCapabilities => {
7469                        self.handle_request(a).await;
7470                    }
7471                    _ => {
7472                        self.pending_requests.push_back(a);
7473                        if self.can_schedule_hw_cycle() {
7474                            self.request_hw_cycle().await;
7475                        } else {
7476                            while let Some(next) = self.pending_requests.pop_front() {
7477                                self.handle_request(next).await;
7478                            }
7479                        }
7480                    }
7481                },
7482                Message::OfflineBounceFinished { result } => {
7483                    if let Ok(Action::TrackOfflineBounce { track_name, .. }) = &result {
7484                        self.offline_bounce_jobs.remove(track_name);
7485                    }
7486                    self.notify_clients(result).await;
7487                    if self.offline_bounce_jobs.is_empty() {
7488                        while let Some(next) = self.pending_requests.pop_front() {
7489                            self.handle_request(next).await;
7490                        }
7491                    }
7492                }
7493                Message::HWFinished => {
7494                    if !self.awaiting_hwfinished {
7495                        continue;
7496                    }
7497                    self.handling_hwfinished = true;
7498                    self.awaiting_hwfinished = false;
7499                    #[cfg(unix)]
7500                    {
7501                        if let Some(jack) = &self.jack_runtime {
7502                            if !self.pending_hw_midi_out_events.is_empty() {
7503                                let out_events =
7504                                    std::mem::take(&mut self.pending_hw_midi_out_events);
7505                                jack.lock().write_events(&out_events);
7506                            }
7507                            let mut in_events = vec![];
7508                            jack.lock().read_events_into(&mut in_events);
7509                            if !in_events.is_empty() {
7510                                self.pending_hw_midi_events.extend(in_events);
7511                            }
7512                        }
7513                    }
7514                    #[cfg(unix)]
7515                    if self.jack_runtime.is_some() {
7516                        self.sync_from_jack_transport().await;
7517                    }
7518                    while let Some(a) = self.pending_requests.pop_front() {
7519                        self.handle_request(a).await;
7520                    }
7521                    self.apply_mute_solo_policy();
7522                    self.append_recorded_cycle();
7523                    self.flush_completed_recordings().await;
7524                    let hw_in_routes = self.midi_hw_in_routes.clone();
7525                    let pending_hw_in_by_device = self.pending_hw_midi_events_by_device.clone();
7526                    let mut reconfigured_tracks = Vec::new();
7527                    for (track_name, track) in self.state.lock().tracks.iter() {
7528                        let track_lock = track.lock();
7529                        if self.jack_runtime_is_some() {
7530                            if !self.pending_hw_midi_events.is_empty() {
7531                                track_lock.push_hw_midi_events(&self.pending_hw_midi_events);
7532                            }
7533                        } else {
7534                            for route in hw_in_routes.iter().filter(|r| &r.to_track == track_name) {
7535                                if let Some(events) = pending_hw_in_by_device.get(&route.device) {
7536                                    track_lock.push_hw_midi_events_to_port(route.to_port, events);
7537                                }
7538                            }
7539                        }
7540                        if track_lock.setup() {
7541                            reconfigured_tracks.push(track_name.clone());
7542                        }
7543                    }
7544                    self.publish_track_meters().await;
7545                    for track_name in reconfigured_tracks {
7546                        let track = self.state.lock().tracks.get(&track_name).cloned();
7547                        if let Some(track) = track {
7548                            let (plugins, connections) = {
7549                                let track_lock = track.lock();
7550                                (
7551                                    track_lock.plugin_graph_plugins(),
7552                                    track_lock.plugin_graph_connections(),
7553                                )
7554                            };
7555                            self.notify_clients(Ok(Action::TrackPluginGraph {
7556                                track_name: track_name.clone(),
7557                                plugins,
7558                                connections,
7559                            }))
7560                            .await;
7561                        }
7562                    }
7563                    self.pending_hw_midi_events.clear();
7564                    self.pending_hw_midi_events_by_device.clear();
7565                    if self.playing {
7566                        if self.transport_panic_flush_pending {
7567                            self.transport_panic_flush_pending = false;
7568                        } else if self.transport_restart_pending {
7569                            self.transport_restart_pending = false;
7570                        } else {
7571                            let next = self
7572                                .transport_sample
7573                                .saturating_add(self.current_cycle_samples());
7574                            let normalized = self.normalize_transport_sample(next);
7575                            let wrapped = normalized != next;
7576                            self.transport_sample = normalized;
7577                            if wrapped {
7578                                self.notify_clients(Ok(Action::TransportPosition(
7579                                    self.transport_sample,
7580                                )))
7581                                .await;
7582                            }
7583                        }
7584                    }
7585                    if self.send_tracks().await && self.hw_worker.is_some() {
7586                        self.request_hw_cycle().await;
7587                    }
7588                    #[cfg(unix)]
7589                    {
7590                        if self.jack_runtime.is_some() {
7591                            self.awaiting_hwfinished = true;
7592                        }
7593                    }
7594                    self.handling_hwfinished = false;
7595                }
7596                Message::HWMidiEvents(events) => {
7597                    for hw_event in events {
7598                        let thru_targets: Vec<String> = self
7599                            .midi_hw_thru_routes
7600                            .iter()
7601                            .filter(|route| route.from_device == hw_event.device)
7602                            .map(|route| route.to_device.clone())
7603                            .collect();
7604                        for device in thru_targets {
7605                            self.pending_hw_midi_out_events_by_device.push(HwMidiEvent {
7606                                device,
7607                                event: hw_event.event.clone(),
7608                            });
7609                        }
7610                        if hw_event.event.data.len() >= 3 {
7611                            let status = hw_event.event.data[0];
7612                            if status & 0xF0 == 0xB0 {
7613                                let channel = status & 0x0F;
7614                                let cc = hw_event.event.data[1];
7615                                let value = hw_event.event.data[2];
7616                                self.handle_incoming_hw_cc(&hw_event.device, channel, cc, value)
7617                                    .await;
7618                            }
7619                        }
7620                        self.pending_hw_midi_events_by_device
7621                            .entry(hw_event.device)
7622                            .or_default()
7623                            .push(hw_event.event);
7624                    }
7625                }
7626                _ => {}
7627            }
7628        }
7629    }
7630
7631    fn collect_hw_midi_output_events(&self) -> Vec<MidiEvent> {
7632        let mut events = vec![];
7633        for track in self.state.lock().tracks.values() {
7634            events.extend(
7635                track
7636                    .lock()
7637                    .take_hw_midi_out_events()
7638                    .into_iter()
7639                    .map(|evt| evt.event),
7640            );
7641        }
7642        events.sort_by_key(|a| a.frame);
7643        events
7644    }
7645
7646    fn collect_hw_midi_output_events_by_device(&mut self) -> Vec<HwMidiEvent> {
7647        let mut events = Vec::<HwMidiEvent>::new();
7648        let routes = self.midi_hw_out_routes.clone();
7649        let mut events_by_track = HashMap::<String, Vec<crate::track::HwMidiOutEvent>>::new();
7650        {
7651            let state = self.state.lock();
7652            for route in &routes {
7653                if events_by_track.contains_key(&route.from_track) {
7654                    continue;
7655                }
7656                let Some(track) = state.tracks.get(&route.from_track) else {
7657                    continue;
7658                };
7659                events_by_track.insert(
7660                    route.from_track.clone(),
7661                    track.lock().take_hw_midi_out_events(),
7662                );
7663            }
7664        }
7665
7666        for route in routes {
7667            let Some(track_events) = events_by_track.get(&route.from_track) else {
7668                continue;
7669            };
7670            for hw_event in track_events
7671                .iter()
7672                .filter(|evt| evt.port == route.from_port)
7673            {
7674                self.update_active_hw_notes_for_track(
7675                    &route.from_track,
7676                    &route.device,
7677                    &hw_event.event.data,
7678                );
7679                events.push(HwMidiEvent {
7680                    device: route.device.clone(),
7681                    event: hw_event.event.clone(),
7682                });
7683            }
7684        }
7685        events.sort_by(|a, b| {
7686            a.event
7687                .frame
7688                .cmp(&b.event.frame)
7689                .then_with(|| a.device.cmp(&b.device))
7690        });
7691        events
7692    }
7693}
7694
7695#[cfg(test)]
7696mod tests {
7697    use super::*;
7698    use crate::mutex::UnsafeMutex;
7699    use tokio::sync::mpsc::channel;
7700    use tokio::time::{Duration as TokioDuration, timeout};
7701
7702    #[test]
7703    #[cfg(unix)]
7704    fn jack_transport_sync_decision_starts_and_syncs_position_on_external_play() {
7705        let decision = Engine::jack_transport_sync_decision(false, 128, true, 256, 64);
7706
7707        assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Start));
7708        assert_eq!(decision.position_sync, Some(256));
7709    }
7710
7711    #[test]
7712    #[cfg(unix)]
7713    fn jack_transport_sync_decision_stops_and_syncs_position_on_external_stop() {
7714        let decision = Engine::jack_transport_sync_decision(true, 512, false, 96, 64);
7715
7716        assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Stop));
7717        assert_eq!(decision.position_sync, Some(96));
7718    }
7719
7720    #[test]
7721    #[cfg(unix)]
7722    fn jack_transport_sync_decision_ignores_small_rolling_drift() {
7723        let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1040, 64);
7724
7725        assert_eq!(decision.play_sync, None);
7726        assert_eq!(decision.position_sync, None);
7727    }
7728
7729    #[test]
7730    #[cfg(unix)]
7731    fn jack_transport_sync_decision_syncs_large_rolling_jump() {
7732        let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1200, 64);
7733
7734        assert_eq!(decision.play_sync, None);
7735        assert_eq!(decision.position_sync, Some(1200));
7736    }
7737
7738    #[test]
7739    #[cfg(unix)]
7740    fn jack_transport_sync_decision_syncs_locate_while_stopped() {
7741        let decision = Engine::jack_transport_sync_decision(false, 400, false, 900, 64);
7742
7743        assert_eq!(decision.play_sync, None);
7744        assert_eq!(decision.position_sync, Some(900));
7745    }
7746
7747    fn make_engine_with_client() -> (Engine, tokio::sync::mpsc::Receiver<Message>) {
7748        let (engine_tx, engine_rx) = channel(16);
7749        let mut engine = Engine::new(engine_rx, engine_tx);
7750        let (client_tx, client_rx) = channel(16);
7751        engine.clients.push(client_tx);
7752        (engine, client_rx)
7753    }
7754
7755    fn insert_track(engine: &mut Engine, track: Track) {
7756        engine.state.lock().tracks.insert(
7757            track.name.clone(),
7758            Arc::new(UnsafeMutex::new(Box::new(track))),
7759        );
7760    }
7761
7762    fn osc_packet(address: &str) -> Vec<u8> {
7763        fn push_padded_osc_string(packet: &mut Vec<u8>, value: &str) {
7764            packet.extend_from_slice(value.as_bytes());
7765            packet.push(0);
7766            while !packet.len().is_multiple_of(4) {
7767                packet.push(0);
7768            }
7769        }
7770
7771        let mut packet = Vec::new();
7772        push_padded_osc_string(&mut packet, address);
7773        push_padded_osc_string(&mut packet, ",");
7774        packet
7775    }
7776
7777    #[tokio::test]
7778    async fn set_osc_enabled_starts_and_stops_server() {
7779        let (mut engine, _client_rx) = make_engine_with_client();
7780
7781        engine
7782            .set_osc_enabled_with(true, |tx| OscServer::start_on_addr(tx, "127.0.0.1:0"))
7783            .expect("start osc server on ephemeral port");
7784        assert!(engine.osc_server.is_some());
7785
7786        engine
7787            .set_osc_enabled_with(false, OscServer::start)
7788            .expect("stop osc server");
7789        assert!(engine.osc_server.is_none());
7790    }
7791
7792    #[tokio::test]
7793    async fn osc_server_forwards_transport_packets_to_engine_channel() {
7794        let (tx, mut rx) = channel(4);
7795        let mut server =
7796            OscServer::start_on_addr(tx, "127.0.0.1:0").expect("start osc test server");
7797        let socket = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind sender socket");
7798        let packet = osc_packet("/transport/play");
7799        socket
7800            .send_to(&packet, server.listen_addr())
7801            .expect("send osc packet");
7802
7803        let message = timeout(TokioDuration::from_secs(1), rx.recv())
7804            .await
7805            .expect("packet delivery timeout")
7806            .expect("osc message");
7807        match message {
7808            Message::Request(Action::Play) => {}
7809            other => panic!("unexpected osc message: {other:?}"),
7810        }
7811
7812        server.stop();
7813    }
7814
7815    #[tokio::test]
7816    async fn track_offline_bounce_rejects_zero_length_requests() {
7817        let (mut engine, mut client_rx) = make_engine_with_client();
7818        insert_track(
7819            &mut engine,
7820            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
7821        );
7822
7823        engine
7824            .handle_request(Action::TrackOfflineBounce {
7825                track_name: "track".to_string(),
7826                output_path: "/tmp/out.wav".to_string(),
7827                start_sample: 0,
7828                length_samples: 0,
7829                automation_lanes: vec![],
7830                apply_fader: false,
7831            })
7832            .await;
7833
7834        match client_rx.recv().await.expect("response") {
7835            Message::Response(Err(err)) => {
7836                assert!(err.contains("has no renderable content for offline bounce"));
7837            }
7838            other => panic!("unexpected message: {other:?}"),
7839        }
7840    }
7841
7842    #[tokio::test]
7843    async fn track_offline_bounce_rejects_when_same_track_is_active() {
7844        let (mut engine, mut client_rx) = make_engine_with_client();
7845        engine.offline_bounce_jobs.insert(
7846            "other".to_string(),
7847            OfflineBounceJob {
7848                cancel: Arc::new(AtomicBool::new(false)),
7849            },
7850        );
7851
7852        engine
7853            .handle_request(Action::TrackOfflineBounce {
7854                track_name: "other".to_string(),
7855                output_path: "/tmp/out.wav".to_string(),
7856                start_sample: 0,
7857                length_samples: 128,
7858                automation_lanes: vec![],
7859                apply_fader: false,
7860            })
7861            .await;
7862
7863        match client_rx.recv().await.expect("response") {
7864            Message::Response(Err(err)) => {
7865                assert!(err.contains("already in progress"));
7866            }
7867            other => panic!("unexpected message: {other:?}"),
7868        }
7869    }
7870
7871    #[tokio::test]
7872    async fn track_offline_bounce_allows_different_track_concurrently() {
7873        let (mut engine, _client_rx) = make_engine_with_client();
7874        insert_track(
7875            &mut engine,
7876            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
7877        );
7878        engine.offline_bounce_jobs.insert(
7879            "other".to_string(),
7880            OfflineBounceJob {
7881                cancel: Arc::new(AtomicBool::new(false)),
7882            },
7883        );
7884
7885        engine
7886            .handle_request(Action::TrackOfflineBounce {
7887                track_name: "track".to_string(),
7888                output_path: "/tmp/out.wav".to_string(),
7889                start_sample: 0,
7890                length_samples: 128,
7891                automation_lanes: vec![],
7892                apply_fader: false,
7893            })
7894            .await;
7895
7896        assert!(engine.offline_bounce_jobs.contains_key("other"));
7897        assert_eq!(engine.pending_requests.len(), 1);
7898    }
7899
7900    #[tokio::test]
7901    async fn reject_if_track_frozen_sends_error_and_blocks_operation() {
7902        let (mut engine, mut client_rx) = make_engine_with_client();
7903        let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
7904        track.set_frozen(true);
7905        insert_track(&mut engine, track);
7906
7907        let rejected = engine
7908            .reject_if_track_frozen("track", "arming/disarming")
7909            .await;
7910
7911        assert!(rejected);
7912        match client_rx.recv().await.expect("response") {
7913            Message::Response(Err(err)) => {
7914                assert_eq!(err, "Track 'track' is frozen; arming/disarming is blocked");
7915            }
7916            other => panic!("unexpected message: {other:?}"),
7917        }
7918    }
7919
7920    #[tokio::test]
7921    async fn undo_restores_original_clip_bounds_after_stretch_style_group() {
7922        let (mut engine, _client_rx) = make_engine_with_client();
7923        let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
7924        let mut clip = AudioClip::new("audio/original.wav".to_string(), 100, 220);
7925        clip.offset = 12;
7926        clip.fade_in_samples = 20;
7927        clip.fade_out_samples = 30;
7928        track.audio.clips.push(clip);
7929        insert_track(&mut engine, track);
7930
7931        engine.handle_request(Action::BeginHistoryGroup).await;
7932        engine
7933            .handle_request(Action::SetClipBounds {
7934                track_name: "track".to_string(),
7935                clip_index: 0,
7936                kind: Kind::Audio,
7937                start: 120,
7938                length: 180,
7939                offset: 0,
7940            })
7941            .await;
7942        engine
7943            .handle_request(Action::SetClipSourceName {
7944                track_name: "track".to_string(),
7945                clip_index: 0,
7946                kind: Kind::Audio,
7947                name: "audio/stretched.wav".to_string(),
7948            })
7949            .await;
7950        engine
7951            .handle_request(Action::SetClipFade {
7952                track_name: "track".to_string(),
7953                clip_index: 0,
7954                kind: Kind::Audio,
7955                fade_enabled: true,
7956                fade_in_samples: 12,
7957                fade_out_samples: 12,
7958            })
7959            .await;
7960        engine.handle_request(Action::EndHistoryGroup).await;
7961
7962        engine.handle_request(Action::Undo).await;
7963
7964        let state = engine.state.lock();
7965        let track = state.tracks.get("track").expect("track exists").lock();
7966        let clip = track.audio.clips.first().expect("clip exists");
7967        assert_eq!(clip.name, "audio/original.wav");
7968        assert_eq!(clip.start, 100);
7969        assert_eq!(clip.end, 220);
7970        assert_eq!(clip.end.saturating_sub(clip.start), 120);
7971        assert_eq!(clip.offset, 12);
7972    }
7973
7974    #[tokio::test]
7975    async fn track_offline_bounce_queues_when_no_worker_is_ready() {
7976        let (mut engine, _client_rx) = make_engine_with_client();
7977        insert_track(
7978            &mut engine,
7979            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
7980        );
7981
7982        engine
7983            .handle_request(Action::TrackOfflineBounce {
7984                track_name: "track".to_string(),
7985                output_path: "/tmp/out.wav".to_string(),
7986                start_sample: 0,
7987                length_samples: 128,
7988                automation_lanes: vec![],
7989                apply_fader: false,
7990            })
7991            .await;
7992
7993        assert!(engine.offline_bounce_jobs.is_empty());
7994        assert_eq!(engine.pending_requests.len(), 1);
7995        assert!(matches!(
7996            engine.pending_requests.front(),
7997            Some(Action::TrackOfflineBounce { track_name, length_samples, .. })
7998                if track_name == "track" && *length_samples == 128
7999        ));
8000    }
8001
8002    #[tokio::test]
8003    async fn track_offline_bounce_returns_missing_track_error() {
8004        let (mut engine, mut client_rx) = make_engine_with_client();
8005
8006        engine
8007            .handle_request(Action::TrackOfflineBounce {
8008                track_name: "missing".to_string(),
8009                output_path: "/tmp/out.wav".to_string(),
8010                start_sample: 0,
8011                length_samples: 128,
8012                automation_lanes: vec![],
8013                apply_fader: false,
8014            })
8015            .await;
8016
8017        match client_rx.recv().await.expect("response") {
8018            Message::Response(Err(err)) => {
8019                assert_eq!(err, "Track not found: missing");
8020            }
8021            other => panic!("unexpected message: {other:?}"),
8022        }
8023    }
8024
8025    #[tokio::test]
8026    async fn track_offline_bounce_clears_job_when_worker_send_fails() {
8027        let (mut engine, mut client_rx) = make_engine_with_client();
8028        insert_track(
8029            &mut engine,
8030            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
8031        );
8032        let (worker_tx, worker_rx) = channel(1);
8033        drop(worker_rx);
8034        engine
8035            .workers
8036            .push(WorkerData::new(worker_tx, tokio::spawn(async {})));
8037        engine.ready_refill_workers.push(0);
8038
8039        engine
8040            .handle_request(Action::TrackOfflineBounce {
8041                track_name: "track".to_string(),
8042                output_path: "/tmp/out.wav".to_string(),
8043                start_sample: 0,
8044                length_samples: 128,
8045                automation_lanes: vec![],
8046                apply_fader: false,
8047            })
8048            .await;
8049
8050        assert!(engine.offline_bounce_jobs.is_empty());
8051        match client_rx.recv().await.expect("response") {
8052            Message::Response(Err(err)) => {
8053                assert!(err.contains("Failed to schedule offline bounce"));
8054            }
8055            other => panic!("unexpected message: {other:?}"),
8056        }
8057    }
8058}