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