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 spec = hound::WavSpec {
2670            channels: rec.channels as u16,
2671            sample_rate: rate as u32,
2672            bits_per_sample: 32,
2673            sample_format: hound::SampleFormat::Float,
2674        };
2675        let write_result = (|| {
2676            let mut writer = hound::WavWriter::create(&file_path, spec)
2677                .map_err(|e| std::io::Error::other(format!("Failed to create WAV writer: {e}")))?;
2678            for sample in &rec.samples {
2679                writer
2680                    .write_sample(*sample)
2681                    .map_err(|e| std::io::Error::other(format!("Failed to write sample: {e}")))?;
2682            }
2683            writer
2684                .finalize()
2685                .map_err(|e| std::io::Error::other(format!("Failed to finalize WAV: {e}")))?;
2686            Ok::<(), std::io::Error>(())
2687        })();
2688        if let Err(e) = write_result {
2689            self.notify_clients(Err(format!(
2690                "Failed to write recording {}: {}",
2691                file_path.display(),
2692                e
2693            )))
2694            .await;
2695            return;
2696        }
2697        let length = rec.samples.len() / rec.channels;
2698        let clip_rel_name = format!("audio/{}", rec.file_name);
2699        let clip = AudioClip::new(
2700            clip_rel_name.clone(),
2701            rec.start_sample,
2702            rec.start_sample.saturating_add(length.max(1)),
2703        );
2704        let (audio_ins, audio_outs) = if let Some(track) = self.state.lock().tracks.get(&track_name)
2705        {
2706            let track = track.lock();
2707            let audio_ins = track.audio.ins.len();
2708            let audio_outs = track.audio.outs.len();
2709            track.audio.clips.push(clip.clone());
2710            (audio_ins, audio_outs)
2711        } else {
2712            (0, 0)
2713        };
2714        self.notify_clients(Ok(Action::AddClip {
2715            name: clip_rel_name,
2716            track_name: track_name.clone(),
2717            start: rec.start_sample,
2718            length,
2719            offset: 0,
2720            input_channel: 0,
2721            muted: false,
2722            peaks_file: None,
2723            kind: Kind::Audio,
2724            fade_enabled: clip.fade_enabled,
2725            fade_in_samples: clip.fade_in_samples,
2726            fade_out_samples: clip.fade_out_samples,
2727            source_name: None,
2728            source_offset: None,
2729            source_length: None,
2730            preview_name: None,
2731            pitch_correction_points: vec![],
2732            pitch_correction_frame_likeness: None,
2733            pitch_correction_inertia_ms: None,
2734            pitch_correction_formant_compensation: None,
2735            plugin_graph_json: Some(Self::default_clip_plugin_graph_json(audio_ins, audio_outs)),
2736        }))
2737        .await;
2738        if let Some(track) = self.state.lock().tracks.get(&track_name).cloned() {
2739            tokio::task::spawn_blocking(move || {
2740                track.lock().preload_clips();
2741                tracing::debug!("Preloaded clips for track '{}' after recording", track_name);
2742            });
2743        }
2744    }
2745
2746    async fn flush_track_recording(&mut self, track_name: &str) {
2747        let Some(audio_dir) = self.session_audio_dir() else {
2748            self.audio_recordings.remove(track_name);
2749            self.midi_recordings.remove(track_name);
2750            self.completed_audio_recordings
2751                .retain(|(name, _)| name != track_name);
2752            self.completed_midi_recordings
2753                .retain(|(name, _)| name != track_name);
2754            return;
2755        };
2756        let Some(midi_dir) = self.session_midi_dir() else {
2757            self.audio_recordings.remove(track_name);
2758            self.midi_recordings.remove(track_name);
2759            self.completed_audio_recordings
2760                .retain(|(name, _)| name != track_name);
2761            self.completed_midi_recordings
2762                .retain(|(name, _)| name != track_name);
2763            return;
2764        };
2765        if std::fs::create_dir_all(&audio_dir).is_err()
2766            || std::fs::create_dir_all(&midi_dir).is_err()
2767        {
2768            return;
2769        }
2770        let rate = self
2771            .hw_driver
2772            .as_ref()
2773            .map(|o| o.lock().sample_rate())
2774            .unwrap_or(48_000);
2775        let mut i = 0;
2776        while i < self.completed_audio_recordings.len() {
2777            if self.completed_audio_recordings[i].0 == track_name {
2778                let (name, rec) = self.completed_audio_recordings.remove(i);
2779                self.flush_recording_entry(&audio_dir, rate, name, rec)
2780                    .await;
2781            } else {
2782                i += 1;
2783            }
2784        }
2785        let mut j = 0;
2786        while j < self.completed_midi_recordings.len() {
2787            if self.completed_midi_recordings[j].0 == track_name {
2788                let (name, rec) = self.completed_midi_recordings.remove(j);
2789                self.flush_midi_recording_entry(&midi_dir, rate as u32, name, rec)
2790                    .await;
2791            } else {
2792                j += 1;
2793            }
2794        }
2795
2796        let Some(rec) = self.audio_recordings.remove(track_name) else {
2797            if let Some(mrec) = self.midi_recordings.remove(track_name) {
2798                self.flush_midi_recording_entry(
2799                    &midi_dir,
2800                    rate as u32,
2801                    track_name.to_string(),
2802                    mrec,
2803                )
2804                .await;
2805            }
2806            return;
2807        };
2808        self.flush_recording_entry(&audio_dir, rate, track_name.to_string(), rec)
2809            .await;
2810        if let Some(mrec) = self.midi_recordings.remove(track_name) {
2811            self.flush_midi_recording_entry(&midi_dir, rate as u32, track_name.to_string(), mrec)
2812                .await;
2813        }
2814    }
2815
2816    async fn flush_midi_recording_entry(
2817        &mut self,
2818        midi_dir: &Path,
2819        sample_rate: u32,
2820        track_name: String,
2821        mut rec: MidiRecordingSession,
2822    ) {
2823        if rec.events.is_empty() {
2824            return;
2825        }
2826        rec.events.sort_by_key(|(sample, _)| *sample);
2827        let clip_rel_name = format!("midi/{}", rec.file_name);
2828        let clip_len_samples = rec
2829            .events
2830            .last()
2831            .map(|(s, _)| s.saturating_sub(rec.start_sample as u64) as usize + 1)
2832            .unwrap_or(1);
2833
2834        for (sample, _) in &mut rec.events {
2835            *sample = sample.saturating_sub(rec.start_sample as u64);
2836        }
2837        let path = midi_dir.join(&rec.file_name);
2838        if let Err(e) = Self::write_midi_file(&path, sample_rate, &rec.events) {
2839            self.notify_clients(Err(format!(
2840                "Failed to write MIDI recording {}: {}",
2841                path.display(),
2842                e
2843            )))
2844            .await;
2845            return;
2846        }
2847        let mut clip = MIDIClip::new(
2848            clip_rel_name.clone(),
2849            rec.start_sample,
2850            rec.start_sample.saturating_add(clip_len_samples.max(1)),
2851        );
2852        clip.offset = 0;
2853        if let Some(track) = self.state.lock().tracks.get(&track_name) {
2854            track.lock().midi.clips.push(clip);
2855        }
2856        self.notify_clients(Ok(Action::AddClip {
2857            name: clip_rel_name,
2858            track_name: track_name.clone(),
2859            start: rec.start_sample,
2860            length: clip_len_samples,
2861            offset: 0,
2862            input_channel: 0,
2863            muted: false,
2864            peaks_file: None,
2865            kind: Kind::MIDI,
2866            fade_enabled: true,
2867            fade_in_samples: 240,
2868            fade_out_samples: 240,
2869            source_name: None,
2870            source_offset: None,
2871            source_length: None,
2872            preview_name: None,
2873            pitch_correction_points: vec![],
2874            pitch_correction_frame_likeness: None,
2875            pitch_correction_inertia_ms: None,
2876            pitch_correction_formant_compensation: None,
2877            plugin_graph_json: None,
2878        }))
2879        .await;
2880        if let Some(track) = self.state.lock().tracks.get(&track_name).cloned() {
2881            tokio::task::spawn_blocking(move || {
2882                track.lock().preload_clips();
2883                tracing::debug!(
2884                    "Preloaded clips for track '{}' after MIDI recording",
2885                    track_name
2886                );
2887            });
2888        }
2889    }
2890
2891    fn write_midi_file(
2892        path: &Path,
2893        sample_rate: u32,
2894        events: &[(u64, Vec<u8>)],
2895    ) -> Result<(), String> {
2896        let ppq: u16 = 480;
2897        let ticks_per_second: u64 = 960;
2898        let arena = Arena::new();
2899        let mut track_events: Vec<TrackEvent<'_>> = vec![TrackEvent {
2900            delta: u28::new(0),
2901            kind: TrackEventKind::Meta(MetaMessage::Tempo(u24::new(500_000))),
2902        }];
2903        let mut prev_ticks = 0_u64;
2904        for (sample, data) in events {
2905            let ticks = sample.saturating_mul(ticks_per_second) / sample_rate.max(1) as u64;
2906            let delta = ticks.saturating_sub(prev_ticks).min(u32::MAX as u64) as u32;
2907            prev_ticks = ticks;
2908            let Ok(live) = LiveEvent::parse(data) else {
2909                continue;
2910            };
2911            let kind = live.as_track_event(&arena);
2912            track_events.push(TrackEvent {
2913                delta: u28::new(delta),
2914                kind,
2915            });
2916        }
2917        track_events.push(TrackEvent {
2918            delta: u28::new(0),
2919            kind: TrackEventKind::Meta(MetaMessage::EndOfTrack),
2920        });
2921
2922        let smf = Smf {
2923            header: Header::new(Format::SingleTrack, Timing::Metrical(u15::new(ppq))),
2924            tracks: vec![track_events],
2925        };
2926        let mut file = File::create(path).map_err(|e| e.to_string())?;
2927        smf.write_std(&mut file).map_err(|e| e.to_string())
2928    }
2929
2930    pub async fn init(&mut self) {
2931        let max_threads = num_cpus::get();
2932        let realtime_count = if max_threads > 1 { 1 } else { max_threads };
2933        for id in 0..max_threads {
2934            let class = if id < realtime_count {
2935                WorkerClass::Realtime
2936            } else {
2937                WorkerClass::Refill
2938            };
2939            let priority = match class {
2940                WorkerClass::Realtime => 20,
2941                WorkerClass::Refill => 8,
2942            };
2943            let (tx, rx) = channel::<Message>(32);
2944            let tx_thread = self.tx.clone();
2945            let handler = tokio::spawn(async move {
2946                let wrk = Worker::new(id, rx, tx_thread, priority);
2947                wrk.await.work().await;
2948            });
2949            self.worker_classes.push(class);
2950            self.workers.push(WorkerData::new(tx.clone(), handler));
2951        }
2952    }
2953
2954    async fn notify_clients(&mut self, action: Result<Action, String>) {
2955        self.clients.retain(|client| !client.is_closed());
2956        for client in &self.clients {
2957            client
2958                .send(Message::Response(action.clone()))
2959                .await
2960                .expect("Error sending response to client");
2961        }
2962    }
2963
2964    fn set_osc_enabled_with<F>(&mut self, enabled: bool, start_server: F) -> Result<(), String>
2965    where
2966        F: FnOnce(Sender<Message>) -> Result<OscServer, String>,
2967    {
2968        if enabled {
2969            if self.osc_server.is_none() {
2970                self.osc_server = Some(start_server(self.tx.clone())?);
2971            }
2972        } else if let Some(mut server) = self.osc_server.take() {
2973            server.stop();
2974        }
2975        Ok(())
2976    }
2977
2978    fn track_handle_by_name(&self, track_name: &str) -> Option<Arc<UnsafeMutex<Box<Track>>>> {
2979        self.state.lock().tracks.get(track_name).cloned()
2980    }
2981
2982    fn track_handle_or_err(
2983        &self,
2984        track_name: &str,
2985    ) -> Result<Arc<UnsafeMutex<Box<Track>>>, String> {
2986        self.track_handle_by_name(track_name)
2987            .ok_or_else(|| format!("Track not found: {track_name}"))
2988    }
2989
2990    fn add_clip_to_track(&self, request: ClipAddRequest<'_>) {
2991        if let Some(track) = self.state.lock().tracks.get(request.track_name) {
2992            let track = track.lock();
2993            if track.is_master {
2994                return;
2995            }
2996            match request.kind {
2997                Kind::Audio => {
2998                    let mut clip = AudioClip::new(
2999                        request.name.to_string(),
3000                        request.start,
3001                        request.start.saturating_add(request.length.max(1)),
3002                    );
3003                    clip.offset = request.offset;
3004                    let max_lane = track.audio.ins.len().saturating_sub(1);
3005                    clip.input_channel = request.input_channel.min(max_lane);
3006                    clip.muted = request.muted;
3007                    clip.peaks_file = request.peaks_file;
3008                    clip.fade_enabled = request.fade_enabled;
3009                    clip.fade_in_samples = request.fade_in_samples;
3010                    clip.fade_out_samples = request.fade_out_samples;
3011                    clip.pitch_correction_preview_name = request.preview_name;
3012                    clip.pitch_correction_source_name = request.source_name;
3013                    clip.pitch_correction_source_offset = request.source_offset;
3014                    clip.pitch_correction_source_length = request.source_length;
3015                    clip.pitch_correction_points = request.pitch_correction_points;
3016                    clip.pitch_correction_frame_likeness = request.pitch_correction_frame_likeness;
3017                    clip.pitch_correction_inertia_ms = request.pitch_correction_inertia_ms;
3018                    clip.pitch_correction_formant_compensation =
3019                        request.pitch_correction_formant_compensation;
3020                    clip.plugin_graph_json = request.plugin_graph_json;
3021                    track.audio.clips.push(clip);
3022                    #[cfg(unix)]
3023                    track.clip_pitch_shifters.clear();
3024                }
3025                Kind::MIDI => {
3026                    let mut clip = MIDIClip::new(
3027                        request.name.to_string(),
3028                        request.start,
3029                        request.start.saturating_add(request.length.max(1)),
3030                    );
3031                    clip.offset = request.offset;
3032                    let max_lane = track.midi.ins.len().saturating_sub(1);
3033                    clip.input_channel = request.input_channel.min(max_lane);
3034                    clip.muted = request.muted;
3035                    track.midi.clips.push(clip);
3036                }
3037            }
3038        }
3039    }
3040
3041    fn audio_clip_from_data(data: &crate::message::AudioClipData) -> AudioClip {
3042        let mut clip = AudioClip::new(
3043            data.name.clone(),
3044            data.start,
3045            data.start.saturating_add(data.length.max(1)),
3046        );
3047        clip.offset = data.offset;
3048        clip.input_channel = data.input_channel;
3049        clip.muted = data.muted;
3050        clip.peaks_file = data.peaks_file.clone();
3051        clip.fade_enabled = data.fade_enabled;
3052        clip.fade_in_samples = data.fade_in_samples;
3053        clip.fade_out_samples = data.fade_out_samples;
3054        clip.pitch_correction_preview_name = data.preview_name.clone();
3055        clip.pitch_correction_source_name = data.source_name.clone();
3056        clip.pitch_correction_source_offset = data.source_offset;
3057        clip.pitch_correction_source_length = data.source_length;
3058        clip.pitch_correction_points = data.pitch_correction_points.clone();
3059        clip.pitch_correction_frame_likeness = data.pitch_correction_frame_likeness;
3060        clip.pitch_correction_inertia_ms = data.pitch_correction_inertia_ms;
3061        clip.pitch_correction_formant_compensation = data.pitch_correction_formant_compensation;
3062        clip.plugin_graph_json = data.plugin_graph_json.clone();
3063        clip.grouped_clips = data
3064            .grouped_clips
3065            .iter()
3066            .map(Self::audio_clip_from_data)
3067            .collect();
3068        for child in &mut clip.grouped_clips {
3069            child.fade_enabled = false;
3070            child.fade_in_samples = 0;
3071            child.fade_out_samples = 0;
3072        }
3073        clip
3074    }
3075
3076    fn midi_clip_from_data(data: &crate::message::MidiClipData) -> MIDIClip {
3077        let mut clip = MIDIClip::new(
3078            data.name.clone(),
3079            data.start,
3080            data.start.saturating_add(data.length.max(1)),
3081        );
3082        clip.offset = data.offset;
3083        clip.input_channel = data.input_channel;
3084        clip.muted = data.muted;
3085        clip.grouped_clips = data
3086            .grouped_clips
3087            .iter()
3088            .map(Self::midi_clip_from_data)
3089            .collect();
3090        clip
3091    }
3092
3093    fn add_grouped_clip_to_track(
3094        &self,
3095        track_name: &str,
3096        kind: Kind,
3097        audio_clip: Option<crate::message::AudioClipData>,
3098        midi_clip: Option<crate::message::MidiClipData>,
3099    ) {
3100        if let Some(track) = self.state.lock().tracks.get(track_name) {
3101            let track = track.lock();
3102            if track.is_master {
3103                return;
3104            }
3105            match kind {
3106                Kind::Audio => {
3107                    if let Some(mut clip) = audio_clip.map(|clip| Self::audio_clip_from_data(&clip))
3108                    {
3109                        let max_lane = track.audio.ins.len().saturating_sub(1);
3110                        clip.input_channel = clip.input_channel.min(max_lane);
3111                        track.audio.clips.push(clip);
3112                        #[cfg(unix)]
3113                        track.clip_pitch_shifters.clear();
3114                    }
3115                }
3116                Kind::MIDI => {
3117                    if let Some(mut clip) = midi_clip.map(|clip| Self::midi_clip_from_data(&clip)) {
3118                        let max_lane = track.midi.ins.len().saturating_sub(1);
3119                        clip.input_channel = clip.input_channel.min(max_lane);
3120                        track.midi.clips.push(clip);
3121                    }
3122                }
3123            }
3124        }
3125    }
3126
3127    fn remove_clips_from_track(&self, track_name: &str, kind: Kind, clip_indices: &[usize]) {
3128        if let Some(track) = self.state.lock().tracks.get(track_name) {
3129            let track = track.lock();
3130            let mut indices = clip_indices.to_vec();
3131            indices.sort_unstable();
3132            indices.dedup();
3133            match kind {
3134                Kind::Audio => {
3135                    for idx in indices.into_iter().rev() {
3136                        if idx < track.audio.clips.len() {
3137                            track.audio.clips.remove(idx);
3138                        }
3139                    }
3140                    #[cfg(unix)]
3141                    track.clip_pitch_shifters.clear();
3142                }
3143                Kind::MIDI => {
3144                    for idx in indices.into_iter().rev() {
3145                        if idx < track.midi.clips.len() {
3146                            track.midi.clips.remove(idx);
3147                        }
3148                    }
3149                }
3150            }
3151        }
3152    }
3153
3154    fn rename_clip_references(
3155        &self,
3156        track_name: &str,
3157        kind: Kind,
3158        clip_index: usize,
3159        new_name: &str,
3160    ) {
3161        let Some(track) = self.state.lock().tracks.get(track_name) else {
3162            return;
3163        };
3164        let track = track.lock();
3165        let old_name = match kind {
3166            Kind::Audio => {
3167                if clip_index >= track.audio.clips.len() {
3168                    return;
3169                }
3170                track.audio.clips[clip_index].name.clone()
3171            }
3172            Kind::MIDI => {
3173                if clip_index >= track.midi.clips.len() {
3174                    return;
3175                }
3176                track.midi.clips[clip_index].name.clone()
3177            }
3178        };
3179
3180        let new_file_name = match kind {
3181            Kind::Audio => format!("audio/{}.wav", new_name),
3182            Kind::MIDI => {
3183                let ext = std::path::Path::new(&old_name)
3184                    .extension()
3185                    .and_then(|e| e.to_str())
3186                    .map(|s| s.to_ascii_lowercase())
3187                    .filter(|e| e == "mid" || e == "midi")
3188                    .unwrap_or_else(|| "mid".to_string());
3189                format!("midi/{}.{}", new_name, ext)
3190            }
3191        };
3192        let _ = track;
3193
3194        for (_, other_track) in self.state.lock().tracks.iter() {
3195            let other_track = other_track.lock();
3196            match kind {
3197                Kind::Audio => {
3198                    for clip in &mut other_track.audio.clips {
3199                        if clip.name == old_name {
3200                            clip.name = new_file_name.clone();
3201                        }
3202                        if clip.pitch_correction_source_name.as_deref() == Some(old_name.as_str()) {
3203                            clip.pitch_correction_source_name = Some(new_file_name.clone());
3204                        }
3205                    }
3206                }
3207                Kind::MIDI => {
3208                    for clip in &mut other_track.midi.clips {
3209                        if clip.name == old_name {
3210                            clip.name = new_file_name.clone();
3211                        }
3212                    }
3213                }
3214            }
3215        }
3216    }
3217
3218    fn set_clip_fade(
3219        &self,
3220        track_name: &str,
3221        clip_index: usize,
3222        kind: Kind,
3223        fade_enabled: bool,
3224        fade_in_samples: usize,
3225        fade_out_samples: usize,
3226    ) {
3227        let Some(track) = self.state.lock().tracks.get(track_name) else {
3228            return;
3229        };
3230        let track = track.lock();
3231        match kind {
3232            Kind::Audio => {
3233                if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3234                    clip.fade_enabled = fade_enabled;
3235                    clip.fade_in_samples = fade_in_samples;
3236                    clip.fade_out_samples = fade_out_samples;
3237                }
3238            }
3239            Kind::MIDI => {}
3240        }
3241    }
3242
3243    fn set_clip_bounds(
3244        &self,
3245        track_name: &str,
3246        clip_index: usize,
3247        kind: Kind,
3248        start: usize,
3249        length: usize,
3250        offset: usize,
3251    ) {
3252        let Some(track) = self.state.lock().tracks.get(track_name) else {
3253            return;
3254        };
3255        let track = track.lock();
3256        match kind {
3257            Kind::Audio => {
3258                if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3259                    clip.start = start;
3260                    clip.end = start.saturating_add(length.max(1));
3261                    clip.offset = offset;
3262                    clip.pitch_correction_preview_name = None;
3263                    clip.pitch_correction_source_name = None;
3264                    clip.pitch_correction_source_offset = None;
3265                    clip.pitch_correction_source_length = None;
3266                    clip.pitch_correction_points.clear();
3267                    clip.pitch_correction_frame_likeness = None;
3268                    clip.pitch_correction_inertia_ms = None;
3269                    clip.pitch_correction_formant_compensation = None;
3270                }
3271                #[cfg(unix)]
3272                track.clip_pitch_shifters.clear();
3273            }
3274            Kind::MIDI => {
3275                if let Some(clip) = track.midi.clips.get_mut(clip_index) {
3276                    clip.start = start;
3277                    clip.end = start.saturating_add(length.max(1));
3278                    clip.offset = offset;
3279                }
3280            }
3281        }
3282    }
3283
3284    fn set_clip_source_name(&self, track_name: &str, clip_index: usize, kind: Kind, name: String) {
3285        let Some(track) = self.state.lock().tracks.get(track_name) else {
3286            return;
3287        };
3288        let track = track.lock();
3289        match kind {
3290            Kind::Audio => {
3291                if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3292                    clip.name = name;
3293                }
3294                #[cfg(unix)]
3295                track.clip_pitch_shifters.clear();
3296            }
3297            Kind::MIDI => {
3298                if let Some(clip) = track.midi.clips.get_mut(clip_index) {
3299                    clip.name = name;
3300                }
3301            }
3302        }
3303    }
3304
3305    fn set_clip_muted(&self, track_name: &str, clip_index: usize, kind: Kind, muted: bool) {
3306        let Some(track) = self.state.lock().tracks.get(track_name) else {
3307            return;
3308        };
3309        let track = track.lock();
3310        match kind {
3311            Kind::Audio => {
3312                if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3313                    clip.muted = muted;
3314                }
3315            }
3316            Kind::MIDI => {
3317                if let Some(clip) = track.midi.clips.get_mut(clip_index) {
3318                    clip.muted = muted;
3319                }
3320            }
3321        }
3322    }
3323
3324    #[allow(clippy::too_many_arguments)]
3325    fn set_clip_pitch_correction(
3326        &self,
3327        track_name: &str,
3328        clip_index: usize,
3329        preview_name: Option<String>,
3330        source_name: Option<String>,
3331        source_offset: Option<usize>,
3332        source_length: Option<usize>,
3333        pitch_correction_points: Vec<crate::message::PitchCorrectionPointData>,
3334        pitch_correction_frame_likeness: Option<f32>,
3335        pitch_correction_inertia_ms: Option<u16>,
3336        pitch_correction_formant_compensation: Option<bool>,
3337    ) {
3338        if let Some(track) = self.state.lock().tracks.get(track_name) {
3339            let track = track.lock();
3340            if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3341                clip.pitch_correction_preview_name = preview_name;
3342                clip.pitch_correction_source_name = source_name;
3343                clip.pitch_correction_source_offset = source_offset;
3344                clip.pitch_correction_source_length = source_length;
3345                clip.pitch_correction_points = pitch_correction_points;
3346                clip.pitch_correction_frame_likeness = pitch_correction_frame_likeness;
3347                clip.pitch_correction_inertia_ms = pitch_correction_inertia_ms;
3348                clip.pitch_correction_formant_compensation = pitch_correction_formant_compensation;
3349            }
3350            #[cfg(unix)]
3351            track.clip_pitch_shifters.clear();
3352        }
3353    }
3354
3355    async fn request_hw_cycle(&mut self) {
3356        if self.awaiting_hwfinished {
3357            return;
3358        }
3359        self.apply_hw_out_gain_and_meter().await;
3360        if let Some(worker) = &self.hw_worker {
3361            if !self.pending_hw_midi_out_events_by_device.is_empty() {
3362                let out_events = std::mem::take(&mut self.pending_hw_midi_out_events_by_device);
3363                if let Err(e) = worker.tx.send(Message::HWMidiOutEvents(out_events)).await {
3364                    error!("Error sending HWMidiOutEvents {e}");
3365                }
3366            }
3367            match worker.tx.send(Message::TracksFinished).await {
3368                Ok(_) => {
3369                    self.awaiting_hwfinished = true;
3370                }
3371                Err(e) => {
3372                    error!("Error sending TracksFinished {e}");
3373                }
3374            }
3375        }
3376    }
3377
3378    async fn clear_hw_midi_output_state(&mut self, send_panic: bool) {
3379        self.pending_hw_midi_out_events.clear();
3380        self.pending_hw_midi_out_events_by_device.clear();
3381        {
3382            let state = self.state.lock();
3383            for track in state.tracks.values() {
3384                track.lock().take_hw_midi_out_events();
3385            }
3386        }
3387
3388        let panic_events = if send_panic {
3389            self.note_off_events_for_all_active_tracks()
3390        } else {
3391            vec![]
3392        };
3393
3394        if let Some(worker) = &self.hw_worker {
3395            if let Err(e) = worker.tx.send(Message::ClearHWMidiOutEvents).await {
3396                error!("Error clearing pending HWMidiOutEvents {e}");
3397            }
3398            if !panic_events.is_empty()
3399                && let Err(e) = worker.tx.send(Message::HWMidiOutEvents(panic_events)).await
3400            {
3401                error!("Error sending transport restart MIDI panic events {e}");
3402            }
3403        } else if !panic_events.is_empty() {
3404            self.pending_hw_midi_out_events_by_device
3405                .extend(panic_events);
3406        }
3407    }
3408
3409    fn invalidate_track_cycle_state(&mut self) {
3410        self.track_process_epoch = self.track_process_epoch.saturating_add(1);
3411        self.track_processing_started_at.clear();
3412        let state = self.state.lock();
3413        for track in state.tracks.values() {
3414            let t = track.lock();
3415            t.audio.finished = false;
3416            t.audio.processing = false;
3417        }
3418    }
3419
3420    fn force_stalled_track_completions(&mut self) {
3421        let now = Instant::now();
3422        let state = self.state.lock();
3423        for (track_name, track) in state.tracks.iter() {
3424            let started = self.track_processing_started_at.get(track_name).copied();
3425            let Some(started) = started else {
3426                continue;
3427            };
3428            if now.duration_since(started) < Self::TRACK_PROCESS_TIMEOUT {
3429                continue;
3430            }
3431            let t = track.lock();
3432            if t.audio.finished || !t.audio.processing {
3433                self.track_processing_started_at.remove(track_name);
3434                continue;
3435            }
3436            for out in &t.audio.outs {
3437                let out_buf = out.buffer.lock();
3438                out_buf.fill(0.0);
3439                *out.finished.lock() = true;
3440            }
3441            t.audio.processing = false;
3442            t.audio.finished = true;
3443            self.track_processing_started_at.remove(track_name);
3444            tracing::warn!(
3445                "Track '{}' exceeded process timeout ({} ms); forcing silent completion for cycle",
3446                track_name,
3447                Self::TRACK_PROCESS_TIMEOUT.as_millis()
3448            );
3449        }
3450    }
3451
3452    fn should_publish_hw_out_meters(&mut self) -> bool {
3453        let now = Instant::now();
3454        match self.last_hw_out_meter_publish {
3455            Some(last) if now.duration_since(last) < Self::METER_PUBLISH_INTERVAL => false,
3456            _ => {
3457                self.last_hw_out_meter_publish = Some(now);
3458                true
3459            }
3460        }
3461    }
3462
3463    fn should_publish_track_meters(&mut self) -> bool {
3464        let now = Instant::now();
3465        match self.last_track_meter_publish {
3466            Some(last) if now.duration_since(last) < Self::METER_PUBLISH_INTERVAL => false,
3467            _ => {
3468                self.last_track_meter_publish = Some(now);
3469                true
3470            }
3471        }
3472    }
3473
3474    fn should_publish_hw_out_linear(&mut self, peaks_linear: &[f32]) -> bool {
3475        #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
3476        {
3477            self.hw_out_meter_publish_phase = !self.hw_out_meter_publish_phase;
3478            if !self.hw_out_meter_publish_phase {
3479                return false;
3480            }
3481            let changed = if self.last_hw_out_meter_linear.len() != peaks_linear.len() {
3482                true
3483            } else {
3484                self.last_hw_out_meter_linear
3485                    .iter()
3486                    .zip(peaks_linear.iter())
3487                    .any(|(prev, next)| (prev - next).abs() >= Self::HW_OUT_METER_LINEAR_EPSILON)
3488            };
3489            if !changed {
3490                return false;
3491            }
3492            self.last_hw_out_meter_linear.clear();
3493            self.last_hw_out_meter_linear
3494                .extend_from_slice(peaks_linear);
3495            true
3496        }
3497        #[cfg(not(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd")))]
3498        {
3499            let _ = peaks_linear;
3500            false
3501        }
3502    }
3503
3504    async fn maybe_notify_hw_out_meter(&mut self, _meter_db: Vec<f32>) {
3505        {}
3506    }
3507
3508    fn collect_changed_track_meters(
3509        &mut self,
3510        _tracks: &[(String, Arc<UnsafeMutex<Box<Track>>>)],
3511    ) -> Vec<(String, Vec<f32>)> {
3512        Vec::new()
3513    }
3514
3515    async fn apply_hw_out_gain_and_meter(&mut self) {
3516        let gain = if self.hw_out_muted {
3517            0.0
3518        } else {
3519            10.0_f32.powf(self.hw_out_level_db / 20.0)
3520        };
3521        let should_notify_interval = self.should_publish_hw_out_meters();
3522        if let Some(oss) = self.hw_driver.clone() {
3523            let hw = oss.lock();
3524            hw.set_output_gain_balance(gain, self.hw_out_balance);
3525            if !should_notify_interval {
3526                return;
3527            }
3528        } else {
3529            #[cfg(unix)]
3530            {
3531                if let Some(jack) = self.jack_runtime.clone() {
3532                    jack.lock().set_output_gain_linear(gain);
3533                    jack.lock().set_output_balance(self.hw_out_balance);
3534                    if !should_notify_interval {
3535                        return;
3536                    }
3537                } else {
3538                    return;
3539                }
3540            }
3541            #[cfg(not(unix))]
3542            {
3543                return;
3544            }
3545        }
3546        let peaks_linear = if let Some(oss) = self.hw_driver.clone() {
3547            oss.lock().output_meter_linear(gain, self.hw_out_balance)
3548        } else {
3549            #[cfg(unix)]
3550            {
3551                if let Some(jack) = self.jack_runtime.clone() {
3552                    let outs = jack.lock().audio_outs();
3553                    let out_count = outs.len();
3554                    let b = if out_count == 2 {
3555                        self.hw_out_balance.clamp(-1.0, 1.0)
3556                    } else {
3557                        0.0
3558                    };
3559                    let mut meters_linear = Vec::with_capacity(out_count);
3560                    for (channel_idx, channel) in outs.iter().enumerate() {
3561                        let balance_gain = if out_count == 2 {
3562                            if channel_idx == 0 {
3563                                (1.0 - b).clamp(0.0, 1.0)
3564                            } else {
3565                                (1.0 + b).clamp(0.0, 1.0)
3566                            }
3567                        } else {
3568                            1.0
3569                        };
3570                        let buf = channel.buffer.lock();
3571                        let peak = crate::simd::peak_abs(buf) * gain * balance_gain;
3572                        meters_linear.push(peak);
3573                    }
3574                    meters_linear
3575                } else {
3576                    return;
3577                }
3578            }
3579            #[cfg(not(unix))]
3580            {
3581                return;
3582            }
3583        };
3584        if self.hw_out_peak_hold_linear.len() != peaks_linear.len() {
3585            self.hw_out_peak_hold_linear.resize(peaks_linear.len(), 0.0);
3586        }
3587        let mut held_peaks = Vec::with_capacity(peaks_linear.len());
3588        for (idx, peak_now) in peaks_linear.iter().copied().enumerate() {
3589            let held = self.hw_out_peak_hold_linear[idx] * 0.92;
3590            let next = peak_now.max(held);
3591            self.hw_out_peak_hold_linear[idx] = next;
3592            held_peaks.push(next);
3593        }
3594        let should_notify =
3595            should_notify_interval && self.should_publish_hw_out_linear(&held_peaks);
3596        let meter_db: Vec<f32> = held_peaks
3597            .into_iter()
3598            .map(Self::meter_linear_to_db)
3599            .collect();
3600        self.latest_hw_out_meter_db = Arc::new(meter_db.clone());
3601        if should_notify {
3602            self.maybe_notify_hw_out_meter(meter_db).await;
3603        }
3604    }
3605
3606    fn preload_track_clips_spawn(&self) {
3607        let tracks: Vec<_> = self.state.lock().tracks.values().cloned().collect();
3608        for track in tracks {
3609            tokio::task::spawn_blocking(move || {
3610                track.lock().preload_clips();
3611            });
3612        }
3613    }
3614
3615    async fn preload_track_clips(&self) {
3616        let tracks: Vec<_> = self.state.lock().tracks.values().cloned().collect();
3617        if tracks.is_empty() {
3618            return;
3619        }
3620        let mut handles = Vec::with_capacity(tracks.len());
3621        for track in tracks {
3622            handles.push(tokio::task::spawn_blocking(move || {
3623                track.lock().preload_clips();
3624            }));
3625        }
3626        for handle in handles {
3627            if let Err(e) = handle.await {
3628                tracing::warn!("Clip preload task panicked: {e}");
3629            }
3630        }
3631    }
3632
3633    async fn send_tracks(&mut self) -> bool {
3634        if !self.playing {
3635            return false;
3636        }
3637        self.refresh_realtime_infection();
3638        let mut cycle_underflows = 0usize;
3639        {
3640            let state = self.state.lock();
3641            for track in state.tracks.values() {
3642                cycle_underflows =
3643                    cycle_underflows.saturating_add(track.lock().take_hybrid_underflow_delta());
3644            }
3645        }
3646        if cycle_underflows > 0 {
3647            self.refill_budget_per_pass = (self.refill_budget_per_pass + 1).min(8);
3648        } else {
3649            self.refill_budget_per_pass = self.refill_budget_per_pass.saturating_sub(1).max(1);
3650        }
3651        self.force_stalled_track_completions();
3652        let mut finished = true;
3653        let mut dispatched = 0;
3654        let mut refill_dispatched = 0usize;
3655        let mut realtime_fallback_dispatched = 0usize;
3656        loop {
3657            let next_track = {
3658                let state = self.state.lock();
3659                let mut next_realtime = None;
3660                let mut next_playback = None;
3661                for track in state.tracks.values() {
3662                    let t = track.lock();
3663                    if t.audio.finished {
3664                        continue;
3665                    }
3666                    let needs_refill_event = t.hybrid_needs_refill();
3667                    if !t.is_realtime_domain()
3668                        && !needs_refill_event
3669                        && t.try_consume_hybrid_playback_cycle()
3670                    {
3671                        continue;
3672                    }
3673                    finished = false;
3674                    if t.audio.processing || !t.audio.ready() {
3675                        continue;
3676                    }
3677                    if t.is_realtime_domain() {
3678                        if next_realtime.is_none() {
3679                            next_realtime = Some(track.clone());
3680                        }
3681                    } else if next_playback.is_none() {
3682                        next_playback = Some(track.clone());
3683                    }
3684                }
3685                if next_realtime.is_none()
3686                    && next_playback.is_some()
3687                    && refill_dispatched >= self.refill_budget_per_pass
3688                {
3689                    self.refill_budget_throttle_count =
3690                        self.refill_budget_throttle_count.saturating_add(1);
3691                }
3692                next_realtime.or(next_playback)
3693            };
3694
3695            let Some(track) = next_track else {
3696                if dispatched > 0 {
3697                    tracing::info!("send_tracks dispatched {} tracks", dispatched);
3698                }
3699                return finished;
3700            };
3701            let worker_class = {
3702                let t = track.lock();
3703                if t.is_realtime_domain() {
3704                    WorkerClass::Realtime
3705                } else {
3706                    WorkerClass::Refill
3707                }
3708            };
3709            let worker_index = if let Some(index) = self.take_ready_worker_index(worker_class) {
3710                Some(index)
3711            } else if matches!(worker_class, WorkerClass::Realtime)
3712                && self.realtime_fallback_enabled
3713                && realtime_fallback_dispatched < self.realtime_fallback_budget_per_pass
3714            {
3715                self.take_ready_worker_index(WorkerClass::Refill)
3716            } else {
3717                None
3718            };
3719            let Some(worker_index) = worker_index else {
3720                self.force_stalled_track_completions();
3721                if dispatched > 0 {
3722                    tracing::info!(
3723                        "send_tracks dispatched {} tracks (no more workers)",
3724                        dispatched
3725                    );
3726                }
3727                return false;
3728            };
3729
3730            let t = track.lock();
3731            if t.audio.finished || t.audio.processing || !t.audio.ready() {
3732                continue;
3733            }
3734            if matches!(worker_class, WorkerClass::Refill) {
3735                // Consume wakeup only when we are actually dispatching refill work.
3736                let _ = t.hybrid_take_refill_wakeup();
3737            }
3738            dispatched += 1;
3739            if matches!(worker_class, WorkerClass::Refill) {
3740                refill_dispatched = refill_dispatched.saturating_add(1);
3741            } else if !matches!(
3742                self.worker_classes
3743                    .get(worker_index)
3744                    .copied()
3745                    .unwrap_or(WorkerClass::Realtime),
3746                WorkerClass::Realtime
3747            ) {
3748                realtime_fallback_dispatched = realtime_fallback_dispatched.saturating_add(1);
3749                self.realtime_fallback_dispatch_count =
3750                    self.realtime_fallback_dispatch_count.saturating_add(1);
3751            }
3752            t.set_transport_sample(self.transport_sample);
3753            t.set_loop_config(self.loop_enabled, self.loop_range_samples);
3754            t.set_transport_timing(self.tempo_bpm, self.tsig_num, self.tsig_denom);
3755            let low_watermark = if self.hybrid_low_watermark_frames > 0 {
3756                self.hybrid_low_watermark_frames
3757            } else {
3758                self.current_cycle_samples().saturating_mul(4).max(1)
3759            };
3760            let realtime_frames = if self.hybrid_realtime_frames > 0 {
3761                self.hybrid_realtime_frames
3762            } else {
3763                self.current_cycle_samples().max(1)
3764            };
3765            let playback_frames = if self.hybrid_playback_frames > 0 {
3766                self.hybrid_playback_frames
3767            } else {
3768                self.current_cycle_samples().max(1)
3769            };
3770            t.configure_hybrid_timing(realtime_frames, low_watermark, playback_frames);
3771            t.process_epoch = self.track_process_epoch;
3772
3773            t.set_clip_playback_enabled(self.clip_playback_enabled && self.playing);
3774
3775            t.set_record_tap_enabled(self.playing && self.record_enabled);
3776            t.audio.processing = true;
3777            self.track_processing_started_at
3778                .insert(t.name.clone(), Instant::now());
3779            let worker = &self.workers[worker_index];
3780            if let Err(e) = worker.tx.send(Message::ProcessTrack(track.clone())).await {
3781                t.audio.processing = false;
3782                self.track_processing_started_at.remove(&t.name);
3783                self.notify_clients(Err(format!("Failed to send track to worker: {}", e)))
3784                    .await;
3785            }
3786        }
3787    }
3788
3789    async fn on_all_tracks_finished(&mut self) {
3790        if self.transport_restart_pending {
3791            let state = self.state.lock();
3792            for track in state.tracks.values() {
3793                track.lock().take_hw_midi_out_events();
3794            }
3795        } else if self.hw_worker.is_some() {
3796            self.active_hw_notes_cycle_start = self.active_hw_notes_by_track.clone();
3797            let mut out_events = self.collect_hw_midi_output_events_by_device();
3798            if self.loop_enabled
3799                && let Some((_, loop_end)) = self.loop_range_samples
3800            {
3801                let cycle_end = self
3802                    .transport_sample
3803                    .saturating_add(self.current_cycle_samples());
3804                if self.transport_sample < loop_end && cycle_end > loop_end {
3805                    let wrap_frame = loop_end
3806                        .saturating_sub(self.transport_sample)
3807                        .min(self.current_cycle_samples())
3808                        as u32;
3809                    out_events.extend(self.note_off_events_for_active_snapshot(
3810                        &self.active_hw_notes_cycle_start,
3811                        wrap_frame,
3812                    ));
3813                    out_events.sort_by(|a, b| {
3814                        a.event
3815                            .frame
3816                            .cmp(&b.event.frame)
3817                            .then_with(|| a.device.cmp(&b.device))
3818                    });
3819                }
3820            }
3821            self.pending_hw_midi_out_events_by_device.extend(out_events);
3822        } else {
3823            self.pending_hw_midi_out_events = self.collect_hw_midi_output_events();
3824        }
3825        self.request_hw_cycle().await;
3826    }
3827
3828    fn take_ready_worker_index(&mut self, class: WorkerClass) -> Option<usize> {
3829        let queue = match class {
3830            WorkerClass::Realtime => &mut self.ready_realtime_workers,
3831            WorkerClass::Refill => &mut self.ready_refill_workers,
3832        };
3833        while !queue.is_empty() {
3834            let worker_index = queue.remove(0);
3835            if worker_index < self.workers.len() {
3836                return Some(worker_index);
3837            }
3838        }
3839        None
3840    }
3841
3842    fn push_ready_worker(&mut self, worker_index: usize) {
3843        match self
3844            .worker_classes
3845            .get(worker_index)
3846            .copied()
3847            .unwrap_or(WorkerClass::Refill)
3848        {
3849            WorkerClass::Realtime => self.ready_realtime_workers.push(worker_index),
3850            WorkerClass::Refill => self.ready_refill_workers.push(worker_index),
3851        }
3852    }
3853
3854    async fn publish_track_meters(&mut self) {
3855        if !self.should_publish_track_meters() {
3856            return;
3857        }
3858        let tracks: Vec<(String, Arc<UnsafeMutex<Box<Track>>>)> = self
3859            .state
3860            .lock()
3861            .tracks
3862            .iter()
3863            .map(|(name, track)| (name.clone(), track.clone()))
3864            .collect();
3865        let mut snapshot = Vec::with_capacity(tracks.len());
3866        for (name, track) in &tracks {
3867            let linear = self
3868                .track_meter_linear_by_track
3869                .get(name)
3870                .cloned()
3871                .unwrap_or_else(|| track.lock().output_meter_linear());
3872            let output_db = linear
3873                .iter()
3874                .copied()
3875                .map(Self::meter_linear_to_db)
3876                .collect::<Vec<_>>();
3877            snapshot.push((name.clone(), output_db));
3878        }
3879        self.latest_track_meter_snapshot = Arc::new(snapshot);
3880        let meters = self.collect_changed_track_meters(&tracks);
3881        for (track_name, output_db) in meters {
3882            self.notify_clients(Ok(Action::TrackMeters {
3883                track_name,
3884                output_db,
3885            }))
3886            .await;
3887        }
3888    }
3889
3890    pub fn check_if_leads_to_kind(
3891        &self,
3892        kind: Kind,
3893        current_track_name: &str,
3894        target_track_name: &str,
3895    ) -> bool {
3896        routing::would_create_cycle(
3897            &target_track_name.to_string(),
3898            &current_track_name.to_string(),
3899            |track_name| self.connected_neighbors(kind, track_name),
3900        )
3901    }
3902
3903    fn connected_neighbors(&self, kind: Kind, current_track_name: &str) -> Vec<String> {
3904        let state = self.state.lock();
3905        let mut found_neighbors = Vec::new();
3906
3907        if let Some(current_track_handle) = state.tracks.get(current_track_name) {
3908            let current_track = current_track_handle.lock();
3909
3910            match kind {
3911                Kind::Audio => {
3912                    for out_port in &current_track.audio.outs {
3913                        let conns = out_port.connections.lock();
3914                        for conn in conns.iter() {
3915                            for (name, next_track_handle) in &state.tracks {
3916                                let next_track = next_track_handle.lock();
3917                                let is_connected =
3918                                    next_track.audio.ins.iter().any(|ins_port| {
3919                                        Arc::ptr_eq(&ins_port.buffer, &conn.buffer)
3920                                    });
3921
3922                                if is_connected {
3923                                    found_neighbors.push(name.clone());
3924                                }
3925                            }
3926                        }
3927                    }
3928                }
3929                Kind::MIDI => {
3930                    for out_port in &current_track.midi.outs {
3931                        let conns = out_port.lock().connections.clone();
3932                        for conn in conns.iter() {
3933                            for (name, next_track_handle) in &state.tracks {
3934                                let next_track = next_track_handle.lock();
3935                                let is_connected = next_track
3936                                    .midi
3937                                    .ins
3938                                    .iter()
3939                                    .any(|ins_port| Arc::ptr_eq(ins_port, conn));
3940
3941                                if is_connected {
3942                                    found_neighbors.push(name.clone());
3943                                }
3944                            }
3945                        }
3946                    }
3947                }
3948            }
3949        }
3950        found_neighbors
3951    }
3952
3953    async fn handle_request(&mut self, a: Action) {
3954        match a {
3955            Action::Undo => {
3956                let actions = match self.history.undo() {
3957                    Some(actions) => actions,
3958                    None => {
3959                        self.notify_clients(Ok(Action::Undo)).await;
3960                        self.notify_clients(Ok(Action::HistoryState {
3961                            dirty: self.history.is_dirty(),
3962                        }))
3963                        .await;
3964                        return;
3965                    }
3966                };
3967
3968                let was_suspended = self.history_suspended;
3969                self.history_suspended = true;
3970                for action in actions {
3971                    self.handle_request_inner(action, false).await;
3972                }
3973                self.history_suspended = was_suspended;
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            }
3980            Action::Redo => {
3981                let actions = match self.history.redo() {
3982                    Some(actions) => actions,
3983                    None => {
3984                        self.notify_clients(Ok(Action::Redo)).await;
3985                        self.notify_clients(Ok(Action::HistoryState {
3986                            dirty: self.history.is_dirty(),
3987                        }))
3988                        .await;
3989                        return;
3990                    }
3991                };
3992
3993                let was_suspended = self.history_suspended;
3994                self.history_suspended = true;
3995                for action in actions {
3996                    self.handle_request_inner(action, false).await;
3997                }
3998                self.history_suspended = was_suspended;
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            }
4005            Action::ApplyGroupedActions(actions) => {
4006                self.handle_request_inner(Action::BeginHistoryGroup, true)
4007                    .await;
4008                for action in actions {
4009                    self.handle_request_inner(action, true).await;
4010                }
4011                self.handle_request_inner(Action::EndHistoryGroup, true)
4012                    .await;
4013            }
4014            other => {
4015                self.handle_request_inner(other, true).await;
4016            }
4017        }
4018    }
4019
4020    async fn handle_request_inner(&mut self, action_to_process: Action, record_history: bool) {
4021        let a = action_to_process.clone();
4022        let suppress_timing_history = self.playing
4023            && matches!(
4024                &action_to_process,
4025                Action::SetTempo(_) | Action::SetTimeSignature { .. }
4026            );
4027        let mut extra_inverse_actions: Vec<Action> = Vec::new();
4028        if record_history
4029            && !self.history_suspended
4030            && let Action::RemoveTrack(ref track_name) = action_to_process
4031        {
4032            for route in self
4033                .midi_hw_in_routes
4034                .iter()
4035                .filter(|route| &route.to_track == track_name)
4036            {
4037                extra_inverse_actions.push(Action::Connect {
4038                    from_track: format!("midi:hw:in:{}", route.device),
4039                    from_port: 0,
4040                    to_track: route.to_track.clone(),
4041                    to_port: route.to_port,
4042                    kind: Kind::MIDI,
4043                });
4044            }
4045            for route in self
4046                .midi_hw_out_routes
4047                .iter()
4048                .filter(|route| &route.from_track == track_name)
4049            {
4050                extra_inverse_actions.push(Action::Connect {
4051                    from_track: route.from_track.clone(),
4052                    from_port: route.from_port,
4053                    to_track: format!("midi:hw:out:{}", route.device),
4054                    to_port: 0,
4055                    kind: Kind::MIDI,
4056                });
4057            }
4058        }
4059        if record_history
4060            && !self.history_suspended
4061            && matches!(action_to_process, Action::ClearAllMidiLearnBindings)
4062        {
4063            if let Some(binding) = self.global_midi_learn_play_pause.clone() {
4064                extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
4065                    target: crate::message::GlobalMidiLearnTarget::PlayPause,
4066                    binding: Some(binding),
4067                });
4068            }
4069            if let Some(binding) = self.global_midi_learn_stop.clone() {
4070                extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
4071                    target: crate::message::GlobalMidiLearnTarget::Stop,
4072                    binding: Some(binding),
4073                });
4074            }
4075            if let Some(binding) = self.global_midi_learn_record_toggle.clone() {
4076                extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
4077                    target: crate::message::GlobalMidiLearnTarget::RecordToggle,
4078                    binding: Some(binding),
4079                });
4080            }
4081        }
4082        let mut inverse_actions = if record_history
4083            && !suppress_timing_history
4084            && should_record(&action_to_process)
4085            && !self.history_suspended
4086        {
4087            let state = self.state.lock();
4088            create_inverse_actions(&action_to_process, state).map(|mut actions| {
4089                actions.extend(extra_inverse_actions);
4090                actions
4091            })
4092        } else {
4093            None
4094        };
4095        if record_history && !suppress_timing_history && !self.history_suspended {
4096            match &action_to_process {
4097                Action::SetTempo(_) => {
4098                    inverse_actions = Some(vec![Action::SetTempo(self.tempo_bpm)]);
4099                }
4100                Action::SetLoopEnabled(_) => {
4101                    inverse_actions = Some(vec![Action::SetLoopEnabled(self.loop_enabled)]);
4102                }
4103                Action::SetLoopRange(_) => {
4104                    inverse_actions = Some(vec![
4105                        Action::SetLoopRange(self.loop_range_samples),
4106                        Action::SetLoopEnabled(self.loop_enabled),
4107                    ]);
4108                }
4109                Action::SetPunchEnabled(_) => {
4110                    inverse_actions = Some(vec![Action::SetPunchEnabled(self.punch_enabled)]);
4111                }
4112                Action::SetPunchRange(_) => {
4113                    inverse_actions = Some(vec![
4114                        Action::SetPunchRange(self.punch_range_samples),
4115                        Action::SetPunchEnabled(self.punch_enabled),
4116                    ]);
4117                }
4118                Action::SetMetronomeEnabled(_) => {
4119                    inverse_actions =
4120                        Some(vec![Action::SetMetronomeEnabled(self.metronome_enabled)]);
4121                }
4122                Action::SetTimeSignature { .. } => {
4123                    inverse_actions = Some(vec![Action::SetTimeSignature {
4124                        numerator: self.tsig_num,
4125                        denominator: self.tsig_denom,
4126                    }]);
4127                }
4128                Action::SetClipPlaybackEnabled(_) => {
4129                    inverse_actions = Some(vec![Action::SetClipPlaybackEnabled(
4130                        self.clip_playback_enabled,
4131                    )]);
4132                }
4133                Action::SetRecordEnabled(_) => {
4134                    inverse_actions = Some(vec![Action::SetRecordEnabled(self.record_enabled)]);
4135                }
4136                Action::SetGlobalMidiLearnBinding { target, .. } => {
4137                    let binding = match target {
4138                        crate::message::GlobalMidiLearnTarget::PlayPause => {
4139                            self.global_midi_learn_play_pause.clone()
4140                        }
4141                        crate::message::GlobalMidiLearnTarget::Stop => {
4142                            self.global_midi_learn_stop.clone()
4143                        }
4144                        crate::message::GlobalMidiLearnTarget::RecordToggle => {
4145                            self.global_midi_learn_record_toggle.clone()
4146                        }
4147                    };
4148                    inverse_actions = Some(vec![Action::SetGlobalMidiLearnBinding {
4149                        target: *target,
4150                        binding,
4151                    }]);
4152                }
4153                _ => {}
4154            }
4155        }
4156
4157        match action_to_process {
4158            Action::Play => {
4159                tracing::info!(
4160                    "Action::Play pressed, transport_sample={}",
4161                    self.transport_sample
4162                );
4163                self.playing = true;
4164                self.transport_restart_pending = true;
4165                self.invalidate_track_cycle_state();
4166                if let Some(driver) = self.hw_driver.as_mut() {
4167                    driver.lock().set_playing(true);
4168                }
4169                #[cfg(unix)]
4170                if let Some(jack) = &self.jack_runtime
4171                    && let Err(e) = jack.lock().transport_start()
4172                {
4173                    self.notify_clients(Err(e)).await;
4174                }
4175                self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
4176                    .await;
4177                self.preload_track_clips().await;
4178                let send_result = self.send_tracks().await;
4179                tracing::info!("send_tracks after Play returned finished={}", send_result);
4180                if !self.awaiting_hwfinished
4181                    && !self.handling_hwfinished
4182                    && send_result
4183                    && self.hw_worker.is_some()
4184                {
4185                    self.transport_restart_pending = false;
4186                    self.request_hw_cycle().await;
4187                }
4188            }
4189            Action::Pause => {
4190                self.clip_playback_enabled = false;
4191                for track in self.state.lock().tracks.values() {
4192                    track.lock().set_clip_playback_enabled(false);
4193                }
4194                if !self.playing {
4195                    self.playing = true;
4196                    self.transport_restart_pending = true;
4197                    self.invalidate_track_cycle_state();
4198                    if let Some(driver) = self.hw_driver.as_mut() {
4199                        driver.lock().set_playing(true);
4200                    }
4201                    #[cfg(unix)]
4202                    if let Some(jack) = &self.jack_runtime
4203                        && let Err(e) = jack.lock().transport_start()
4204                    {
4205                        self.notify_clients(Err(e)).await;
4206                    }
4207                    self.preload_track_clips().await;
4208                    if !self.awaiting_hwfinished
4209                        && !self.handling_hwfinished
4210                        && self.send_tracks().await
4211                        && self.hw_worker.is_some()
4212                    {
4213                        self.transport_restart_pending = false;
4214                        self.request_hw_cycle().await;
4215                    }
4216                }
4217                self.notify_clients(Ok(Action::Pause)).await;
4218                self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
4219                    .await;
4220            }
4221            Action::Stop => {
4222                self.playing = false;
4223                self.transport_panic_flush_pending = false;
4224                self.transport_restart_pending = false;
4225                self.invalidate_track_cycle_state();
4226                if let Some(driver) = self.hw_driver.as_mut() {
4227                    driver.lock().set_playing(false);
4228                }
4229                #[cfg(unix)]
4230                if let Some(jack) = &self.jack_runtime
4231                    && let Err(e) = jack.lock().transport_stop()
4232                {
4233                    self.notify_clients(Err(e)).await;
4234                }
4235                let panic_events = self.note_off_events_for_all_active_tracks();
4236                if let Some(worker) = &self.hw_worker {
4237                    if !panic_events.is_empty()
4238                        && let Err(e) = worker.tx.send(Message::HWMidiOutEvents(panic_events)).await
4239                    {
4240                        error!("Error sending stop MIDI panic events {e}");
4241                    }
4242                } else {
4243                    self.pending_hw_midi_out_events_by_device
4244                        .extend(panic_events);
4245                }
4246                self.flush_recordings().await;
4247                self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
4248                    .await;
4249            }
4250            Action::JumpToEnd => {
4251                self.transport_sample = self.normalize_transport_sample(self.session_end_sample());
4252                self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
4253                    .await;
4254            }
4255            Action::Panic => {
4256                let panic_events = self.panic_events_for_all_hw_midi_outputs();
4257                if let Some(worker) = &self.hw_worker {
4258                    if !panic_events.is_empty() {
4259                        if let Err(e) = worker.tx.send(Message::ClearHWMidiOutEvents).await {
4260                            error!("Error clearing HW MIDI queue for panic {e}");
4261                        }
4262                        self.midi_hub
4263                            .lock()
4264                            .write_events_blocking(&panic_events, Duration::from_millis(250));
4265                    }
4266                } else if !panic_events.is_empty() {
4267                    self.pending_hw_midi_out_events_by_device
4268                        .extend(panic_events);
4269                }
4270            }
4271            Action::SetClipPlaybackEnabled(enabled) => {
4272                self.clip_playback_enabled = enabled;
4273                for track in self.state.lock().tracks.values() {
4274                    track.lock().set_clip_playback_enabled(enabled);
4275                }
4276            }
4277            Action::TransportPosition(sample) => {
4278                self.transport_sample = self.normalize_transport_sample(sample);
4279                #[cfg(unix)]
4280                if let Some(jack) = &self.jack_runtime
4281                    && let Err(e) = jack.lock().transport_locate(self.transport_sample)
4282                {
4283                    self.notify_clients(Err(e)).await;
4284                }
4285                if self.playing {
4286                    self.transport_restart_pending = true;
4287                    self.invalidate_track_cycle_state();
4288                    self.transport_panic_flush_pending = self.hw_worker.is_some();
4289                    self.clear_hw_midi_output_state(true).await;
4290                    if !self.awaiting_hwfinished && !self.handling_hwfinished {
4291                        if self.hw_worker.is_some() {
4292                            self.request_hw_cycle().await;
4293                        } else if self.send_tracks().await {
4294                            self.transport_restart_pending = false;
4295                            self.request_hw_cycle().await;
4296                        }
4297                    }
4298                }
4299            }
4300            Action::SetLoopEnabled(enabled) => {
4301                self.loop_enabled = enabled && self.loop_range_samples.is_some();
4302            }
4303            Action::SetLoopRange(range) => {
4304                self.loop_range_samples = range.and_then(|(start, end)| {
4305                    if end > start {
4306                        Some((start, end))
4307                    } else {
4308                        None
4309                    }
4310                });
4311                self.loop_enabled = self.loop_range_samples.is_some();
4312                if self.loop_enabled
4313                    && let Some((loop_start, loop_end)) = self.loop_range_samples
4314                    && self.transport_sample >= loop_end
4315                {
4316                    self.transport_sample = loop_start;
4317                    self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
4318                        .await;
4319                }
4320            }
4321            Action::SetPunchEnabled(enabled) => {
4322                self.punch_enabled = enabled && self.punch_range_samples.is_some();
4323            }
4324            Action::SetPunchRange(range) => {
4325                self.punch_range_samples = range.and_then(|(start, end)| {
4326                    if end > start {
4327                        Some((start, end))
4328                    } else {
4329                        None
4330                    }
4331                });
4332                self.punch_enabled = self.punch_range_samples.is_some();
4333            }
4334            Action::SetMetronomeEnabled(enabled) => {
4335                self.metronome_enabled = enabled;
4336                if enabled {
4337                    self.ensure_metronome_track().await;
4338                }
4339                if let Some(track) = self.state.lock().tracks.get(Self::METRONOME_TRACK).cloned() {
4340                    track.lock().set_metronome_enabled(enabled);
4341                }
4342            }
4343            Action::SetTempo(bpm) => {
4344                self.tempo_bpm = bpm.max(1.0);
4345            }
4346            Action::SetTimeSignature {
4347                numerator,
4348                denominator,
4349            } => {
4350                self.tsig_num = numerator.max(1);
4351                self.tsig_denom = denominator.max(1);
4352            }
4353            Action::SetOscEnabled(enabled) => {
4354                if let Err(err) = self.set_osc_enabled_with(enabled, OscServer::start) {
4355                    self.notify_clients(Err(err)).await;
4356                }
4357            }
4358            Action::SetRecordEnabled(enabled) => {
4359                self.record_enabled = enabled;
4360                if !enabled {
4361                    if self.awaiting_hwfinished {
4362                        self.append_recorded_cycle();
4363                    }
4364                    self.flush_recordings().await;
4365                } else if self.session_dir.is_none() {
4366                    self.notify_clients(Err(
4367                        "Recording enabled but session path is not set".to_string()
4368                    ))
4369                    .await;
4370                }
4371            }
4372            Action::BeginHistoryGroup if self.history_group.is_none() => {
4373                self.history_group = Some(UndoEntry {
4374                    forward_actions: vec![],
4375                    inverse_actions: vec![],
4376                });
4377            }
4378            Action::EndHistoryGroup => {
4379                if let Some(mut group) = self.history_group.take()
4380                    && !group.forward_actions.is_empty()
4381                    && !group.inverse_actions.is_empty()
4382                {
4383                    let mut add_tracks = Vec::new();
4384                    let mut connections = Vec::new();
4385                    let mut rest = Vec::new();
4386                    for action in group.inverse_actions {
4387                        if matches!(action, Action::AddTrack { .. }) {
4388                            add_tracks.push(action);
4389                        } else if matches!(action, Action::Connect { .. }) {
4390                            connections.push(action);
4391                        } else {
4392                            rest.push(action);
4393                        }
4394                    }
4395                    group.inverse_actions = add_tracks;
4396                    group.inverse_actions.extend(rest);
4397                    group.inverse_actions.extend(connections);
4398                    self.history.record(group);
4399                }
4400            }
4401            Action::SetSessionPath(ref path) => {
4402                self.session_dir = Some(Path::new(path).to_path_buf());
4403                self.ensure_session_subdirs();
4404                #[cfg(all(unix, not(target_os = "macos")))]
4405                let _lv2_dir = self.session_plugins_dir();
4406                for track in self.state.lock().tracks.values() {
4407                    track.lock().set_session_base_dir(self.session_dir.clone());
4408                }
4409            }
4410            Action::MarkHistorySavePoint => {
4411                self.history.mark_save_point();
4412                self.notify_clients(Ok(Action::HistoryState {
4413                    dirty: self.history.is_dirty(),
4414                }))
4415                .await;
4416            }
4417            Action::ClearHistory => {
4418                self.history.clear();
4419                self.history.mark_save_point();
4420            }
4421            Action::BeginSessionRestore => {
4422                self.history_suspended = true;
4423                self.history.clear();
4424            }
4425            Action::EndSessionRestore => {
4426                self.history.clear();
4427                self.history_suspended = false;
4428                self.preload_track_clips_spawn();
4429            }
4430            Action::Quit => {
4431                self.flush_recordings().await;
4432                self.ready_realtime_workers.clear();
4433                self.ready_refill_workers.clear();
4434                while !self.workers.is_empty() {
4435                    let worker = self.workers.remove(0);
4436                    worker
4437                        .tx
4438                        .send(Message::Request(a.clone()))
4439                        .await
4440                        .expect("Failed sending quit message to worker");
4441                    worker
4442                        .handle
4443                        .await
4444                        .expect("Failed waiting for worker to quit");
4445                }
4446
4447                if let Some(worker) = self.hw_worker.take() {
4448                    let mut panic_events = self.note_off_events_for_all_active_tracks();
4449                    panic_events.extend(self.panic_events_for_all_hw_midi_outputs());
4450                    if !panic_events.is_empty() {
4451                        if let Err(e) = worker.tx.send(Message::ClearHWMidiOutEvents).await {
4452                            error!("Error clearing HW MIDI queue during quit {e}");
4453                        }
4454                        self.midi_hub
4455                            .lock()
4456                            .write_events_blocking(&panic_events, Duration::from_millis(250));
4457                    }
4458                    worker
4459                        .tx
4460                        .send(Message::Request(a.clone()))
4461                        .await
4462                        .expect("Failed sending quit message to HW worker");
4463                    worker
4464                        .handle
4465                        .await
4466                        .expect("Failed waiting for HW worker to quit");
4467                }
4468                #[cfg(unix)]
4469                {
4470                    self.jack_runtime = None;
4471                }
4472            }
4473            Action::AddTrack {
4474                ref name,
4475                audio_ins,
4476                midi_ins,
4477                audio_outs,
4478                midi_outs,
4479            } => {
4480                let tracks = &mut self.state.lock().tracks;
4481                if tracks.contains_key(name) {
4482                    self.notify_clients(Err(format!("Track {} already exists", name)))
4483                        .await;
4484                    return;
4485                }
4486                let maybe_hw = if let Some(oss) = &self.hw_driver {
4487                    let hw = oss.lock();
4488                    Some((hw.cycle_samples(), hw.sample_rate() as f64))
4489                } else {
4490                    #[cfg(unix)]
4491                    if let Some(jack) = &self.jack_runtime {
4492                        let j = jack.lock();
4493                        Some((j.buffer_size, j.sample_rate as f64))
4494                    } else {
4495                        None
4496                    }
4497                    #[cfg(not(unix))]
4498                    None
4499                };
4500
4501                if let Some((chsamples, sample_rate)) = maybe_hw {
4502                    tracks.insert(
4503                        name.clone(),
4504                        Arc::new(UnsafeMutex::new(Box::new(Track::new(
4505                            name.clone(),
4506                            audio_ins,
4507                            audio_outs,
4508                            midi_ins,
4509                            midi_outs,
4510                            chsamples,
4511                            sample_rate,
4512                        )))),
4513                    );
4514                    if let Some(track) = tracks.get(name) {
4515                        track.lock().ensure_default_audio_passthrough();
4516                        track.lock().ensure_default_midi_passthrough();
4517                        track
4518                            .lock()
4519                            .set_clip_playback_enabled(self.clip_playback_enabled);
4520                        track.lock().set_transport_timing(
4521                            self.tempo_bpm,
4522                            self.tsig_num,
4523                            self.tsig_denom,
4524                        );
4525                        track.lock().set_session_base_dir(self.session_dir.clone());
4526                    }
4527                } else {
4528                    self.notify_clients(Err(
4529                        "Engine needs to open audio device before adding audio track".to_string(),
4530                    ))
4531                    .await;
4532                }
4533            }
4534            Action::TrackAddAudioInput(ref name) => {
4535                let track = match self.track_handle_or_err(name) {
4536                    Ok(track) => track,
4537                    Err(e) => {
4538                        self.notify_clients(Err(e)).await;
4539                        return;
4540                    }
4541                };
4542                if let Err(e) = track.lock().add_audio_input() {
4543                    self.notify_clients(Err(e)).await;
4544                    return;
4545                }
4546            }
4547            Action::TrackAddAudioOutput(ref name) => {
4548                let track = match self.track_handle_or_err(name) {
4549                    Ok(track) => track,
4550                    Err(e) => {
4551                        self.notify_clients(Err(e)).await;
4552                        return;
4553                    }
4554                };
4555                if let Err(e) = track.lock().add_audio_output() {
4556                    self.notify_clients(Err(e)).await;
4557                    return;
4558                }
4559            }
4560            Action::TrackRemoveAudioInput(ref name) => {
4561                let track = match self.track_handle_or_err(name) {
4562                    Ok(track) => track,
4563                    Err(e) => {
4564                        self.notify_clients(Err(e)).await;
4565                        return;
4566                    }
4567                };
4568                if let Err(e) = track.lock().remove_audio_input() {
4569                    self.notify_clients(Err(e)).await;
4570                    return;
4571                }
4572            }
4573            Action::TrackRemoveAudioOutput(ref name) => {
4574                let track = match self.track_handle_or_err(name) {
4575                    Ok(track) => track,
4576                    Err(e) => {
4577                        self.notify_clients(Err(e)).await;
4578                        return;
4579                    }
4580                };
4581                let (hw_outputs, track_inputs) = {
4582                    let state = self.state.lock();
4583                    let hw_outputs = self.all_hw_output_audio_ports();
4584                    let track_inputs = state
4585                        .tracks
4586                        .iter()
4587                        .filter(|(track_name, _)| *track_name != name)
4588                        .flat_map(|(_, handle)| handle.lock().audio.ins.clone())
4589                        .collect::<Vec<_>>();
4590                    (hw_outputs, track_inputs)
4591                };
4592                if let Err(e) = track.lock().remove_audio_output(&hw_outputs, &track_inputs) {
4593                    self.notify_clients(Err(e)).await;
4594                    return;
4595                }
4596            }
4597            Action::RenameTrack {
4598                ref old_name,
4599                ref new_name,
4600            } => {
4601                if self.state.lock().tracks.contains_key(new_name) {
4602                    self.notify_clients(Err(format!("Track '{}' already exists", new_name)))
4603                        .await;
4604                    return;
4605                }
4606
4607                let Some(track) = self.state.lock().tracks.remove(old_name) else {
4608                    self.notify_clients(Err(format!("Track '{}' not found", old_name)))
4609                        .await;
4610                    return;
4611                };
4612
4613                track.lock().name = new_name.clone();
4614                self.state.lock().tracks.insert(new_name.clone(), track);
4615                for other in self.state.lock().tracks.values() {
4616                    let other = other.lock();
4617                    if other.vca_master.as_deref() == Some(old_name.as_str()) {
4618                        other.set_vca_master(Some(new_name.clone()));
4619                    }
4620                }
4621
4622                if let Some(recording) = self.audio_recordings.remove(old_name) {
4623                    self.audio_recordings.insert(new_name.clone(), recording);
4624                }
4625                if let Some(recording) = self.midi_recordings.remove(old_name) {
4626                    self.midi_recordings.insert(new_name.clone(), recording);
4627                }
4628
4629                for route in &mut self.midi_hw_in_routes {
4630                    if route.to_track == *old_name {
4631                        route.to_track = new_name.clone();
4632                    }
4633                }
4634                for route in &mut self.midi_hw_out_routes {
4635                    if route.from_track == *old_name {
4636                        route.from_track = new_name.clone();
4637                    }
4638                }
4639                if let Some((armed_track, target, device)) = self.pending_midi_learn.clone()
4640                    && armed_track == *old_name
4641                {
4642                    self.pending_midi_learn = Some((new_name.clone(), target, device));
4643                }
4644
4645                self.notify_clients(Ok(Action::RenameTrack {
4646                    old_name: old_name.clone(),
4647                    new_name: new_name.clone(),
4648                }))
4649                .await;
4650            }
4651            Action::RemoveTrack(ref name) => {
4652                self.state.lock().tracks.remove(name);
4653                self.audio_recordings.remove(name);
4654                self.midi_recordings.remove(name);
4655                self.midi_hw_in_routes.retain(|r| r.to_track != *name);
4656                self.midi_hw_out_routes.retain(|r| r.from_track != *name);
4657                if self
4658                    .pending_midi_learn
4659                    .as_ref()
4660                    .is_some_and(|(track_name, _, _)| track_name == name)
4661                {
4662                    self.pending_midi_learn = None;
4663                }
4664                for track in self.state.lock().tracks.values() {
4665                    let track = track.lock();
4666                    if track.vca_master.as_deref() == Some(name.as_str()) {
4667                        track.set_vca_master(None);
4668                    }
4669                }
4670            }
4671            Action::TrackLevel(ref name, level) => {
4672                if name == "hw:out" {
4673                    self.hw_out_level_db = level;
4674                } else if let Some(track) = self.state.lock().tracks.get(name) {
4675                    let previous = track.lock().level();
4676                    track.lock().set_level(level);
4677                    let delta = level - previous;
4678                    if delta.abs() > f32::EPSILON {
4679                        for follower_name in self.vca_followers(name) {
4680                            if let Some(follower) = self.state.lock().tracks.get(&follower_name) {
4681                                let next = (follower.lock().level() + delta).clamp(-90.0, 20.0);
4682                                follower.lock().set_level(next);
4683                                self.notify_clients(Ok(Action::TrackLevel(
4684                                    follower_name.clone(),
4685                                    next,
4686                                )))
4687                                .await;
4688                            }
4689                        }
4690                    }
4691                }
4692            }
4693            Action::TrackBalance(ref name, balance) => {
4694                if name == "hw:out" {
4695                    self.hw_out_balance = balance.clamp(-1.0, 1.0);
4696                } else if let Some(track) = self.state.lock().tracks.get(name) {
4697                    track.lock().set_balance(balance);
4698                }
4699            }
4700            Action::TrackAutomationLevel(ref name, level) => {
4701                if let Some(track) = self.state.lock().tracks.get(name) {
4702                    let previous = track.lock().level();
4703                    track.lock().set_level(level);
4704                    let delta = level - previous;
4705                    if delta.abs() > f32::EPSILON {
4706                        for follower_name in self.vca_followers(name) {
4707                            if let Some(follower) = self.state.lock().tracks.get(&follower_name) {
4708                                let next = (follower.lock().level() + delta).clamp(-90.0, 20.0);
4709                                follower.lock().set_level(next);
4710                                self.notify_clients(Ok(Action::TrackAutomationLevel(
4711                                    follower_name.clone(),
4712                                    next,
4713                                )))
4714                                .await;
4715                            }
4716                        }
4717                    }
4718                }
4719            }
4720            Action::TrackAutomationBalance(ref name, balance) => {
4721                if let Some(track) = self.state.lock().tracks.get(name) {
4722                    track.lock().set_balance(balance);
4723                }
4724            }
4725            Action::TrackAutomationMute(ref name, muted) => {
4726                if let Some(track) = self.state.lock().tracks.get(name) {
4727                    track.lock().set_muted(muted);
4728                    for follower_name in self.vca_followers(name) {
4729                        if let Some(follower) = self.state.lock().tracks.get(&follower_name) {
4730                            follower.lock().set_muted(muted);
4731                            self.notify_clients(Ok(Action::TrackAutomationMute(
4732                                follower_name.clone(),
4733                                muted,
4734                            )))
4735                            .await;
4736                        }
4737                    }
4738                }
4739            }
4740            Action::RequestMeterSnapshot => {
4741                self.notify_clients(Ok(Action::MeterSnapshot {
4742                    hw_out_db: self.latest_hw_out_meter_db.clone(),
4743                    track_meters: self.latest_track_meter_snapshot.clone(),
4744                }))
4745                .await;
4746                return;
4747            }
4748            Action::TrackMeters { .. } => {}
4749            Action::MeterSnapshot { .. } => {}
4750            Action::TrackToggleArm(ref name) => {
4751                if self.reject_if_track_frozen(name, "arming/disarming").await {
4752                    return;
4753                }
4754                if let Some(track) = self.state.lock().tracks.get(name).cloned() {
4755                    track.lock().arm();
4756                    if !track.lock().armed && self.audio_recordings.contains_key(name) {
4757                        self.flush_track_recording(name).await;
4758                    }
4759                }
4760            }
4761            Action::TrackToggleMute(ref name) => {
4762                if name == "hw:out" {
4763                    self.hw_out_muted = !self.hw_out_muted;
4764                } else if let Some(track) = self.state.lock().tracks.get(name) {
4765                    track.lock().mute();
4766                    let muted = track.lock().muted;
4767                    for follower_name in self.vca_followers(name) {
4768                        if let Some(follower) = self.state.lock().tracks.get(&follower_name)
4769                            && follower.lock().muted != muted
4770                        {
4771                            follower.lock().set_muted(muted);
4772                            self.notify_clients(Ok(Action::TrackToggleMute(follower_name.clone())))
4773                                .await;
4774                        }
4775                    }
4776                }
4777            }
4778            Action::TrackTogglePhase(ref name) => {
4779                if let Some(track) = self.state.lock().tracks.get(name) {
4780                    track.lock().invert_phase();
4781                }
4782            }
4783            Action::TrackToggleSolo(ref name) => {
4784                if name == "hw:out" {
4785                    return;
4786                }
4787                if let Some(track) = self.state.lock().tracks.get(name) {
4788                    track.lock().solo();
4789                    let soloed = track.lock().soloed;
4790                    for follower_name in self.vca_followers(name) {
4791                        if let Some(follower) = self.state.lock().tracks.get(&follower_name)
4792                            && follower.lock().soloed != soloed
4793                        {
4794                            follower.lock().solo();
4795                            self.notify_clients(Ok(Action::TrackToggleSolo(follower_name.clone())))
4796                                .await;
4797                        }
4798                    }
4799                }
4800            }
4801            Action::TrackToggleMaster(ref name) => {
4802                if let Some(track) = self.state.lock().tracks.get(name) {
4803                    let blocked = {
4804                        let t = track.lock();
4805                        t.vca_master.is_some() || !self.vca_followers(name).is_empty()
4806                    };
4807                    if blocked {
4808                        self.notify_clients(Err(format!(
4809                            "Track '{}' cannot be promoted to Master while part of a VCA group",
4810                            name
4811                        )))
4812                        .await;
4813                        return;
4814                    }
4815                    track.lock().toggle_master();
4816                }
4817            }
4818            Action::TrackToggleInputMonitor(ref name) => {
4819                if let Some(track) = self.state.lock().tracks.get(name) {
4820                    track.lock().toggle_input_monitor();
4821                }
4822            }
4823            Action::TrackToggleDiskMonitor(ref name) => {
4824                if let Some(track) = self.state.lock().tracks.get(name) {
4825                    track.lock().toggle_disk_monitor();
4826                }
4827            }
4828            Action::TrackSetColor {
4829                ref track_name,
4830                color,
4831            } => {
4832                if let Some(track) = self.state.lock().tracks.get(track_name) {
4833                    track.lock().color = color;
4834                }
4835            }
4836            Action::TrackArmMidiLearn {
4837                ref track_name,
4838                target,
4839            } => {
4840                if let Err(e) = self.track_handle_or_err(track_name) {
4841                    self.notify_clients(Err(e)).await;
4842                    return;
4843                }
4844                self.pending_midi_learn = Some((track_name.clone(), target, None));
4845            }
4846            Action::GlobalArmMidiLearn { target } => {
4847                self.pending_global_midi_learn = Some(target);
4848            }
4849            Action::TrackSetMidiLearnBinding {
4850                ref track_name,
4851                target,
4852                ref binding,
4853            } => {
4854                if let Some(binding) = binding.as_ref() {
4855                    let conflicts = self.midi_learn_slot_conflicts(
4856                        binding,
4857                        Some(MidiLearnSlot::Track(track_name.clone(), target)),
4858                    );
4859                    if !conflicts.is_empty() {
4860                        self.notify_clients(Err(format!(
4861                            "MIDI learn conflict for '{}' {:?}: {}",
4862                            track_name,
4863                            target,
4864                            conflicts.join(", ")
4865                        )))
4866                        .await;
4867                        return;
4868                    }
4869                }
4870                let track = match self.track_handle_or_err(track_name) {
4871                    Ok(track) => track,
4872                    Err(e) => {
4873                        self.notify_clients(Err(e)).await;
4874                        return;
4875                    }
4876                };
4877                match target {
4878                    crate::message::TrackMidiLearnTarget::Volume => {
4879                        track.lock().midi_learn_volume = binding.clone();
4880                    }
4881                    crate::message::TrackMidiLearnTarget::Balance => {
4882                        track.lock().midi_learn_balance = binding.clone();
4883                    }
4884                    crate::message::TrackMidiLearnTarget::Mute => {
4885                        track.lock().midi_learn_mute = binding.clone();
4886                    }
4887                    crate::message::TrackMidiLearnTarget::Solo => {
4888                        track.lock().midi_learn_solo = binding.clone();
4889                    }
4890                    crate::message::TrackMidiLearnTarget::Arm => {
4891                        track.lock().midi_learn_arm = binding.clone();
4892                    }
4893                    crate::message::TrackMidiLearnTarget::InputMonitor => {
4894                        track.lock().midi_learn_input_monitor = binding.clone();
4895                    }
4896                    crate::message::TrackMidiLearnTarget::DiskMonitor => {
4897                        track.lock().midi_learn_disk_monitor = binding.clone();
4898                    }
4899                }
4900            }
4901            Action::SetGlobalMidiLearnBinding {
4902                target,
4903                ref binding,
4904            } => {
4905                if let Some(binding) = binding.as_ref() {
4906                    let conflicts = self
4907                        .midi_learn_slot_conflicts(binding, Some(MidiLearnSlot::Global(target)));
4908                    if !conflicts.is_empty() {
4909                        self.notify_clients(Err(format!(
4910                            "Global MIDI learn conflict for {:?}: {}",
4911                            target,
4912                            conflicts.join(", ")
4913                        )))
4914                        .await;
4915                        return;
4916                    }
4917                }
4918                match target {
4919                    crate::message::GlobalMidiLearnTarget::PlayPause => {
4920                        self.global_midi_learn_play_pause = binding.clone();
4921                    }
4922                    crate::message::GlobalMidiLearnTarget::Stop => {
4923                        self.global_midi_learn_stop = binding.clone();
4924                    }
4925                    crate::message::GlobalMidiLearnTarget::RecordToggle => {
4926                        self.global_midi_learn_record_toggle = binding.clone();
4927                    }
4928                }
4929            }
4930            Action::TrackSetVcaMaster {
4931                ref track_name,
4932                ref master_track,
4933            } => {
4934                let track = match self.track_handle_or_err(track_name) {
4935                    Ok(track) => track,
4936                    Err(e) => {
4937                        self.notify_clients(Err(e)).await;
4938                        return;
4939                    }
4940                };
4941                if track.lock().is_master {
4942                    self.notify_clients(Err(format!(
4943                        "Master track '{}' cannot be part of a VCA group",
4944                        track_name
4945                    )))
4946                    .await;
4947                    return;
4948                }
4949                if let Some(master_name) = master_track
4950                    && master_name == track_name
4951                {
4952                    self.notify_clients(Err("Track cannot be its own VCA master".to_string()))
4953                        .await;
4954                    return;
4955                }
4956                if let Some(master_name) = master_track
4957                    && let Some(master) = self.state.lock().tracks.get(master_name)
4958                    && master.lock().is_master
4959                {
4960                    self.notify_clients(Err(format!(
4961                        "Track '{}' cannot be grouped to Master track '{}'",
4962                        track_name, master_name
4963                    )))
4964                    .await;
4965                    return;
4966                }
4967                track.lock().set_vca_master(master_track.clone());
4968            }
4969            Action::TrackSetMidiLaneChannel {
4970                ref track_name,
4971                lane,
4972                channel,
4973            } => {
4974                let track = match self.track_handle_or_err(track_name) {
4975                    Ok(track) => track,
4976                    Err(e) => {
4977                        self.notify_clients(Err(e)).await;
4978                        return;
4979                    }
4980                };
4981                track.lock().set_midi_lane_channel(lane, channel);
4982            }
4983            Action::TrackSetFrozen {
4984                ref track_name,
4985                frozen,
4986            } => {
4987                let track = match self.track_handle_or_err(track_name) {
4988                    Ok(track) => track,
4989                    Err(e) => {
4990                        self.notify_clients(Err(e)).await;
4991                        return;
4992                    }
4993                };
4994                track.lock().set_frozen(frozen);
4995            }
4996            Action::TrackOfflineBounce {
4997                track_name,
4998                output_path,
4999                start_sample,
5000                length_samples,
5001                automation_lanes,
5002                apply_fader,
5003            } => {
5004                if self.offline_bounce_jobs.contains_key(&track_name) {
5005                    self.notify_clients(Err(format!(
5006                        "Offline bounce for track '{}' is already in progress",
5007                        track_name
5008                    )))
5009                    .await;
5010                    return;
5011                }
5012                if let Err(e) = self.track_handle_or_err(&track_name) {
5013                    self.notify_clients(Err(e)).await;
5014                    return;
5015                }
5016                if length_samples == 0 {
5017                    self.notify_clients(Err(format!(
5018                        "Track '{}' has no renderable content for offline bounce",
5019                        track_name
5020                    )))
5021                    .await;
5022                    return;
5023                }
5024                let Some(worker_index) = self.take_ready_worker_index(WorkerClass::Refill) else {
5025                    self.pending_requests
5026                        .push_front(Action::TrackOfflineBounce {
5027                            track_name,
5028                            output_path,
5029                            start_sample,
5030                            length_samples,
5031                            automation_lanes,
5032                            apply_fader,
5033                        });
5034                    return;
5035                };
5036                let cancel = Arc::new(AtomicBool::new(false));
5037                self.offline_bounce_jobs.insert(
5038                    track_name.clone(),
5039                    OfflineBounceJob {
5040                        cancel: cancel.clone(),
5041                    },
5042                );
5043                let track_name_clone = track_name.clone();
5044                let worker = &self.workers[worker_index];
5045                let job = crate::message::OfflineBounceWork {
5046                    state: self.state.clone(),
5047                    track_name,
5048                    output_path,
5049                    start_sample,
5050                    length_samples,
5051                    tempo_bpm: self.tempo_bpm,
5052                    tsig_num: self.tsig_num,
5053                    tsig_denom: self.tsig_denom,
5054                    automation_lanes,
5055                    cancel,
5056                    apply_fader,
5057                };
5058                if let Err(e) = worker.tx.send(Message::ProcessOfflineBounce(job)).await {
5059                    self.offline_bounce_jobs.remove(&track_name_clone);
5060                    self.notify_clients(Err(format!("Failed to schedule offline bounce: {e}")))
5061                        .await;
5062                }
5063                return;
5064            }
5065            Action::TrackOfflineBounceCancel { .. } => {}
5066            Action::TrackOfflineBounceCancelAll => {}
5067            Action::TrackOfflineBounceCanceled { .. } => {}
5068            Action::TrackOfflineBounceProgress { .. } => {}
5069            Action::PianoKey {
5070                ref track_name,
5071                note,
5072                velocity,
5073                on,
5074            } => {
5075                if let Some(track) = self.state.lock().tracks.get(track_name) {
5076                    let status = if on { 0x90 } else { 0x80 };
5077                    let event = MidiEvent::new(0, vec![status, note.min(127), velocity.min(127)]);
5078                    track.lock().push_hw_midi_events(&[event]);
5079                }
5080            }
5081            Action::ModifyMidiNotes { .. }
5082            | Action::ModifyMidiControllers { .. }
5083            | Action::DeleteMidiControllers { .. }
5084            | Action::InsertMidiControllers { .. }
5085            | Action::DeleteMidiNotes { .. }
5086            | Action::InsertMidiNotes { .. } => {
5087                if let Err(e) = self.apply_midi_edit_action(&action_to_process) {
5088                    self.notify_clients(Err(e)).await;
5089                    return;
5090                }
5091            }
5092            Action::SetMidiSysExEvents { .. } => {
5093                if let Err(e) = self.apply_midi_edit_action(&action_to_process) {
5094                    self.notify_clients(Err(e)).await;
5095                    return;
5096                }
5097            }
5098            Action::TrackClearDefaultPassthrough { ref track_name } => {
5099                if self
5100                    .reject_if_track_frozen(track_name, "plugin graph editing")
5101                    .await
5102                {
5103                    return;
5104                }
5105                let track = match self.track_handle_or_err(track_name) {
5106                    Ok(track) => track,
5107                    Err(e) => {
5108                        self.notify_clients(Err(e)).await;
5109                        return;
5110                    }
5111                };
5112                track.lock().clear_default_passthrough();
5113            }
5114            #[cfg(all(unix, not(target_os = "macos")))]
5115            Action::ClipSetLv2PluginState { ref track_name, .. } => {
5116                self.notify_clients(Err(format!(
5117                    "Track '{}': clip LV2 plugin state changes are not supported",
5118                    track_name
5119                )))
5120                .await;
5121            }
5122            Action::TrackGetClapNoteNames { ref track_name } => {
5123                let track = match self.track_handle_or_err(track_name) {
5124                    Ok(track) => track,
5125                    Err(e) => {
5126                        self.notify_clients(Err(e)).await;
5127                        return;
5128                    }
5129                };
5130                let note_names = track.lock().get_clap_note_names();
5131                self.notify_clients(Ok(Action::TrackClapNoteNames {
5132                    track_name: track_name.clone(),
5133                    note_names,
5134                }))
5135                .await;
5136            }
5137            Action::TrackGetPluginGraph { ref track_name } => {
5138                let track = match self.track_handle_or_err(track_name) {
5139                    Ok(track) => track,
5140                    Err(e) => {
5141                        self.notify_clients(Err(e)).await;
5142                        return;
5143                    }
5144                };
5145                let (plugins, connections) = {
5146                    let track = track.lock();
5147                    (
5148                        track.plugin_graph_plugins(),
5149                        track.plugin_graph_connections(),
5150                    )
5151                };
5152                self.notify_clients(Ok(Action::TrackPluginGraph {
5153                    track_name: track_name.clone(),
5154                    plugins,
5155                    connections,
5156                }))
5157                .await;
5158                return;
5159            }
5160            Action::TrackPluginGraph { .. } => {}
5161            Action::TrackConnectPluginAudio {
5162                ref track_name,
5163                ref from_node,
5164                from_port,
5165                ref to_node,
5166                to_port,
5167            } => {
5168                if self
5169                    .reject_if_track_frozen(track_name, "plugin routing changes")
5170                    .await
5171                {
5172                    return;
5173                }
5174                let track = match self.track_handle_or_err(track_name) {
5175                    Ok(track) => track,
5176                    Err(e) => {
5177                        self.notify_clients(Err(e)).await;
5178                        return;
5179                    }
5180                };
5181                if let Err(e) = track.lock().connect_plugin_audio(
5182                    from_node.clone(),
5183                    from_port,
5184                    to_node.clone(),
5185                    to_port,
5186                ) {
5187                    self.notify_clients(Err(e)).await;
5188                    return;
5189                }
5190            }
5191            Action::TrackConnectPluginMidi {
5192                ref track_name,
5193                ref from_node,
5194                from_port,
5195                ref to_node,
5196                to_port,
5197            } => {
5198                if self
5199                    .reject_if_track_frozen(track_name, "plugin routing changes")
5200                    .await
5201                {
5202                    return;
5203                }
5204                let track = match self.track_handle_or_err(track_name) {
5205                    Ok(track) => track,
5206                    Err(e) => {
5207                        self.notify_clients(Err(e)).await;
5208                        return;
5209                    }
5210                };
5211                if let Err(e) = track.lock().connect_plugin_midi(
5212                    from_node.clone(),
5213                    from_port,
5214                    to_node.clone(),
5215                    to_port,
5216                ) {
5217                    self.notify_clients(Err(e)).await;
5218                    return;
5219                }
5220            }
5221            Action::TrackDisconnectPluginAudio {
5222                ref track_name,
5223                ref from_node,
5224                from_port,
5225                ref to_node,
5226                to_port,
5227            } => {
5228                if self
5229                    .reject_if_track_frozen(track_name, "plugin routing changes")
5230                    .await
5231                {
5232                    return;
5233                }
5234                let track = match self.track_handle_or_err(track_name) {
5235                    Ok(track) => track,
5236                    Err(e) => {
5237                        self.notify_clients(Err(e)).await;
5238                        return;
5239                    }
5240                };
5241                if let Err(e) = track.lock().disconnect_plugin_audio(
5242                    from_node.clone(),
5243                    from_port,
5244                    to_node.clone(),
5245                    to_port,
5246                ) {
5247                    self.notify_clients(Err(e)).await;
5248                    return;
5249                }
5250            }
5251            Action::TrackDisconnectPluginMidi {
5252                ref track_name,
5253                ref from_node,
5254                from_port,
5255                ref to_node,
5256                to_port,
5257            } => {
5258                if self
5259                    .reject_if_track_frozen(track_name, "plugin routing changes")
5260                    .await
5261                {
5262                    return;
5263                }
5264                let track = match self.track_handle_or_err(track_name) {
5265                    Ok(track) => track,
5266                    Err(e) => {
5267                        self.notify_clients(Err(e)).await;
5268                        return;
5269                    }
5270                };
5271                if let Err(e) = track.lock().disconnect_plugin_midi(
5272                    from_node.clone(),
5273                    from_port,
5274                    to_node.clone(),
5275                    to_port,
5276                ) {
5277                    self.notify_clients(Err(e)).await;
5278                    return;
5279                }
5280            }
5281            #[cfg(all(unix, not(target_os = "macos")))]
5282            Action::ListLv2Plugins => {
5283                match crate::plugins::scan_plugins::<crate::plugins::types::Lv2PluginInfo>("lv2") {
5284                    Ok(plugins) => {
5285                        self.notify_clients(Ok(Action::Lv2Plugins(plugins))).await;
5286                    }
5287                    Err(e) => {
5288                        self.notify_clients(Err(e)).await;
5289                    }
5290                }
5291                return;
5292            }
5293            #[cfg(all(unix, not(target_os = "macos")))]
5294            Action::Lv2Plugins(_) => {}
5295            Action::ListVst3Plugins => {
5296                match crate::plugins::scan_plugins::<crate::plugins::types::Vst3PluginInfo>("vst3")
5297                {
5298                    Ok(plugins) => {
5299                        self.notify_clients(Ok(Action::Vst3Plugins(plugins))).await;
5300                    }
5301                    Err(e) => {
5302                        self.notify_clients(Err(e)).await;
5303                    }
5304                }
5305                return;
5306            }
5307            Action::Vst3Plugins(_) => {}
5308            Action::ListClapPlugins => {
5309                match crate::plugins::scan_plugins::<crate::plugins::types::ClapPluginInfo>("clap")
5310                {
5311                    Ok(plugins) => {
5312                        self.notify_clients(Ok(Action::ClapPlugins(plugins))).await;
5313                    }
5314                    Err(e) => {
5315                        self.notify_clients(Err(e)).await;
5316                    }
5317                }
5318                return;
5319            }
5320            Action::ListClapPluginsWithCapabilities => {
5321                match crate::plugins::scan_plugins::<crate::plugins::types::ClapPluginInfo>("clap")
5322                {
5323                    Ok(plugins) => {
5324                        self.notify_clients(Ok(Action::ClapPlugins(plugins))).await;
5325                    }
5326                    Err(e) => {
5327                        self.notify_clients(Err(e)).await;
5328                    }
5329                }
5330                return;
5331            }
5332            Action::ClapPlugins(_) => {}
5333            Action::TrackLoadClapPlugin {
5334                ref track_name,
5335                ref plugin_path,
5336                instance_id,
5337            } => {
5338                if self
5339                    .reject_if_track_frozen(track_name, "CLAP plugin loading")
5340                    .await
5341                {
5342                    return;
5343                }
5344                let track = match self.track_handle_or_err(track_name) {
5345                    Ok(track) => track,
5346                    Err(e) => {
5347                        self.notify_clients(Err(e)).await;
5348                        return;
5349                    }
5350                };
5351                let track = track.lock();
5352                if track.audio.processing {
5353                    self.notify_clients(Err(format!(
5354                        "Track '{}' is currently processing audio; stop playback before loading CLAP plugins",
5355                        track_name
5356                    )))
5357                    .await;
5358                    return;
5359                }
5360                if let Err(e) = track.load_clap_plugin(plugin_path, instance_id) {
5361                    self.notify_clients(Err(e)).await;
5362                    return;
5363                }
5364            }
5365            Action::TrackUnloadClapPlugin {
5366                ref track_name,
5367                ref plugin_path,
5368            } => {
5369                if self
5370                    .reject_if_track_frozen(track_name, "CLAP plugin unloading")
5371                    .await
5372                {
5373                    return;
5374                }
5375                let track = match self.track_handle_or_err(track_name) {
5376                    Ok(track) => track,
5377                    Err(e) => {
5378                        self.notify_clients(Err(e)).await;
5379                        return;
5380                    }
5381                };
5382                let track = track.lock();
5383                if track.audio.processing {
5384                    self.notify_clients(Err(format!(
5385                        "Track '{}' is currently processing audio; stop playback before unloading CLAP plugins",
5386                        track_name
5387                    )))
5388                    .await;
5389                    return;
5390                }
5391                if let Err(e) = track.unload_clap_plugin(plugin_path) {
5392                    self.notify_clients(Err(e)).await;
5393                    return;
5394                }
5395            }
5396            Action::TrackUnloadClapPluginInstance {
5397                ref track_name,
5398                instance_id,
5399            } => {
5400                if self
5401                    .reject_if_track_frozen(track_name, "CLAP plugin unloading")
5402                    .await
5403                {
5404                    return;
5405                }
5406                let track = match self.track_handle_or_err(track_name) {
5407                    Ok(track) => track,
5408                    Err(e) => {
5409                        self.notify_clients(Err(e)).await;
5410                        return;
5411                    }
5412                };
5413                let track = track.lock();
5414                if track.audio.processing {
5415                    self.notify_clients(Err(format!(
5416                        "Track '{}' is currently processing audio; stop playback before unloading CLAP plugins",
5417                        track_name
5418                    )))
5419                    .await;
5420                    return;
5421                }
5422                if let Err(e) = track.unload_clap_plugin_instance(instance_id) {
5423                    self.notify_clients(Err(e)).await;
5424                    return;
5425                }
5426            }
5427            Action::TrackShowClapGui {
5428                ref track_name,
5429                instance_id,
5430            } => {
5431                let track = match self.track_handle_or_err(track_name) {
5432                    Ok(track) => track,
5433                    Err(e) => {
5434                        self.notify_clients(Err(e)).await;
5435                        return;
5436                    }
5437                };
5438                if let Err(e) = track.lock().show_clap_gui(instance_id) {
5439                    self.notify_clients(Err(e)).await;
5440                    return;
5441                }
5442            }
5443            Action::TrackLoadVst3Plugin {
5444                ref track_name,
5445                ref plugin_path,
5446                instance_id,
5447            } => {
5448                if self
5449                    .reject_if_track_frozen(track_name, "VST3 plugin loading")
5450                    .await
5451                {
5452                    return;
5453                }
5454                let track = match self.track_handle_or_err(track_name) {
5455                    Ok(track) => track,
5456                    Err(e) => {
5457                        self.notify_clients(Err(e)).await;
5458                        return;
5459                    }
5460                };
5461                let track = track.lock();
5462                if track.audio.processing {
5463                    self.notify_clients(Err(format!(
5464                        "Track '{}' is currently processing audio; stop playback before loading VST3 plugins",
5465                        track_name
5466                    )))
5467                    .await;
5468                    return;
5469                }
5470                if let Err(e) = track.load_vst3_plugin(plugin_path, instance_id) {
5471                    self.notify_clients(Err(e)).await;
5472                    return;
5473                }
5474            }
5475            Action::TrackUnloadVst3Plugin {
5476                ref track_name,
5477                ref plugin_path,
5478            } => {
5479                if self
5480                    .reject_if_track_frozen(track_name, "VST3 plugin unloading")
5481                    .await
5482                {
5483                    return;
5484                }
5485                let track = match self.track_handle_or_err(track_name) {
5486                    Ok(track) => track,
5487                    Err(e) => {
5488                        self.notify_clients(Err(e)).await;
5489                        return;
5490                    }
5491                };
5492                let track = track.lock();
5493                if track.audio.processing {
5494                    self.notify_clients(Err(format!(
5495                        "Track '{}' is currently processing audio; stop playback before unloading VST3 plugins",
5496                        track_name
5497                    )))
5498                    .await;
5499                    return;
5500                }
5501                if let Err(e) = track.unload_vst3_plugin(plugin_path) {
5502                    self.notify_clients(Err(e)).await;
5503                    return;
5504                }
5505            }
5506            Action::TrackUnloadVst3PluginInstance {
5507                ref track_name,
5508                instance_id,
5509            } => {
5510                if self
5511                    .reject_if_track_frozen(track_name, "VST3 plugin unloading")
5512                    .await
5513                {
5514                    return;
5515                }
5516                let track = match self.track_handle_or_err(track_name) {
5517                    Ok(track) => track,
5518                    Err(e) => {
5519                        self.notify_clients(Err(e)).await;
5520                        return;
5521                    }
5522                };
5523                let track = track.lock();
5524                if track.audio.processing {
5525                    self.notify_clients(Err(format!(
5526                        "Track '{}' is currently processing audio; stop playback before unloading VST3 plugins",
5527                        track_name
5528                    )))
5529                    .await;
5530                    return;
5531                }
5532                if let Err(e) = track.unload_vst3_plugin_instance(instance_id) {
5533                    self.notify_clients(Err(e)).await;
5534                    return;
5535                }
5536            }
5537            Action::TrackShowVst3Gui {
5538                ref track_name,
5539                instance_id,
5540            } => {
5541                let track = match self.track_handle_or_err(track_name) {
5542                    Ok(track) => track,
5543                    Err(e) => {
5544                        self.notify_clients(Err(e)).await;
5545                        return;
5546                    }
5547                };
5548                if let Err(e) = track.lock().show_vst3_gui(instance_id) {
5549                    self.notify_clients(Err(e)).await;
5550                    return;
5551                }
5552            }
5553            #[cfg(all(unix, not(target_os = "macos")))]
5554            Action::TrackLoadLv2Plugin {
5555                ref track_name,
5556                ref plugin_uri,
5557                instance_id,
5558            } => {
5559                if self
5560                    .reject_if_track_frozen(track_name, "LV2 plugin loading")
5561                    .await
5562                {
5563                    return;
5564                }
5565                let track = match self.track_handle_or_err(track_name) {
5566                    Ok(track) => track,
5567                    Err(e) => {
5568                        self.notify_clients(Err(e)).await;
5569                        return;
5570                    }
5571                };
5572                let track = track.lock();
5573                if track.audio.processing {
5574                    self.notify_clients(Err(format!(
5575                        "Track '{}' is currently processing audio; stop playback before loading LV2 plugins",
5576                        track_name
5577                    )))
5578                    .await;
5579                    return;
5580                }
5581                if let Err(e) = track.load_lv2_plugin(plugin_uri, instance_id) {
5582                    self.notify_clients(Err(e)).await;
5583                    return;
5584                }
5585            }
5586            #[cfg(all(unix, not(target_os = "macos")))]
5587            Action::TrackUnloadLv2Plugin {
5588                ref track_name,
5589                ref plugin_uri,
5590            } => {
5591                if self
5592                    .reject_if_track_frozen(track_name, "LV2 plugin unloading")
5593                    .await
5594                {
5595                    return;
5596                }
5597                let track = match self.track_handle_or_err(track_name) {
5598                    Ok(track) => track,
5599                    Err(e) => {
5600                        self.notify_clients(Err(e)).await;
5601                        return;
5602                    }
5603                };
5604                let track = track.lock();
5605                if track.audio.processing {
5606                    self.notify_clients(Err(format!(
5607                        "Track '{}' is currently processing audio; stop playback before unloading LV2 plugins",
5608                        track_name
5609                    )))
5610                    .await;
5611                    return;
5612                }
5613                if let Err(e) = track.unload_lv2_plugin(plugin_uri) {
5614                    self.notify_clients(Err(e)).await;
5615                    return;
5616                }
5617            }
5618            #[cfg(all(unix, not(target_os = "macos")))]
5619            Action::TrackUnloadLv2PluginInstance {
5620                ref track_name,
5621                instance_id,
5622            } => {
5623                if self
5624                    .reject_if_track_frozen(track_name, "LV2 plugin unloading")
5625                    .await
5626                {
5627                    return;
5628                }
5629                let track = match self.track_handle_or_err(track_name) {
5630                    Ok(track) => track,
5631                    Err(e) => {
5632                        self.notify_clients(Err(e)).await;
5633                        return;
5634                    }
5635                };
5636                let track = track.lock();
5637                if track.audio.processing {
5638                    self.notify_clients(Err(format!(
5639                        "Track '{}' is currently processing audio; stop playback before unloading LV2 plugins",
5640                        track_name
5641                    )))
5642                    .await;
5643                    return;
5644                }
5645                if let Err(e) = track.unload_lv2_plugin_instance(instance_id) {
5646                    self.notify_clients(Err(e)).await;
5647                    return;
5648                }
5649            }
5650            #[cfg(all(unix, not(target_os = "macos")))]
5651            Action::TrackShowLv2Gui {
5652                ref track_name,
5653                instance_id,
5654            } => {
5655                let track = match self.track_handle_or_err(track_name) {
5656                    Ok(track) => track,
5657                    Err(e) => {
5658                        self.notify_clients(Err(e)).await;
5659                        return;
5660                    }
5661                };
5662                if let Err(e) = track.lock().show_lv2_gui(instance_id) {
5663                    self.notify_clients(Err(e)).await;
5664                    return;
5665                }
5666            }
5667            Action::TrackSetClapParameter {
5668                ref track_name,
5669                instance_id,
5670                param_id,
5671                value,
5672            } => {
5673                if self
5674                    .reject_if_track_frozen(track_name, "CLAP parameter changes")
5675                    .await
5676                {
5677                    return;
5678                }
5679                match self.track_handle_or_err(track_name) {
5680                    Ok(track) => {
5681                        if let Err(e) =
5682                            track
5683                                .lock()
5684                                .set_clap_parameter(instance_id, param_id, value)
5685                        {
5686                            self.notify_clients(Err(e)).await;
5687                            return;
5688                        }
5689                        self.notify_clients(Ok(a.clone())).await;
5690                    }
5691                    Err(e) => {
5692                        self.notify_clients(Err(e)).await;
5693                    }
5694                }
5695            }
5696            Action::ClipSetClapParameter {
5697                ref track_name,
5698                clip_idx,
5699                instance_id,
5700                param_id,
5701                value,
5702            } => {
5703                if self
5704                    .reject_if_track_frozen(track_name, "CLAP parameter changes")
5705                    .await
5706                {
5707                    return;
5708                }
5709                match self.track_handle_or_err(track_name) {
5710                    Ok(track) => {
5711                        if let Err(e) = track.lock().clip_set_clap_parameter(
5712                            clip_idx,
5713                            instance_id,
5714                            param_id,
5715                            value,
5716                        ) {
5717                            self.notify_clients(Err(e)).await;
5718                            return;
5719                        }
5720                        self.notify_clients(Ok(a.clone())).await;
5721                    }
5722                    Err(e) => {
5723                        self.notify_clients(Err(e)).await;
5724                    }
5725                }
5726            }
5727            Action::TrackSetClapParameterAt {
5728                ref track_name,
5729                instance_id,
5730                param_id,
5731                value,
5732                frame,
5733            } => {
5734                if self
5735                    .reject_if_track_frozen(track_name, "CLAP parameter changes")
5736                    .await
5737                {
5738                    return;
5739                }
5740                match self.track_handle_or_err(track_name) {
5741                    Ok(track) => {
5742                        if let Err(e) =
5743                            track
5744                                .lock()
5745                                .set_clap_parameter_at(instance_id, param_id, value, frame)
5746                        {
5747                            self.notify_clients(Err(e)).await;
5748                            return;
5749                        }
5750                        self.notify_clients(Ok(a.clone())).await;
5751                    }
5752                    Err(e) => {
5753                        self.notify_clients(Err(e)).await;
5754                    }
5755                }
5756            }
5757            Action::TrackBeginClapParameterEdit {
5758                ref track_name,
5759                instance_id,
5760                param_id,
5761                frame,
5762            } => {
5763                if self
5764                    .reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
5765                    .await
5766                {
5767                    return;
5768                }
5769                match self.track_handle_or_err(track_name) {
5770                    Ok(track) => {
5771                        if let Err(e) =
5772                            track
5773                                .lock()
5774                                .begin_clap_parameter_edit(instance_id, param_id, frame)
5775                        {
5776                            self.notify_clients(Err(e)).await;
5777                            return;
5778                        }
5779                        self.notify_clients(Ok(a.clone())).await;
5780                    }
5781                    Err(e) => {
5782                        self.notify_clients(Err(e)).await;
5783                    }
5784                }
5785            }
5786            Action::TrackEndClapParameterEdit {
5787                ref track_name,
5788                instance_id,
5789                param_id,
5790                frame,
5791            } => {
5792                if self
5793                    .reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
5794                    .await
5795                {
5796                    return;
5797                }
5798                match self.track_handle_or_err(track_name) {
5799                    Ok(track) => {
5800                        if let Err(e) =
5801                            track
5802                                .lock()
5803                                .end_clap_parameter_edit(instance_id, param_id, frame)
5804                        {
5805                            self.notify_clients(Err(e)).await;
5806                            return;
5807                        }
5808                        self.notify_clients(Ok(a.clone())).await;
5809                    }
5810                    Err(e) => {
5811                        self.notify_clients(Err(e)).await;
5812                    }
5813                }
5814            }
5815            Action::TrackGetClapParameters {
5816                ref track_name,
5817                instance_id,
5818            } => match self.track_handle_or_err(track_name) {
5819                Ok(track) => match track.lock().get_clap_parameters(instance_id) {
5820                    Ok(parameters) => {
5821                        self.notify_clients(Ok(Action::TrackClapParameters {
5822                            track_name: track_name.clone(),
5823                            instance_id,
5824                            parameters,
5825                        }))
5826                        .await;
5827                    }
5828                    Err(e) => {
5829                        self.notify_clients(Err(e)).await;
5830                    }
5831                },
5832                Err(e) => {
5833                    self.notify_clients(Err(e)).await;
5834                }
5835            },
5836            Action::TrackClapParameters { .. } => {}
5837            Action::TrackClapSnapshotState {
5838                ref track_name,
5839                instance_id,
5840            } => match self.track_handle_or_err(track_name) {
5841                Ok(track) => {
5842                    let plugin_path = track
5843                        .lock()
5844                        .clap_plugins
5845                        .iter()
5846                        .find(|instance| instance.id == instance_id)
5847                        .map(|instance| instance.processor.lock().path().to_string())
5848                        .unwrap_or_default();
5849                    match track.lock().clap_snapshot_state(instance_id) {
5850                        Ok(state) => {
5851                            self.notify_clients(Ok(Action::TrackClapStateSnapshot {
5852                                track_name: track_name.clone(),
5853                                instance_id,
5854                                plugin_path,
5855                                state,
5856                            }))
5857                            .await;
5858                        }
5859                        Err(e) => {
5860                            self.notify_clients(Err(e)).await;
5861                        }
5862                    }
5863                }
5864                Err(e) => {
5865                    self.notify_clients(Err(e)).await;
5866                }
5867            },
5868            Action::ClipClapSnapshotState {
5869                ref track_name,
5870                clip_idx,
5871                instance_id,
5872            } => match self.track_handle_or_err(track_name) {
5873                Ok(track) => match track.lock().clip_clap_snapshot_state(clip_idx, instance_id) {
5874                    Ok((plugin_path, state)) => {
5875                        self.notify_clients(Ok(Action::ClipClapStateSnapshot {
5876                            track_name: track_name.clone(),
5877                            clip_idx,
5878                            instance_id,
5879                            plugin_path,
5880                            state,
5881                        }))
5882                        .await;
5883                    }
5884                    Err(e) => {
5885                        self.notify_clients(Err(e)).await;
5886                    }
5887                },
5888                Err(e) => {
5889                    self.notify_clients(Err(e)).await;
5890                }
5891            },
5892            Action::TrackClapStateSnapshot { .. } => {}
5893            Action::ClipClapStateSnapshot { .. } => {}
5894            Action::TrackClapRestoreState {
5895                ref track_name,
5896                instance_id,
5897                ref state,
5898            } => {
5899                if self
5900                    .reject_if_track_frozen(track_name, "CLAP state restore")
5901                    .await
5902                {
5903                    return;
5904                }
5905                let track = match self.track_handle_or_err(track_name) {
5906                    Ok(track) => track,
5907                    Err(e) => {
5908                        self.notify_clients(Err(e)).await;
5909                        return;
5910                    }
5911                };
5912                let track = track.lock();
5913                if track.audio.processing {
5914                    self.notify_clients(Err(format!(
5915                        "Track '{}' is currently processing audio; stop playback before restoring CLAP state",
5916                        track_name
5917                    )))
5918                    .await;
5919                    return;
5920                }
5921                if let Err(e) = track.clap_restore_state(instance_id, state) {
5922                    self.notify_clients(Err(e)).await;
5923                    return;
5924                }
5925            }
5926            Action::ClipClapRestoreState {
5927                ref track_name,
5928                clip_idx,
5929                instance_id,
5930                ref state,
5931            } => {
5932                if self
5933                    .reject_if_track_frozen(track_name, "CLAP state restore")
5934                    .await
5935                {
5936                    return;
5937                }
5938                let track = match self.track_handle_or_err(track_name) {
5939                    Ok(track) => track,
5940                    Err(e) => {
5941                        self.notify_clients(Err(e)).await;
5942                        return;
5943                    }
5944                };
5945                let track = track.lock();
5946                if track.audio.processing {
5947                    self.notify_clients(Err(format!(
5948                        "Track '{}' is currently processing audio; stop playback before restoring CLAP state",
5949                        track_name
5950                    )))
5951                    .await;
5952                    return;
5953                }
5954                if let Err(e) = track.clip_clap_restore_state(clip_idx, instance_id, state) {
5955                    self.notify_clients(Err(e)).await;
5956                    return;
5957                }
5958            }
5959            Action::TrackSnapshotAllClapStates { ref track_name } => {
5960                let track = match self.track_handle_or_err(track_name) {
5961                    Ok(track) => track,
5962                    Err(e) => {
5963                        self.notify_clients(Err(e)).await;
5964                        return;
5965                    }
5966                };
5967                for (instance_id, plugin_path, state) in track.lock().clap_snapshot_all_states() {
5968                    self.notify_clients(Ok(Action::TrackClapStateSnapshot {
5969                        track_name: track_name.clone(),
5970                        instance_id,
5971                        plugin_path,
5972                        state,
5973                    }))
5974                    .await;
5975                }
5976                self.notify_clients(Ok(Action::TrackSnapshotAllClapStatesDone {
5977                    track_name: track_name.clone(),
5978                }))
5979                .await;
5980            }
5981            Action::TrackSnapshotAllClapStatesDone { .. } => {}
5982            Action::TrackGetVst3Graph { ref track_name } => {
5983                match self.track_handle_or_err(track_name) {
5984                    Ok(track) => {
5985                        let t = track.lock();
5986                        let plugins = t.vst3_graph_plugins();
5987                        let connections = t.vst3_graph_connections();
5988                        self.notify_clients(Ok(Action::TrackVst3Graph {
5989                            track_name: track_name.clone(),
5990                            plugins,
5991                            connections,
5992                        }))
5993                        .await;
5994                    }
5995                    Err(e) => {
5996                        self.notify_clients(Err(e)).await;
5997                    }
5998                }
5999            }
6000            Action::TrackVst3Graph { .. } => {}
6001            Action::TrackSetVst3Parameter {
6002                ref track_name,
6003                instance_id,
6004                param_id,
6005                value,
6006            } => {
6007                if self
6008                    .reject_if_track_frozen(track_name, "VST3 parameter changes")
6009                    .await
6010                {
6011                    return;
6012                }
6013                match self.track_handle_or_err(track_name) {
6014                    Ok(track) => {
6015                        if let Err(e) =
6016                            track
6017                                .lock()
6018                                .set_vst3_parameter(instance_id, param_id, value)
6019                        {
6020                            self.notify_clients(Err(e)).await;
6021                            return;
6022                        }
6023                        self.notify_clients(Ok(a.clone())).await;
6024                    }
6025                    Err(e) => {
6026                        self.notify_clients(Err(e)).await;
6027                    }
6028                }
6029            }
6030            Action::TrackSetPluginBypassed {
6031                ref track_name,
6032                instance_id,
6033                ref format,
6034                bypassed,
6035            } => match self.track_handle_or_err(track_name) {
6036                Ok(track) => {
6037                    let result = match format.as_str() {
6038                        "CLAP" => track.lock().set_clap_plugin_bypassed(instance_id, bypassed),
6039                        "VST3" => track.lock().set_vst3_plugin_bypassed(instance_id, bypassed),
6040                        #[cfg(all(unix, not(target_os = "macos")))]
6041                        "LV2" => track.lock().set_lv2_plugin_bypassed(instance_id, bypassed),
6042                        _ => Err(format!("Unknown plugin format for bypass: {format}")),
6043                    };
6044                    if let Err(e) = result {
6045                        self.notify_clients(Err(e)).await;
6046                        return;
6047                    }
6048                    self.notify_clients(Ok(a.clone())).await;
6049                }
6050                Err(e) => {
6051                    self.notify_clients(Err(e)).await;
6052                }
6053            },
6054            Action::TrackGetVst3Parameters {
6055                ref track_name,
6056                instance_id,
6057            } => match self.track_handle_or_err(track_name) {
6058                Ok(track) => match track.lock().get_vst3_parameters(instance_id) {
6059                    Ok(parameters) => {
6060                        self.notify_clients(Ok(Action::TrackVst3Parameters {
6061                            track_name: track_name.clone(),
6062                            instance_id,
6063                            parameters,
6064                        }))
6065                        .await;
6066                    }
6067                    Err(e) => {
6068                        self.notify_clients(Err(e)).await;
6069                    }
6070                },
6071                Err(e) => {
6072                    self.notify_clients(Err(e)).await;
6073                }
6074            },
6075            Action::TrackVst3Parameters { .. } => {}
6076            Action::TrackVst3SnapshotState {
6077                ref track_name,
6078                instance_id,
6079            } => match self.track_handle_or_err(track_name) {
6080                Ok(track) => match track.lock().vst3_snapshot_state(instance_id) {
6081                    Ok(state) => {
6082                        self.notify_clients(Ok(Action::TrackVst3StateSnapshot {
6083                            track_name: track_name.clone(),
6084                            instance_id,
6085                            state,
6086                        }))
6087                        .await;
6088                    }
6089                    Err(e) => {
6090                        self.notify_clients(Err(e)).await;
6091                    }
6092                },
6093                Err(e) => {
6094                    self.notify_clients(Err(e)).await;
6095                }
6096            },
6097            Action::ClipVst3SnapshotState {
6098                ref track_name,
6099                clip_idx,
6100                instance_id,
6101            } => match self.track_handle_or_err(track_name) {
6102                Ok(track) => match track.lock().clip_vst3_snapshot_state(clip_idx, instance_id) {
6103                    Ok(state) => {
6104                        self.notify_clients(Ok(Action::ClipVst3StateSnapshot {
6105                            track_name: track_name.clone(),
6106                            clip_idx,
6107                            instance_id,
6108                            state,
6109                        }))
6110                        .await;
6111                    }
6112                    Err(e) => {
6113                        self.notify_clients(Err(e)).await;
6114                    }
6115                },
6116                Err(e) => {
6117                    self.notify_clients(Err(e)).await;
6118                }
6119            },
6120            Action::TrackVst3StateSnapshot { .. } => {}
6121            Action::ClipVst3StateSnapshot { .. } => {}
6122            Action::TrackVst3RestoreState {
6123                ref track_name,
6124                instance_id,
6125                ref state,
6126            } => match self.track_handle_or_err(track_name) {
6127                Ok(track) => {
6128                    if let Err(e) = track.lock().vst3_restore_state(instance_id, state) {
6129                        self.notify_clients(Err(e)).await;
6130                        return;
6131                    }
6132                    self.notify_clients(Ok(a.clone())).await;
6133                }
6134                Err(e) => {
6135                    self.notify_clients(Err(e)).await;
6136                }
6137            },
6138            Action::TrackConnectVst3Audio {
6139                ref track_name,
6140                ref from_node,
6141                from_port,
6142                ref to_node,
6143                to_port,
6144            } => {
6145                if self
6146                    .reject_if_track_frozen(track_name, "VST3 routing changes")
6147                    .await
6148                {
6149                    return;
6150                }
6151                match self.track_handle_or_err(track_name) {
6152                    Ok(track) => {
6153                        if let Err(e) = track
6154                            .lock()
6155                            .connect_vst3_audio(from_node, from_port, to_node, to_port)
6156                        {
6157                            self.notify_clients(Err(e)).await;
6158                            return;
6159                        }
6160                        self.notify_clients(Ok(a.clone())).await;
6161                    }
6162                    Err(e) => {
6163                        self.notify_clients(Err(e)).await;
6164                    }
6165                }
6166            }
6167            Action::TrackDisconnectVst3Audio {
6168                ref track_name,
6169                ref from_node,
6170                from_port,
6171                ref to_node,
6172                to_port,
6173            } => {
6174                if self
6175                    .reject_if_track_frozen(track_name, "VST3 routing changes")
6176                    .await
6177                {
6178                    return;
6179                }
6180                match self.track_handle_or_err(track_name) {
6181                    Ok(track) => {
6182                        if let Err(e) = track
6183                            .lock()
6184                            .disconnect_vst3_audio(from_node, from_port, to_node, to_port)
6185                        {
6186                            self.notify_clients(Err(e)).await;
6187                            return;
6188                        }
6189                        self.notify_clients(Ok(a.clone())).await;
6190                    }
6191                    Err(e) => {
6192                        self.notify_clients(Err(e)).await;
6193                    }
6194                }
6195            }
6196            Action::ClipMove {
6197                ref kind,
6198                ref from,
6199                ref to,
6200                copy,
6201            } => {
6202                if let Some(from_track_handle) = self.state.lock().tracks.get(&from.track_name)
6203                    && let Some(to_track_handle) = self.state.lock().tracks.get(&to.track_name)
6204                {
6205                    let from_track = from_track_handle.lock();
6206                    let to_track = to_track_handle.lock();
6207                    match kind {
6208                        Kind::Audio => {
6209                            if from.clip_index >= from_track.audio.clips.len() {
6210                                self.notify_clients(Err(format!(
6211                                    "Clip index {} is too high, as track {} has only {} clips!",
6212                                    from.clip_index,
6213                                    from_track.name.clone(),
6214                                    from_track.audio.clips.len(),
6215                                )))
6216                                .await;
6217                                return;
6218                            }
6219                            if from_track.audio.ins.len() != to_track.audio.ins.len() {
6220                                self.notify_clients(Err(format!(
6221                                    "Cannot move/copy audio clip from '{}' ({} inputs) to '{}' ({} inputs)",
6222                                    from_track.name,
6223                                    from_track.audio.ins.len(),
6224                                    to_track.name,
6225                                    to_track.audio.ins.len()
6226                                )))
6227                                .await;
6228                                return;
6229                            }
6230                            let clip_copy = from_track.audio.clips[from.clip_index].clone();
6231                            if !copy {
6232                                from_track.audio.clips.remove(from.clip_index);
6233                            }
6234                            let mut clip_copy = clip_copy;
6235                            clip_copy.start = to.sample_offset;
6236                            let max_lane = to_track.audio.ins.len().saturating_sub(1);
6237                            clip_copy.input_channel = to.input_channel.min(max_lane);
6238                            to_track.audio.clips.push(clip_copy);
6239                        }
6240                        Kind::MIDI => {
6241                            if from.clip_index >= from_track.midi.clips.len() {
6242                                self.notify_clients(Err(format!(
6243                                    "Clip index {} is too high, as track {} has only {} clips!",
6244                                    from.clip_index,
6245                                    from_track.name.clone(),
6246                                    from_track.midi.clips.len(),
6247                                )))
6248                                .await;
6249                                return;
6250                            }
6251                            let clip_copy = from_track.midi.clips[from.clip_index].clone();
6252                            if !copy {
6253                                from_track.midi.clips.remove(from.clip_index);
6254                            }
6255                            let mut clip_copy = clip_copy;
6256                            clip_copy.start = to.sample_offset;
6257                            let max_lane = to_track.midi.ins.len().saturating_sub(1);
6258                            clip_copy.input_channel = to.input_channel.min(max_lane);
6259                            to_track.midi.clips.push(clip_copy);
6260                        }
6261                    }
6262                }
6263            }
6264            Action::AddClip {
6265                ref name,
6266                ref track_name,
6267                start,
6268                length,
6269                offset,
6270                input_channel,
6271                muted,
6272                ref peaks_file,
6273                kind,
6274                fade_enabled,
6275                fade_in_samples,
6276                fade_out_samples,
6277                ref source_name,
6278                source_offset,
6279                source_length,
6280                ref preview_name,
6281                ref pitch_correction_points,
6282                pitch_correction_frame_likeness,
6283                pitch_correction_inertia_ms,
6284                pitch_correction_formant_compensation,
6285                ref plugin_graph_json,
6286            } => {
6287                self.add_clip_to_track(ClipAddRequest {
6288                    name,
6289                    track_name,
6290                    start,
6291                    length,
6292                    offset,
6293                    input_channel,
6294                    muted,
6295                    peaks_file: peaks_file.clone(),
6296                    kind,
6297                    fade_enabled,
6298                    fade_in_samples,
6299                    fade_out_samples,
6300                    source_name: source_name.clone(),
6301                    source_offset,
6302                    source_length,
6303                    preview_name: preview_name.clone(),
6304                    pitch_correction_points: pitch_correction_points.clone(),
6305                    pitch_correction_frame_likeness,
6306                    pitch_correction_inertia_ms,
6307                    pitch_correction_formant_compensation,
6308                    plugin_graph_json: plugin_graph_json.clone(),
6309                });
6310                if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
6311                    let track_name = track_name.clone();
6312                    tokio::task::spawn_blocking(move || {
6313                        track.lock().preload_clips();
6314                        tracing::debug!("Preloaded clips for track '{}' after AddClip", track_name);
6315                    });
6316                }
6317            }
6318            Action::AddGroupedClip {
6319                ref track_name,
6320                kind,
6321                ref audio_clip,
6322                ref midi_clip,
6323            } => {
6324                self.add_grouped_clip_to_track(
6325                    track_name,
6326                    kind,
6327                    audio_clip.clone(),
6328                    midi_clip.clone(),
6329                );
6330                if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
6331                    let track_name = track_name.clone();
6332                    tokio::task::spawn_blocking(move || {
6333                        track.lock().preload_clips();
6334                        tracing::debug!(
6335                            "Preloaded clips for track '{}' after AddGroupedClip",
6336                            track_name
6337                        );
6338                    });
6339                }
6340            }
6341            Action::RemoveClip {
6342                ref track_name,
6343                kind,
6344                ref clip_indices,
6345            } => {
6346                self.remove_clips_from_track(track_name, kind, clip_indices);
6347            }
6348            Action::RenameClip {
6349                ref track_name,
6350                kind,
6351                clip_index,
6352                ref new_name,
6353            } => {
6354                self.rename_clip_references(track_name, kind, clip_index, new_name);
6355            }
6356            Action::SetClipSourceName {
6357                ref track_name,
6358                kind,
6359                clip_index,
6360                ref name,
6361            } => {
6362                self.set_clip_source_name(track_name, clip_index, kind, name.clone());
6363            }
6364            Action::SetClipFade {
6365                ref track_name,
6366                clip_index,
6367                kind,
6368                fade_enabled,
6369                fade_in_samples,
6370                fade_out_samples,
6371            } => {
6372                self.set_clip_fade(
6373                    track_name,
6374                    clip_index,
6375                    kind,
6376                    fade_enabled,
6377                    fade_in_samples,
6378                    fade_out_samples,
6379                );
6380            }
6381            Action::SetClipBounds {
6382                ref track_name,
6383                clip_index,
6384                kind,
6385                start,
6386                length,
6387                offset,
6388            } => {
6389                self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
6390            }
6391            Action::SyncClipBounds {
6392                ref track_name,
6393                clip_index,
6394                kind,
6395                start,
6396                length,
6397                offset,
6398            } => {
6399                self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
6400            }
6401            Action::SetClipMuted {
6402                ref track_name,
6403                clip_index,
6404                kind,
6405                muted,
6406            } => {
6407                self.set_clip_muted(track_name, clip_index, kind, muted);
6408            }
6409            Action::SetClipPluginGraphJson {
6410                ref track_name,
6411                clip_index,
6412                ref plugin_graph_json,
6413            } => {
6414                self.set_clip_plugin_graph_json(track_name, clip_index, plugin_graph_json.clone());
6415            }
6416            Action::SetClipPitchCorrection {
6417                ref track_name,
6418                clip_index,
6419                ref preview_name,
6420                ref source_name,
6421                source_offset,
6422                source_length,
6423                ref pitch_correction_points,
6424                pitch_correction_frame_likeness,
6425                pitch_correction_inertia_ms,
6426                pitch_correction_formant_compensation,
6427            } => {
6428                self.set_clip_pitch_correction(
6429                    track_name,
6430                    clip_index,
6431                    preview_name.clone(),
6432                    source_name.clone(),
6433                    source_offset,
6434                    source_length,
6435                    pitch_correction_points.clone(),
6436                    pitch_correction_frame_likeness,
6437                    pitch_correction_inertia_ms,
6438                    pitch_correction_formant_compensation,
6439                );
6440            }
6441            Action::Connect {
6442                ref from_track,
6443                from_port,
6444                ref to_track,
6445                to_port,
6446                kind,
6447            } => {
6448                match kind {
6449                    Kind::Audio => {
6450                        let from_audio_io = if from_track == "hw:in" {
6451                            self.hw_input_audio_port(from_port)
6452                        } else {
6453                            self.state
6454                                .lock()
6455                                .tracks
6456                                .get(from_track)
6457                                .and_then(|t| t.lock().audio.outs.get(from_port).cloned())
6458                        };
6459                        let to_audio_io = if to_track == "hw:out" {
6460                            self.hw_output_audio_port(to_port)
6461                        } else {
6462                            self.state
6463                                .lock()
6464                                .tracks
6465                                .get(to_track)
6466                                .and_then(|t| t.lock().audio.ins.get(to_port).cloned())
6467                        };
6468                        match (from_audio_io, to_audio_io) {
6469                            (Some(source), Some(target)) => {
6470                                if from_track != "hw:in"
6471                                    && to_track != "hw:out"
6472                                    && self.check_if_leads_to_kind(
6473                                        Kind::Audio,
6474                                        to_track,
6475                                        from_track,
6476                                    )
6477                                {
6478                                    self.notify_clients(Err(
6479                                        "Circular routing is not allowed!".into()
6480                                    ))
6481                                    .await;
6482                                    return;
6483                                }
6484                                crate::audio::io::AudioIO::connect(&source, &target);
6485                            }
6486                            (None, _) => {
6487                                self.notify_clients(Err(format!(
6488                                    "Source track '{}' not found",
6489                                    from_track
6490                                )))
6491                                .await;
6492                                return;
6493                            }
6494                            (_, None) => {
6495                                self.notify_clients(Err(format!(
6496                                    "Destination track '{}' not found",
6497                                    to_track
6498                                )))
6499                                .await;
6500                                return;
6501                            }
6502                        }
6503                    }
6504                    Kind::MIDI => {
6505                        let from_hw_in_device = Self::midi_hw_in_device(from_track);
6506                        let to_hw_out_device = Self::midi_hw_out_device(to_track);
6507                        let from_is_invalid_hw = Self::midi_hw_out_device(from_track).is_some();
6508                        let to_is_invalid_hw = Self::midi_hw_in_device(to_track).is_some();
6509
6510                        if from_is_invalid_hw || to_is_invalid_hw {
6511                            self.notify_clients(Err(
6512                                "Invalid MIDI hardware connection direction".to_string()
6513                            ))
6514                            .await;
6515                            return;
6516                        }
6517
6518                        if from_hw_in_device.is_none()
6519                            && to_hw_out_device.is_none()
6520                            && self.check_if_leads_to_kind(Kind::MIDI, to_track, from_track)
6521                        {
6522                            self.notify_clients(Err("Circular routing is not allowed!".into()))
6523                                .await;
6524                            return;
6525                        }
6526
6527                        let state = self.state.lock();
6528                        let from_track_handle = state.tracks.get(from_track);
6529                        let to_track_handle = state.tracks.get(to_track);
6530
6531                        if let (Some(from_device), Some(to_device)) =
6532                            (from_hw_in_device, to_hw_out_device)
6533                        {
6534                            let route = MidiHwThruRoute {
6535                                from_device: from_device.to_string(),
6536                                to_device: to_device.to_string(),
6537                            };
6538                            if !self.midi_hw_thru_routes.iter().any(|r| r == &route) {
6539                                self.midi_hw_thru_routes.push(route);
6540                            }
6541                        } else if let Some(device) = from_hw_in_device {
6542                            if let Some(t_t) = to_track_handle {
6543                                if t_t.lock().midi.ins.get(to_port).is_none() {
6544                                    self.notify_clients(Err(format!(
6545                                        "MIDI input port {} not found on track '{}'",
6546                                        to_port, to_track
6547                                    )))
6548                                    .await;
6549                                    return;
6550                                }
6551                                let route = MidiHwInRoute {
6552                                    device: device.to_string(),
6553                                    to_track: to_track.to_string(),
6554                                    to_port,
6555                                };
6556                                if !self.midi_hw_in_routes.iter().any(|r| r == &route) {
6557                                    self.midi_hw_in_routes.push(route);
6558                                }
6559                            } else {
6560                                self.notify_clients(Err(format!(
6561                                    "MIDI destination track not found: {}",
6562                                    to_track
6563                                )))
6564                                .await;
6565                                return;
6566                            }
6567                        } else if let Some(device) = to_hw_out_device {
6568                            if let Some(f_t) = from_track_handle {
6569                                if f_t.lock().midi.outs.get(from_port).is_none() {
6570                                    self.notify_clients(Err(format!(
6571                                        "MIDI output port {} not found on track '{}'",
6572                                        from_port, from_track
6573                                    )))
6574                                    .await;
6575                                    return;
6576                                }
6577                                let route = MidiHwOutRoute {
6578                                    from_track: from_track.to_string(),
6579                                    from_port,
6580                                    device: device.to_string(),
6581                                };
6582                                if !self.midi_hw_out_routes.iter().any(|r| r == &route) {
6583                                    self.midi_hw_out_routes.push(route);
6584                                }
6585                            } else {
6586                                self.notify_clients(Err(format!(
6587                                    "MIDI source track not found: {}",
6588                                    from_track
6589                                )))
6590                                .await;
6591                                return;
6592                            }
6593                        } else {
6594                            match (from_track_handle, to_track_handle) {
6595                                (Some(f_t), Some(t_t)) => {
6596                                    let to_in_res = t_t.lock().midi.ins.get(to_port).cloned();
6597                                    if let Some(to_in) = to_in_res {
6598                                        let from_track = f_t.lock();
6599                                        if let Err(e) =
6600                                            from_track.midi.connect_out(from_port, to_in)
6601                                        {
6602                                            self.notify_clients(Err(e)).await;
6603                                            return;
6604                                        }
6605                                        from_track.invalidate_midi_route_cache();
6606                                    } else {
6607                                        self.notify_clients(Err(format!(
6608                                            "MIDI input port {} not found on track '{}'",
6609                                            to_port, to_track
6610                                        )))
6611                                        .await;
6612                                        return;
6613                                    }
6614                                }
6615                                _ => {
6616                                    self.notify_clients(Err(format!(
6617                                        "MIDI tracks not found: {} or {}",
6618                                        from_track, to_track
6619                                    )))
6620                                    .await;
6621                                    return;
6622                                }
6623                            }
6624                        }
6625                    }
6626                };
6627            }
6628            Action::Disconnect {
6629                ref from_track,
6630                from_port,
6631                ref to_track,
6632                to_port,
6633                kind,
6634            } => {
6635                if kind == Kind::Audio {
6636                    if let Err(e) = self.disconnect_audio_route_and_notify(a.clone()).await {
6637                        self.notify_clients(Err(e)).await;
6638                    }
6639                } else if kind == Kind::MIDI {
6640                    let from_hw_in_device = Self::midi_hw_in_device(from_track);
6641                    let to_hw_out_device = Self::midi_hw_out_device(to_track);
6642
6643                    if let (Some(from_device), Some(to_device)) =
6644                        (from_hw_in_device, to_hw_out_device)
6645                    {
6646                        let before = self.midi_hw_thru_routes.len();
6647                        self.midi_hw_thru_routes.retain(|r| {
6648                            !(r.from_device == from_device && r.to_device == to_device)
6649                        });
6650                        if self.midi_hw_thru_routes.len() < before {
6651                            self.notify_clients(Ok(a.clone())).await;
6652                        } else {
6653                            self.notify_clients(Err(format!(
6654                                "Disconnect failed: MIDI route not found ({} -> {})",
6655                                from_track, to_track
6656                            )))
6657                            .await;
6658                        }
6659                        return;
6660                    }
6661
6662                    if let Some(device) = from_hw_in_device {
6663                        let before = self.midi_hw_in_routes.len();
6664                        self.midi_hw_in_routes.retain(|r| {
6665                            !(r.device == device && r.to_track == *to_track && r.to_port == to_port)
6666                        });
6667                        if self.midi_hw_in_routes.len() < before {
6668                            self.notify_clients(Ok(a.clone())).await;
6669                        } else {
6670                            self.notify_clients(Err(format!(
6671                                "Disconnect failed: MIDI route not found ({} -> {})",
6672                                from_track, to_track
6673                            )))
6674                            .await;
6675                        }
6676                        return;
6677                    }
6678
6679                    if let Some(device) = to_hw_out_device {
6680                        let before = self.midi_hw_out_routes.len();
6681                        self.midi_hw_out_routes.retain(|r| {
6682                            !(r.from_track == *from_track
6683                                && r.from_port == from_port
6684                                && r.device == device)
6685                        });
6686                        if self.midi_hw_out_routes.len() < before {
6687                            self.notify_clients(Ok(a.clone())).await;
6688                        } else {
6689                            self.notify_clients(Err(format!(
6690                                "Disconnect failed: MIDI route not found ({} -> {})",
6691                                from_track, to_track
6692                            )))
6693                            .await;
6694                        }
6695                        return;
6696                    }
6697
6698                    let state = self.state.lock();
6699                    if let (Some(f_t), Some(t_t)) =
6700                        (state.tracks.get(from_track), state.tracks.get(to_track))
6701                        && let Some(to_in) = t_t.lock().midi.ins.get(to_port).cloned()
6702                    {
6703                        let from_track = f_t.lock();
6704                        if let Err(e) = from_track.midi.disconnect_out(from_port, &to_in) {
6705                            self.notify_clients(Err(e)).await;
6706                        } else {
6707                            from_track.invalidate_midi_route_cache();
6708                            self.notify_clients(Ok(a.clone())).await;
6709                        }
6710                    } else {
6711                        self.notify_clients(Err(format!(
6712                            "Disconnect failed: MIDI ports not found ({} -> {})",
6713                            from_track, to_track
6714                        )))
6715                        .await;
6716                    }
6717                }
6718            }
6719
6720            Action::OpenAudioDevice {
6721                ref device,
6722                ref input_device,
6723                sample_rate_hz,
6724                bits,
6725                exclusive,
6726                period_frames,
6727                realtime_frames,
6728                low_watermark_frames,
6729                nperiods,
6730                sync_mode,
6731            } => {
6732                #[cfg(unix)]
6733                {
6734                    let request = AudioOpenRequest {
6735                        device,
6736                        input_device: input_device.as_deref(),
6737                        sample_rate_hz,
6738                        bits,
6739                        exclusive,
6740                        period_frames,
6741                        realtime_frames,
6742                        low_watermark_frames,
6743                        nperiods,
6744                        sync_mode,
6745                    };
6746                    if self.maybe_open_jack_runtime(request).await.is_some() {
6747                        return;
6748                    }
6749                }
6750                let hw_opts =
6751                    Self::build_hw_options(exclusive, realtime_frames, nperiods, sync_mode);
6752                self.hybrid_playback_frames = period_frames.max(1);
6753                self.hybrid_realtime_frames = realtime_frames.max(1);
6754                self.hybrid_low_watermark_frames = low_watermark_frames.max(1);
6755                let open_result = self
6756                    .open_non_jack_audio_device(
6757                        device,
6758                        input_device.as_deref(),
6759                        sample_rate_hz,
6760                        bits,
6761                        hw_opts,
6762                    )
6763                    .await;
6764                match open_result {
6765                    Ok(()) => {}
6766                    Err(e) => {
6767                        self.notify_clients(Err(e)).await;
6768                        return;
6769                    }
6770                }
6771                self.finalize_open_audio_device().await;
6772            }
6773            Action::JackAddAudioInputPort => {
6774                #[cfg(unix)]
6775                {
6776                    if let Some(jack) = self.jack_runtime.clone() {
6777                        let (input_channels, output_channels, rate) = {
6778                            let jack = jack.lock();
6779                            if let Err(e) = jack.add_audio_input_port() {
6780                                self.notify_clients(Err(e)).await;
6781                                return;
6782                            }
6783                            (
6784                                jack.input_channels(),
6785                                jack.output_channels(),
6786                                jack.sample_rate,
6787                            )
6788                        };
6789                        self.publish_hw_infos(input_channels, output_channels, rate)
6790                            .await;
6791                        self.notify_clients(Ok(a.clone())).await;
6792                    } else {
6793                        self.notify_clients(Err(
6794                            "JACK runtime is not active; open the JACK backend first".to_string(),
6795                        ))
6796                        .await;
6797                    }
6798                }
6799                #[cfg(not(unix))]
6800                {
6801                    self.notify_clients(Err(
6802                        "JACK backend is not available on this platform build".to_string(),
6803                    ))
6804                    .await;
6805                }
6806            }
6807            Action::JackRemoveAudioInputPort(_removed_port) => {
6808                #[cfg(unix)]
6809                {
6810                    let removed_port = _removed_port;
6811                    if let Some(jack) = self.jack_runtime.clone() {
6812                        let (removed_port, removed_io) = {
6813                            let jack = jack.lock();
6814                            let removed_port = Some(removed_port);
6815                            let removed_io =
6816                                removed_port.and_then(|port| jack.input_audio_port(port));
6817                            match (removed_port, removed_io) {
6818                                (Some(port), Some(io)) => (port, io),
6819                                _ => {
6820                                    self.notify_clients(Err(
6821                                        "JACK audio input port index is out of range".to_string(),
6822                                    ))
6823                                    .await;
6824                                    return;
6825                                }
6826                            }
6827                        };
6828                        let reindex_notifications =
6829                            self.reindex_notifications_for_removed_hw_input(removed_port);
6830                        for disconnect in
6831                            self.disconnect_actions_for_removed_hw_input(removed_port, &removed_io)
6832                        {
6833                            if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await
6834                            {
6835                                self.notify_clients(Err(e)).await;
6836                                return;
6837                            }
6838                        }
6839                        let (input_channels, output_channels, rate) = {
6840                            let jack = jack.lock();
6841                            if let Err(e) = jack.remove_audio_input_port(removed_port) {
6842                                self.notify_clients(Err(e)).await;
6843                                return;
6844                            }
6845                            (
6846                                jack.input_channels(),
6847                                jack.output_channels(),
6848                                jack.sample_rate,
6849                            )
6850                        };
6851                        for action in reindex_notifications {
6852                            self.notify_clients(Ok(action)).await;
6853                        }
6854                        self.publish_hw_infos(input_channels, output_channels, rate)
6855                            .await;
6856                        self.notify_clients(Ok(a.clone())).await;
6857                    } else {
6858                        self.notify_clients(Err(
6859                            "JACK runtime is not active; open the JACK backend first".to_string(),
6860                        ))
6861                        .await;
6862                    }
6863                }
6864                #[cfg(not(unix))]
6865                {
6866                    self.notify_clients(Err(
6867                        "JACK backend is not available on this platform build".to_string(),
6868                    ))
6869                    .await;
6870                }
6871            }
6872            Action::JackAddAudioOutputPort => {
6873                #[cfg(unix)]
6874                {
6875                    if let Some(jack) = self.jack_runtime.clone() {
6876                        let (input_channels, output_channels, rate) = {
6877                            let jack = jack.lock();
6878                            if let Err(e) = jack.add_audio_output_port() {
6879                                self.notify_clients(Err(e)).await;
6880                                return;
6881                            }
6882                            (
6883                                jack.input_channels(),
6884                                jack.output_channels(),
6885                                jack.sample_rate,
6886                            )
6887                        };
6888                        self.publish_hw_infos(input_channels, output_channels, rate)
6889                            .await;
6890                        self.notify_clients(Ok(a.clone())).await;
6891                    } else {
6892                        self.notify_clients(Err(
6893                            "JACK runtime is not active; open the JACK backend first".to_string(),
6894                        ))
6895                        .await;
6896                    }
6897                }
6898                #[cfg(not(unix))]
6899                {
6900                    self.notify_clients(Err(
6901                        "JACK backend is not available on this platform build".to_string(),
6902                    ))
6903                    .await;
6904                }
6905            }
6906            Action::JackRemoveAudioOutputPort(_removed_port) => {
6907                #[cfg(unix)]
6908                {
6909                    let removed_port = _removed_port;
6910                    if let Some(jack) = self.jack_runtime.clone() {
6911                        let (removed_port, removed_io) = {
6912                            let jack = jack.lock();
6913                            let removed_port = Some(removed_port);
6914                            let removed_io =
6915                                removed_port.and_then(|port| jack.output_audio_port(port));
6916                            match (removed_port, removed_io) {
6917                                (Some(port), Some(io)) => (port, io),
6918                                _ => {
6919                                    self.notify_clients(Err(
6920                                        "JACK audio output port index is out of range".to_string(),
6921                                    ))
6922                                    .await;
6923                                    return;
6924                                }
6925                            }
6926                        };
6927                        let reindex_notifications =
6928                            self.reindex_notifications_for_removed_hw_output(removed_port);
6929                        for disconnect in
6930                            self.disconnect_actions_for_removed_hw_output(removed_port, &removed_io)
6931                        {
6932                            if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await
6933                            {
6934                                self.notify_clients(Err(e)).await;
6935                                return;
6936                            }
6937                        }
6938                        let (input_channels, output_channels, rate) = {
6939                            let jack = jack.lock();
6940                            if let Err(e) = jack.remove_audio_output_port(removed_port) {
6941                                self.notify_clients(Err(e)).await;
6942                                return;
6943                            }
6944                            (
6945                                jack.input_channels(),
6946                                jack.output_channels(),
6947                                jack.sample_rate,
6948                            )
6949                        };
6950                        for action in reindex_notifications {
6951                            self.notify_clients(Ok(action)).await;
6952                        }
6953                        self.publish_hw_infos(input_channels, output_channels, rate)
6954                            .await;
6955                        self.notify_clients(Ok(a.clone())).await;
6956                    } else {
6957                        self.notify_clients(Err(
6958                            "JACK runtime is not active; open the JACK backend first".to_string(),
6959                        ))
6960                        .await;
6961                    }
6962                }
6963                #[cfg(not(unix))]
6964                {
6965                    self.notify_clients(Err(
6966                        "JACK backend is not available on this platform build".to_string(),
6967                    ))
6968                    .await;
6969                }
6970            }
6971            Action::OpenMidiInputDevice(ref device) => {
6972                let midi_hub = self.midi_hub.lock();
6973                if let Err(e) = midi_hub.open_input(device) {
6974                    self.notify_clients(Err(e)).await;
6975                    return;
6976                }
6977            }
6978            Action::OpenMidiOutputDevice(ref device) => {
6979                let midi_hub = self.midi_hub.lock();
6980                if let Err(e) = midi_hub.open_output(device) {
6981                    self.notify_clients(Err(e)).await;
6982                    return;
6983                }
6984            }
6985            Action::RequestSessionDiagnostics => {
6986                let (
6987                    track_count,
6988                    frozen_track_count,
6989                    audio_clip_count,
6990                    midi_clip_count,
6991                    lv2_instance_count,
6992                    vst3_instance_count,
6993                    clap_instance_count,
6994                ) = {
6995                    let tracks = &self.state.lock().tracks;
6996                    let mut track_count = 0usize;
6997                    let mut frozen_track_count = 0usize;
6998                    let mut audio_clip_count = 0usize;
6999                    let mut midi_clip_count = 0usize;
7000                    #[cfg(all(unix, not(target_os = "macos")))]
7001                    let mut lv2_instance_count = 0usize;
7002                    #[cfg(not(all(unix, not(target_os = "macos"))))]
7003                    let lv2_instance_count = 0usize;
7004                    let mut vst3_instance_count = 0usize;
7005                    let mut clap_instance_count = 0usize;
7006                    for track in tracks.values() {
7007                        let t = track.lock();
7008                        track_count += 1;
7009                        if t.frozen {
7010                            frozen_track_count += 1;
7011                        }
7012                        audio_clip_count += t.audio.clips.len();
7013                        midi_clip_count += t.midi.clips.len();
7014                        #[cfg(all(unix, not(target_os = "macos")))]
7015                        {
7016                            lv2_instance_count += t.lv2_plugins.len();
7017                        }
7018                        vst3_instance_count += t.vst3_plugins.len();
7019                        clap_instance_count += t.clap_plugins.len();
7020                    }
7021                    (
7022                        track_count,
7023                        frozen_track_count,
7024                        audio_clip_count,
7025                        midi_clip_count,
7026                        lv2_instance_count,
7027                        vst3_instance_count,
7028                        clap_instance_count,
7029                    )
7030                };
7031                #[cfg(not(all(unix, not(target_os = "macos"))))]
7032                let _lv2_instance_count = lv2_instance_count;
7033                let pending_hw_midi_events = self.pending_hw_midi_events.len()
7034                    + self
7035                        .pending_hw_midi_events_by_device
7036                        .values()
7037                        .map(std::vec::Vec::len)
7038                        .sum::<usize>();
7039                let sample_rate_hz = if let Some(hw) = &self.hw_driver {
7040                    hw.lock().sample_rate() as usize
7041                } else {
7042                    #[cfg(unix)]
7043                    {
7044                        self.jack_runtime
7045                            .as_ref()
7046                            .map(|j| j.lock().sample_rate)
7047                            .unwrap_or(0)
7048                    }
7049                    #[cfg(not(unix))]
7050                    0
7051                };
7052                let cycle_samples = self.current_cycle_samples();
7053                tracing::info!(
7054                    "Hybrid diagnostics: refill_budget_per_pass={}, refill_budget_throttle_count={}, realtime_fallback_dispatch_count={}, realtime_ready={}, refill_ready={}",
7055                    self.refill_budget_per_pass,
7056                    self.refill_budget_throttle_count,
7057                    self.realtime_fallback_dispatch_count,
7058                    self.ready_realtime_workers.len(),
7059                    self.ready_refill_workers.len()
7060                );
7061                self.notify_clients(Ok(Action::SessionDiagnosticsReport {
7062                    track_count,
7063                    frozen_track_count,
7064                    audio_clip_count,
7065                    midi_clip_count,
7066                    #[cfg(all(unix, not(target_os = "macos")))]
7067                    lv2_instance_count,
7068                    vst3_instance_count,
7069                    clap_instance_count,
7070                    pending_requests: self.pending_requests.len(),
7071                    workers_total: self.workers.len(),
7072                    workers_ready: self.ready_realtime_workers.len()
7073                        + self.ready_refill_workers.len(),
7074                    pending_hw_midi_events,
7075                    playing: self.playing,
7076                    transport_sample: self.transport_sample,
7077                    tempo_bpm: self.tempo_bpm,
7078                    sample_rate_hz,
7079                    cycle_samples,
7080                }))
7081                .await;
7082            }
7083            Action::RequestMidiLearnMappingsReport => {
7084                let mut lines = Vec::<String>::new();
7085                let fmt_binding = |b: &crate::message::MidiLearnBinding| {
7086                    let device = b.device.as_deref().unwrap_or("*");
7087                    format!("{device} CH{} CC{}", b.channel + 1, b.cc)
7088                };
7089                if let Some(b) = self.global_midi_learn_play_pause.as_ref() {
7090                    lines.push(format!("Global PlayPause: {}", fmt_binding(b)));
7091                }
7092                if let Some(b) = self.global_midi_learn_stop.as_ref() {
7093                    lines.push(format!("Global Stop: {}", fmt_binding(b)));
7094                }
7095                if let Some(b) = self.global_midi_learn_record_toggle.as_ref() {
7096                    lines.push(format!("Global RecordToggle: {}", fmt_binding(b)));
7097                }
7098                for (track_name, track) in self.state.lock().tracks.iter() {
7099                    let t = track.lock();
7100                    if let Some(b) = t.midi_learn_volume.as_ref() {
7101                        lines.push(format!("{} Volume: {}", track_name, fmt_binding(b)));
7102                    }
7103                    if let Some(b) = t.midi_learn_balance.as_ref() {
7104                        lines.push(format!("{} Balance: {}", track_name, fmt_binding(b)));
7105                    }
7106                    if let Some(b) = t.midi_learn_mute.as_ref() {
7107                        lines.push(format!("{} Mute: {}", track_name, fmt_binding(b)));
7108                    }
7109                    if let Some(b) = t.midi_learn_solo.as_ref() {
7110                        lines.push(format!("{} Solo: {}", track_name, fmt_binding(b)));
7111                    }
7112                    if let Some(b) = t.midi_learn_arm.as_ref() {
7113                        lines.push(format!("{} Arm: {}", track_name, fmt_binding(b)));
7114                    }
7115                    if let Some(b) = t.midi_learn_input_monitor.as_ref() {
7116                        lines.push(format!("{} InputMonitor: {}", track_name, fmt_binding(b)));
7117                    }
7118                    if let Some(b) = t.midi_learn_disk_monitor.as_ref() {
7119                        lines.push(format!("{} DiskMonitor: {}", track_name, fmt_binding(b)));
7120                    }
7121                }
7122                if lines.is_empty() {
7123                    lines.push("No MIDI learn mappings configured".to_string());
7124                }
7125                self.notify_clients(Ok(Action::MidiLearnMappingsReport { lines }))
7126                    .await;
7127            }
7128            Action::ClearAllMidiLearnBindings => {
7129                self.pending_midi_learn = None;
7130                self.pending_global_midi_learn = None;
7131                self.global_midi_learn_play_pause = None;
7132                self.global_midi_learn_stop = None;
7133                self.global_midi_learn_record_toggle = None;
7134                self.midi_cc_gate.clear();
7135                for track in self.state.lock().tracks.values() {
7136                    let t = track.lock();
7137                    t.midi_learn_volume = None;
7138                    t.midi_learn_balance = None;
7139                    t.midi_learn_mute = None;
7140                    t.midi_learn_solo = None;
7141                    t.midi_learn_arm = None;
7142                    t.midi_learn_input_monitor = None;
7143                    t.midi_learn_disk_monitor = None;
7144                }
7145            }
7146            #[cfg(all(unix, not(target_os = "macos")))]
7147            Action::TrackLv2PluginControls { .. } => {}
7148            #[cfg(all(unix, not(target_os = "macos")))]
7149            Action::ClipLv2PluginControls { .. } => {}
7150            #[cfg(all(unix, not(target_os = "macos")))]
7151            Action::TrackLv2Midnam { .. } => {}
7152            Action::TrackClapNoteNames { .. } => {}
7153            Action::SessionDiagnosticsReport { .. } => {}
7154            Action::MidiLearnMappingsReport { .. } => {}
7155            Action::HWInfo { .. } => {}
7156            Action::HistoryState { .. } => {}
7157            Action::Undo => {}
7158            Action::Redo => {}
7159            Action::ApplyGroupedActions(_) => {}
7160            _ => {}
7161        }
7162
7163        if let Some(inverse) = inverse_actions {
7164            if let Some(group) = self.history_group.as_mut() {
7165                group.forward_actions.push(action_to_process.clone());
7166                group.inverse_actions.splice(0..0, inverse);
7167            } else {
7168                self.history.record(UndoEntry {
7169                    forward_actions: vec![action_to_process.clone()],
7170                    inverse_actions: inverse,
7171                });
7172            }
7173        }
7174
7175        self.notify_clients(Ok(action_to_process)).await;
7176    }
7177
7178    pub async fn work(&mut self) {
7179        while let Some(message) = self.rx.recv().await {
7180            match message {
7181                Message::Ready(id) => self.push_ready_worker(id),
7182                Message::Finished {
7183                    worker_id,
7184                    track_name,
7185                    output_linear,
7186                    process_epoch,
7187                    parameter_updates,
7188                } => {
7189                    self.push_ready_worker(worker_id);
7190                    self.track_processing_started_at.remove(&track_name);
7191                    if process_epoch != self.track_process_epoch {
7192                        if let Some(track) = self.state.lock().tracks.get(&track_name).cloned() {
7193                            let t = track.lock();
7194                            t.audio.finished = false;
7195                            t.audio.processing = false;
7196                        }
7197                        continue;
7198                    }
7199                    self.track_meter_linear_by_track
7200                        .insert(track_name, output_linear);
7201                    for action in parameter_updates {
7202                        self.notify_clients(Ok(action)).await;
7203                    }
7204                    self.force_stalled_track_completions();
7205                    let all_finished = self.send_tracks().await;
7206                    if all_finished {
7207                        self.on_all_tracks_finished().await;
7208                    }
7209                }
7210                Message::Channel(s) => {
7211                    self.clients.push(s);
7212                }
7213
7214                Message::Request(a) => match a {
7215                    Action::TrackOfflineBounceCancel { track_name } => {
7216                        if let Some(job) = self.offline_bounce_jobs.get(&track_name) {
7217                            job.cancel.store(true, Ordering::Relaxed);
7218                        }
7219                    }
7220                    Action::TrackOfflineBounceCancelAll => {
7221                        for job in self.offline_bounce_jobs.values() {
7222                            job.cancel.store(true, Ordering::Relaxed);
7223                        }
7224                    }
7225                    _ if !self.offline_bounce_jobs.is_empty() => {
7226                        self.pending_requests.push_back(a);
7227                    }
7228                    Action::OpenAudioDevice { .. }
7229                    | Action::OpenMidiInputDevice(_)
7230                    | Action::OpenMidiOutputDevice(_)
7231                    | Action::RequestMeterSnapshot
7232                    | Action::Quit
7233                    | Action::Play
7234                    | Action::Pause
7235                    | Action::Stop
7236                    | Action::TransportPosition(_)
7237                    | Action::JumpToEnd
7238                    | Action::SetLoopEnabled(_)
7239                    | Action::SetLoopRange(_)
7240                    | Action::SetPunchEnabled(_)
7241                    | Action::SetPunchRange(_)
7242                    | Action::SetMetronomeEnabled(_)
7243                    | Action::SetTempo(_)
7244                    | Action::SetTimeSignature { .. }
7245                    | Action::SetOscEnabled(_)
7246                    | Action::SetClipPlaybackEnabled(_)
7247                    | Action::SetRecordEnabled(_)
7248                    | Action::SetSessionPath(_)
7249                    | Action::ClearHistory
7250                    | Action::BeginSessionRestore
7251                    | Action::PianoKey { .. }
7252                    | Action::ModifyMidiNotes { .. }
7253                    | Action::ModifyMidiControllers { .. }
7254                    | Action::DeleteMidiControllers { .. }
7255                    | Action::InsertMidiControllers { .. }
7256                    | Action::DeleteMidiNotes { .. }
7257                    | Action::InsertMidiNotes { .. }
7258                    | Action::SetMidiSysExEvents { .. } => {
7259                        self.handle_request(a).await;
7260                    }
7261                    #[cfg(all(unix, not(target_os = "macos")))]
7262                    Action::ListLv2Plugins => {
7263                        self.handle_request(a).await;
7264                    }
7265                    Action::ListVst3Plugins => {
7266                        self.handle_request(a).await;
7267                    }
7268                    Action::ListClapPlugins => {
7269                        self.handle_request(a).await;
7270                    }
7271                    Action::ListClapPluginsWithCapabilities => {
7272                        self.handle_request(a).await;
7273                    }
7274                    _ => {
7275                        self.pending_requests.push_back(a);
7276                        if self.can_schedule_hw_cycle() {
7277                            self.request_hw_cycle().await;
7278                        } else {
7279                            while let Some(next) = self.pending_requests.pop_front() {
7280                                self.handle_request(next).await;
7281                            }
7282                        }
7283                    }
7284                },
7285                Message::OfflineBounceFinished { result } => {
7286                    if let Ok(Action::TrackOfflineBounce { track_name, .. }) = &result {
7287                        self.offline_bounce_jobs.remove(track_name);
7288                    }
7289                    self.notify_clients(result).await;
7290                    if self.offline_bounce_jobs.is_empty() {
7291                        while let Some(next) = self.pending_requests.pop_front() {
7292                            self.handle_request(next).await;
7293                        }
7294                    }
7295                }
7296                Message::HWFinished => {
7297                    if !self.awaiting_hwfinished {
7298                        continue;
7299                    }
7300                    self.handling_hwfinished = true;
7301                    self.awaiting_hwfinished = false;
7302                    #[cfg(unix)]
7303                    {
7304                        if let Some(jack) = &self.jack_runtime {
7305                            if !self.pending_hw_midi_out_events.is_empty() {
7306                                let out_events =
7307                                    std::mem::take(&mut self.pending_hw_midi_out_events);
7308                                jack.lock().write_events(&out_events);
7309                            }
7310                            let mut in_events = vec![];
7311                            jack.lock().read_events_into(&mut in_events);
7312                            if !in_events.is_empty() {
7313                                self.pending_hw_midi_events.extend(in_events);
7314                            }
7315                        }
7316                    }
7317                    #[cfg(unix)]
7318                    if self.jack_runtime.is_some() {
7319                        self.sync_from_jack_transport().await;
7320                    }
7321                    while let Some(a) = self.pending_requests.pop_front() {
7322                        self.handle_request(a).await;
7323                    }
7324                    self.apply_mute_solo_policy();
7325                    self.append_recorded_cycle();
7326                    self.flush_completed_recordings().await;
7327                    let hw_in_routes = self.midi_hw_in_routes.clone();
7328                    let pending_hw_in_by_device = self.pending_hw_midi_events_by_device.clone();
7329                    let mut reconfigured_tracks = Vec::new();
7330                    for (track_name, track) in self.state.lock().tracks.iter() {
7331                        let track_lock = track.lock();
7332                        if self.jack_runtime_is_some() {
7333                            if !self.pending_hw_midi_events.is_empty() {
7334                                track_lock.push_hw_midi_events(&self.pending_hw_midi_events);
7335                            }
7336                        } else {
7337                            for route in hw_in_routes.iter().filter(|r| &r.to_track == track_name) {
7338                                if let Some(events) = pending_hw_in_by_device.get(&route.device) {
7339                                    track_lock.push_hw_midi_events_to_port(route.to_port, events);
7340                                }
7341                            }
7342                        }
7343                        if track_lock.setup() {
7344                            reconfigured_tracks.push(track_name.clone());
7345                        }
7346                    }
7347                    self.publish_track_meters().await;
7348                    for track_name in reconfigured_tracks {
7349                        let track = self.state.lock().tracks.get(&track_name).cloned();
7350                        if let Some(track) = track {
7351                            let (plugins, connections) = {
7352                                let track_lock = track.lock();
7353                                (
7354                                    track_lock.plugin_graph_plugins(),
7355                                    track_lock.plugin_graph_connections(),
7356                                )
7357                            };
7358                            self.notify_clients(Ok(Action::TrackPluginGraph {
7359                                track_name: track_name.clone(),
7360                                plugins,
7361                                connections,
7362                            }))
7363                            .await;
7364                        }
7365                    }
7366                    self.pending_hw_midi_events.clear();
7367                    self.pending_hw_midi_events_by_device.clear();
7368                    if self.playing {
7369                        if self.transport_panic_flush_pending {
7370                            self.transport_panic_flush_pending = false;
7371                        } else if self.transport_restart_pending {
7372                            self.transport_restart_pending = false;
7373                        } else {
7374                            let next = self
7375                                .transport_sample
7376                                .saturating_add(self.current_cycle_samples());
7377                            let normalized = self.normalize_transport_sample(next);
7378                            let wrapped = normalized != next;
7379                            self.transport_sample = normalized;
7380                            if wrapped {
7381                                self.notify_clients(Ok(Action::TransportPosition(
7382                                    self.transport_sample,
7383                                )))
7384                                .await;
7385                            }
7386                        }
7387                    }
7388                    if self.send_tracks().await && self.hw_worker.is_some() {
7389                        self.request_hw_cycle().await;
7390                    }
7391                    #[cfg(unix)]
7392                    {
7393                        if self.jack_runtime.is_some() {
7394                            self.awaiting_hwfinished = true;
7395                        }
7396                    }
7397                    self.handling_hwfinished = false;
7398                }
7399                Message::HWMidiEvents(events) => {
7400                    for hw_event in events {
7401                        let thru_targets: Vec<String> = self
7402                            .midi_hw_thru_routes
7403                            .iter()
7404                            .filter(|route| route.from_device == hw_event.device)
7405                            .map(|route| route.to_device.clone())
7406                            .collect();
7407                        for device in thru_targets {
7408                            self.pending_hw_midi_out_events_by_device.push(HwMidiEvent {
7409                                device,
7410                                event: hw_event.event.clone(),
7411                            });
7412                        }
7413                        if hw_event.event.data.len() >= 3 {
7414                            let status = hw_event.event.data[0];
7415                            if status & 0xF0 == 0xB0 {
7416                                let channel = status & 0x0F;
7417                                let cc = hw_event.event.data[1];
7418                                let value = hw_event.event.data[2];
7419                                self.handle_incoming_hw_cc(&hw_event.device, channel, cc, value)
7420                                    .await;
7421                            }
7422                        }
7423                        self.pending_hw_midi_events_by_device
7424                            .entry(hw_event.device)
7425                            .or_default()
7426                            .push(hw_event.event);
7427                    }
7428                }
7429                _ => {}
7430            }
7431        }
7432    }
7433
7434    fn collect_hw_midi_output_events(&self) -> Vec<MidiEvent> {
7435        let mut events = vec![];
7436        for track in self.state.lock().tracks.values() {
7437            events.extend(
7438                track
7439                    .lock()
7440                    .take_hw_midi_out_events()
7441                    .into_iter()
7442                    .map(|evt| evt.event),
7443            );
7444        }
7445        events.sort_by_key(|a| a.frame);
7446        events
7447    }
7448
7449    fn collect_hw_midi_output_events_by_device(&mut self) -> Vec<HwMidiEvent> {
7450        let mut events = Vec::<HwMidiEvent>::new();
7451        let routes = self.midi_hw_out_routes.clone();
7452        let mut events_by_track = HashMap::<String, Vec<crate::track::HwMidiOutEvent>>::new();
7453        {
7454            let state = self.state.lock();
7455            for route in &routes {
7456                if events_by_track.contains_key(&route.from_track) {
7457                    continue;
7458                }
7459                let Some(track) = state.tracks.get(&route.from_track) else {
7460                    continue;
7461                };
7462                events_by_track.insert(
7463                    route.from_track.clone(),
7464                    track.lock().take_hw_midi_out_events(),
7465                );
7466            }
7467        }
7468
7469        for route in routes {
7470            let Some(track_events) = events_by_track.get(&route.from_track) else {
7471                continue;
7472            };
7473            for hw_event in track_events
7474                .iter()
7475                .filter(|evt| evt.port == route.from_port)
7476            {
7477                self.update_active_hw_notes_for_track(
7478                    &route.from_track,
7479                    &route.device,
7480                    &hw_event.event.data,
7481                );
7482                events.push(HwMidiEvent {
7483                    device: route.device.clone(),
7484                    event: hw_event.event.clone(),
7485                });
7486            }
7487        }
7488        events.sort_by(|a, b| {
7489            a.event
7490                .frame
7491                .cmp(&b.event.frame)
7492                .then_with(|| a.device.cmp(&b.device))
7493        });
7494        events
7495    }
7496}
7497
7498#[cfg(test)]
7499mod tests {
7500    use super::*;
7501    use crate::mutex::UnsafeMutex;
7502    use tokio::sync::mpsc::channel;
7503    use tokio::time::{Duration as TokioDuration, timeout};
7504
7505    #[test]
7506    #[cfg(unix)]
7507    fn jack_transport_sync_decision_starts_and_syncs_position_on_external_play() {
7508        let decision = Engine::jack_transport_sync_decision(false, 128, true, 256, 64);
7509
7510        assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Start));
7511        assert_eq!(decision.position_sync, Some(256));
7512    }
7513
7514    #[test]
7515    #[cfg(unix)]
7516    fn jack_transport_sync_decision_stops_and_syncs_position_on_external_stop() {
7517        let decision = Engine::jack_transport_sync_decision(true, 512, false, 96, 64);
7518
7519        assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Stop));
7520        assert_eq!(decision.position_sync, Some(96));
7521    }
7522
7523    #[test]
7524    #[cfg(unix)]
7525    fn jack_transport_sync_decision_ignores_small_rolling_drift() {
7526        let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1040, 64);
7527
7528        assert_eq!(decision.play_sync, None);
7529        assert_eq!(decision.position_sync, None);
7530    }
7531
7532    #[test]
7533    #[cfg(unix)]
7534    fn jack_transport_sync_decision_syncs_large_rolling_jump() {
7535        let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1200, 64);
7536
7537        assert_eq!(decision.play_sync, None);
7538        assert_eq!(decision.position_sync, Some(1200));
7539    }
7540
7541    #[test]
7542    #[cfg(unix)]
7543    fn jack_transport_sync_decision_syncs_locate_while_stopped() {
7544        let decision = Engine::jack_transport_sync_decision(false, 400, false, 900, 64);
7545
7546        assert_eq!(decision.play_sync, None);
7547        assert_eq!(decision.position_sync, Some(900));
7548    }
7549
7550    fn make_engine_with_client() -> (Engine, tokio::sync::mpsc::Receiver<Message>) {
7551        let (engine_tx, engine_rx) = channel(16);
7552        let mut engine = Engine::new(engine_rx, engine_tx);
7553        let (client_tx, client_rx) = channel(16);
7554        engine.clients.push(client_tx);
7555        (engine, client_rx)
7556    }
7557
7558    fn insert_track(engine: &mut Engine, track: Track) {
7559        engine.state.lock().tracks.insert(
7560            track.name.clone(),
7561            Arc::new(UnsafeMutex::new(Box::new(track))),
7562        );
7563    }
7564
7565    fn osc_packet(address: &str) -> Vec<u8> {
7566        fn push_padded_osc_string(packet: &mut Vec<u8>, value: &str) {
7567            packet.extend_from_slice(value.as_bytes());
7568            packet.push(0);
7569            while !packet.len().is_multiple_of(4) {
7570                packet.push(0);
7571            }
7572        }
7573
7574        let mut packet = Vec::new();
7575        push_padded_osc_string(&mut packet, address);
7576        push_padded_osc_string(&mut packet, ",");
7577        packet
7578    }
7579
7580    #[tokio::test]
7581    async fn set_osc_enabled_starts_and_stops_server() {
7582        let (mut engine, _client_rx) = make_engine_with_client();
7583
7584        engine
7585            .set_osc_enabled_with(true, |tx| OscServer::start_on_addr(tx, "127.0.0.1:0"))
7586            .expect("start osc server on ephemeral port");
7587        assert!(engine.osc_server.is_some());
7588
7589        engine
7590            .set_osc_enabled_with(false, OscServer::start)
7591            .expect("stop osc server");
7592        assert!(engine.osc_server.is_none());
7593    }
7594
7595    #[tokio::test]
7596    async fn osc_server_forwards_transport_packets_to_engine_channel() {
7597        let (tx, mut rx) = channel(4);
7598        let mut server =
7599            OscServer::start_on_addr(tx, "127.0.0.1:0").expect("start osc test server");
7600        let socket = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind sender socket");
7601        let packet = osc_packet("/transport/play");
7602        socket
7603            .send_to(&packet, server.listen_addr())
7604            .expect("send osc packet");
7605
7606        let message = timeout(TokioDuration::from_secs(1), rx.recv())
7607            .await
7608            .expect("packet delivery timeout")
7609            .expect("osc message");
7610        match message {
7611            Message::Request(Action::Play) => {}
7612            other => panic!("unexpected osc message: {other:?}"),
7613        }
7614
7615        server.stop();
7616    }
7617
7618    #[tokio::test]
7619    async fn track_offline_bounce_rejects_zero_length_requests() {
7620        let (mut engine, mut client_rx) = make_engine_with_client();
7621        insert_track(
7622            &mut engine,
7623            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
7624        );
7625
7626        engine
7627            .handle_request(Action::TrackOfflineBounce {
7628                track_name: "track".to_string(),
7629                output_path: "/tmp/out.wav".to_string(),
7630                start_sample: 0,
7631                length_samples: 0,
7632                automation_lanes: vec![],
7633                apply_fader: false,
7634            })
7635            .await;
7636
7637        match client_rx.recv().await.expect("response") {
7638            Message::Response(Err(err)) => {
7639                assert!(err.contains("has no renderable content for offline bounce"));
7640            }
7641            other => panic!("unexpected message: {other:?}"),
7642        }
7643    }
7644
7645    #[tokio::test]
7646    async fn track_offline_bounce_rejects_when_same_track_is_active() {
7647        let (mut engine, mut client_rx) = make_engine_with_client();
7648        engine.offline_bounce_jobs.insert(
7649            "other".to_string(),
7650            OfflineBounceJob {
7651                cancel: Arc::new(AtomicBool::new(false)),
7652            },
7653        );
7654
7655        engine
7656            .handle_request(Action::TrackOfflineBounce {
7657                track_name: "other".to_string(),
7658                output_path: "/tmp/out.wav".to_string(),
7659                start_sample: 0,
7660                length_samples: 128,
7661                automation_lanes: vec![],
7662                apply_fader: false,
7663            })
7664            .await;
7665
7666        match client_rx.recv().await.expect("response") {
7667            Message::Response(Err(err)) => {
7668                assert!(err.contains("already in progress"));
7669            }
7670            other => panic!("unexpected message: {other:?}"),
7671        }
7672    }
7673
7674    #[tokio::test]
7675    async fn track_offline_bounce_allows_different_track_concurrently() {
7676        let (mut engine, _client_rx) = make_engine_with_client();
7677        insert_track(
7678            &mut engine,
7679            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
7680        );
7681        engine.offline_bounce_jobs.insert(
7682            "other".to_string(),
7683            OfflineBounceJob {
7684                cancel: Arc::new(AtomicBool::new(false)),
7685            },
7686        );
7687
7688        engine
7689            .handle_request(Action::TrackOfflineBounce {
7690                track_name: "track".to_string(),
7691                output_path: "/tmp/out.wav".to_string(),
7692                start_sample: 0,
7693                length_samples: 128,
7694                automation_lanes: vec![],
7695                apply_fader: false,
7696            })
7697            .await;
7698
7699        assert!(engine.offline_bounce_jobs.contains_key("other"));
7700        assert_eq!(engine.pending_requests.len(), 1);
7701    }
7702
7703    #[tokio::test]
7704    async fn reject_if_track_frozen_sends_error_and_blocks_operation() {
7705        let (mut engine, mut client_rx) = make_engine_with_client();
7706        let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
7707        track.set_frozen(true);
7708        insert_track(&mut engine, track);
7709
7710        let rejected = engine
7711            .reject_if_track_frozen("track", "arming/disarming")
7712            .await;
7713
7714        assert!(rejected);
7715        match client_rx.recv().await.expect("response") {
7716            Message::Response(Err(err)) => {
7717                assert_eq!(err, "Track 'track' is frozen; arming/disarming is blocked");
7718            }
7719            other => panic!("unexpected message: {other:?}"),
7720        }
7721    }
7722
7723    #[tokio::test]
7724    async fn undo_restores_original_clip_bounds_after_stretch_style_group() {
7725        let (mut engine, _client_rx) = make_engine_with_client();
7726        let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
7727        let mut clip = AudioClip::new("audio/original.wav".to_string(), 100, 220);
7728        clip.offset = 12;
7729        clip.fade_in_samples = 20;
7730        clip.fade_out_samples = 30;
7731        track.audio.clips.push(clip);
7732        insert_track(&mut engine, track);
7733
7734        engine.handle_request(Action::BeginHistoryGroup).await;
7735        engine
7736            .handle_request(Action::SetClipBounds {
7737                track_name: "track".to_string(),
7738                clip_index: 0,
7739                kind: Kind::Audio,
7740                start: 120,
7741                length: 180,
7742                offset: 0,
7743            })
7744            .await;
7745        engine
7746            .handle_request(Action::SetClipSourceName {
7747                track_name: "track".to_string(),
7748                clip_index: 0,
7749                kind: Kind::Audio,
7750                name: "audio/stretched.wav".to_string(),
7751            })
7752            .await;
7753        engine
7754            .handle_request(Action::SetClipFade {
7755                track_name: "track".to_string(),
7756                clip_index: 0,
7757                kind: Kind::Audio,
7758                fade_enabled: true,
7759                fade_in_samples: 12,
7760                fade_out_samples: 12,
7761            })
7762            .await;
7763        engine.handle_request(Action::EndHistoryGroup).await;
7764
7765        engine.handle_request(Action::Undo).await;
7766
7767        let state = engine.state.lock();
7768        let track = state.tracks.get("track").expect("track exists").lock();
7769        let clip = track.audio.clips.first().expect("clip exists");
7770        assert_eq!(clip.name, "audio/original.wav");
7771        assert_eq!(clip.start, 100);
7772        assert_eq!(clip.end, 220);
7773        assert_eq!(clip.end.saturating_sub(clip.start), 120);
7774        assert_eq!(clip.offset, 12);
7775    }
7776
7777    #[tokio::test]
7778    async fn track_offline_bounce_queues_when_no_worker_is_ready() {
7779        let (mut engine, _client_rx) = make_engine_with_client();
7780        insert_track(
7781            &mut engine,
7782            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
7783        );
7784
7785        engine
7786            .handle_request(Action::TrackOfflineBounce {
7787                track_name: "track".to_string(),
7788                output_path: "/tmp/out.wav".to_string(),
7789                start_sample: 0,
7790                length_samples: 128,
7791                automation_lanes: vec![],
7792                apply_fader: false,
7793            })
7794            .await;
7795
7796        assert!(engine.offline_bounce_jobs.is_empty());
7797        assert_eq!(engine.pending_requests.len(), 1);
7798        assert!(matches!(
7799            engine.pending_requests.front(),
7800            Some(Action::TrackOfflineBounce { track_name, length_samples, .. })
7801                if track_name == "track" && *length_samples == 128
7802        ));
7803    }
7804
7805    #[tokio::test]
7806    async fn track_offline_bounce_returns_missing_track_error() {
7807        let (mut engine, mut client_rx) = make_engine_with_client();
7808
7809        engine
7810            .handle_request(Action::TrackOfflineBounce {
7811                track_name: "missing".to_string(),
7812                output_path: "/tmp/out.wav".to_string(),
7813                start_sample: 0,
7814                length_samples: 128,
7815                automation_lanes: vec![],
7816                apply_fader: false,
7817            })
7818            .await;
7819
7820        match client_rx.recv().await.expect("response") {
7821            Message::Response(Err(err)) => {
7822                assert_eq!(err, "Track not found: missing");
7823            }
7824            other => panic!("unexpected message: {other:?}"),
7825        }
7826    }
7827
7828    #[tokio::test]
7829    async fn track_offline_bounce_clears_job_when_worker_send_fails() {
7830        let (mut engine, mut client_rx) = make_engine_with_client();
7831        insert_track(
7832            &mut engine,
7833            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
7834        );
7835        let (worker_tx, worker_rx) = channel(1);
7836        drop(worker_rx);
7837        engine
7838            .workers
7839            .push(WorkerData::new(worker_tx, tokio::spawn(async {})));
7840        engine.ready_refill_workers.push(0);
7841
7842        engine
7843            .handle_request(Action::TrackOfflineBounce {
7844                track_name: "track".to_string(),
7845                output_path: "/tmp/out.wav".to_string(),
7846                start_sample: 0,
7847                length_samples: 128,
7848                automation_lanes: vec![],
7849                apply_fader: false,
7850            })
7851            .await;
7852
7853        assert!(engine.offline_bounce_jobs.is_empty());
7854        match client_rx.recv().await.expect("response") {
7855            Message::Response(Err(err)) => {
7856                assert!(err.contains("Failed to schedule offline bounce"));
7857            }
7858            other => panic!("unexpected message: {other:?}"),
7859        }
7860    }
7861}