Skip to main content

maolan_engine/
engine.rs

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