Skip to main content

maolan_engine/
engine.rs

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