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