Skip to main content

maolan_engine/
engine.rs

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