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