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