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                let plugins = {
5003                    let host = crate::lv2::Lv2Host::new(48_000.0);
5004                    host.list_plugins()
5005                };
5006                self.notify_clients(Ok(Action::Lv2Plugins(plugins))).await;
5007                return;
5008            }
5009            #[cfg(all(unix, not(target_os = "macos")))]
5010            Action::Lv2Plugins(_) => {}
5011            Action::ListVst3Plugins => {
5012                self.notify_clients(Ok(Action::Vst3Plugins(crate::vst3::list_plugins())))
5013                    .await;
5014                return;
5015            }
5016            Action::Vst3Plugins(_) => {}
5017            Action::ListClapPlugins => {
5018                self.notify_clients(Ok(Action::ClapPlugins(crate::clap::list_plugins())))
5019                    .await;
5020                return;
5021            }
5022            Action::ListClapPluginsWithCapabilities => {
5023                self.notify_clients(Ok(Action::ClapPlugins(
5024                    crate::clap::list_plugins_with_capabilities(true),
5025                )))
5026                .await;
5027                return;
5028            }
5029            Action::ClapPlugins(_) => {}
5030            Action::TrackLoadClapPlugin {
5031                ref track_name,
5032                ref plugin_path,
5033                instance_id,
5034            } => {
5035                if self
5036                    .reject_if_track_frozen(track_name, "CLAP plugin loading")
5037                    .await
5038                {
5039                    return;
5040                }
5041                let track = match self.track_handle_or_err(track_name) {
5042                    Ok(track) => track,
5043                    Err(e) => {
5044                        self.notify_clients(Err(e)).await;
5045                        return;
5046                    }
5047                };
5048                let track = track.lock();
5049                if track.audio.processing {
5050                    self.notify_clients(Err(format!(
5051                        "Track '{}' is currently processing audio; stop playback before loading CLAP plugins",
5052                        track_name
5053                    )))
5054                    .await;
5055                    return;
5056                }
5057                if let Err(e) = track.load_clap_plugin(plugin_path, instance_id) {
5058                    self.notify_clients(Err(e)).await;
5059                    return;
5060                }
5061            }
5062            Action::TrackUnloadClapPlugin {
5063                ref track_name,
5064                ref plugin_path,
5065            } => {
5066                if self
5067                    .reject_if_track_frozen(track_name, "CLAP plugin unloading")
5068                    .await
5069                {
5070                    return;
5071                }
5072                let track = match self.track_handle_or_err(track_name) {
5073                    Ok(track) => track,
5074                    Err(e) => {
5075                        self.notify_clients(Err(e)).await;
5076                        return;
5077                    }
5078                };
5079                let track = track.lock();
5080                if track.audio.processing {
5081                    self.notify_clients(Err(format!(
5082                        "Track '{}' is currently processing audio; stop playback before unloading CLAP plugins",
5083                        track_name
5084                    )))
5085                    .await;
5086                    return;
5087                }
5088                if let Err(e) = track.unload_clap_plugin(plugin_path) {
5089                    self.notify_clients(Err(e)).await;
5090                    return;
5091                }
5092            }
5093            Action::TrackUnloadClapPluginInstance {
5094                ref track_name,
5095                instance_id,
5096            } => {
5097                if self
5098                    .reject_if_track_frozen(track_name, "CLAP plugin unloading")
5099                    .await
5100                {
5101                    return;
5102                }
5103                let track = match self.track_handle_or_err(track_name) {
5104                    Ok(track) => track,
5105                    Err(e) => {
5106                        self.notify_clients(Err(e)).await;
5107                        return;
5108                    }
5109                };
5110                let track = track.lock();
5111                if track.audio.processing {
5112                    self.notify_clients(Err(format!(
5113                        "Track '{}' is currently processing audio; stop playback before unloading CLAP plugins",
5114                        track_name
5115                    )))
5116                    .await;
5117                    return;
5118                }
5119                if let Err(e) = track.unload_clap_plugin_instance(instance_id) {
5120                    self.notify_clients(Err(e)).await;
5121                    return;
5122                }
5123            }
5124            Action::TrackShowClapGui {
5125                ref track_name,
5126                instance_id,
5127            } => {
5128                let track = match self.track_handle_or_err(track_name) {
5129                    Ok(track) => track,
5130                    Err(e) => {
5131                        self.notify_clients(Err(e)).await;
5132                        return;
5133                    }
5134                };
5135                if let Err(e) = track.lock().show_clap_gui(instance_id) {
5136                    self.notify_clients(Err(e)).await;
5137                    return;
5138                }
5139            }
5140            Action::TrackLoadVst3Plugin {
5141                ref track_name,
5142                ref plugin_path,
5143                instance_id,
5144            } => {
5145                if self
5146                    .reject_if_track_frozen(track_name, "VST3 plugin loading")
5147                    .await
5148                {
5149                    return;
5150                }
5151                let track = match self.track_handle_or_err(track_name) {
5152                    Ok(track) => track,
5153                    Err(e) => {
5154                        self.notify_clients(Err(e)).await;
5155                        return;
5156                    }
5157                };
5158                let track = track.lock();
5159                if track.audio.processing {
5160                    self.notify_clients(Err(format!(
5161                        "Track '{}' is currently processing audio; stop playback before loading VST3 plugins",
5162                        track_name
5163                    )))
5164                    .await;
5165                    return;
5166                }
5167                if let Err(e) = track.load_vst3_plugin(plugin_path, instance_id) {
5168                    self.notify_clients(Err(e)).await;
5169                    return;
5170                }
5171            }
5172            Action::TrackUnloadVst3Plugin {
5173                ref track_name,
5174                ref plugin_path,
5175            } => {
5176                if self
5177                    .reject_if_track_frozen(track_name, "VST3 plugin unloading")
5178                    .await
5179                {
5180                    return;
5181                }
5182                let track = match self.track_handle_or_err(track_name) {
5183                    Ok(track) => track,
5184                    Err(e) => {
5185                        self.notify_clients(Err(e)).await;
5186                        return;
5187                    }
5188                };
5189                let track = track.lock();
5190                if track.audio.processing {
5191                    self.notify_clients(Err(format!(
5192                        "Track '{}' is currently processing audio; stop playback before unloading VST3 plugins",
5193                        track_name
5194                    )))
5195                    .await;
5196                    return;
5197                }
5198                if let Err(e) = track.unload_vst3_plugin(plugin_path) {
5199                    self.notify_clients(Err(e)).await;
5200                    return;
5201                }
5202            }
5203            Action::TrackUnloadVst3PluginInstance {
5204                ref track_name,
5205                instance_id,
5206            } => {
5207                if self
5208                    .reject_if_track_frozen(track_name, "VST3 plugin unloading")
5209                    .await
5210                {
5211                    return;
5212                }
5213                let track = match self.track_handle_or_err(track_name) {
5214                    Ok(track) => track,
5215                    Err(e) => {
5216                        self.notify_clients(Err(e)).await;
5217                        return;
5218                    }
5219                };
5220                let track = track.lock();
5221                if track.audio.processing {
5222                    self.notify_clients(Err(format!(
5223                        "Track '{}' is currently processing audio; stop playback before unloading VST3 plugins",
5224                        track_name
5225                    )))
5226                    .await;
5227                    return;
5228                }
5229                if let Err(e) = track.unload_vst3_plugin_instance(instance_id) {
5230                    self.notify_clients(Err(e)).await;
5231                    return;
5232                }
5233            }
5234            Action::TrackShowVst3Gui {
5235                ref track_name,
5236                instance_id,
5237            } => {
5238                let track = match self.track_handle_or_err(track_name) {
5239                    Ok(track) => track,
5240                    Err(e) => {
5241                        self.notify_clients(Err(e)).await;
5242                        return;
5243                    }
5244                };
5245                if let Err(e) = track.lock().show_vst3_gui(instance_id) {
5246                    self.notify_clients(Err(e)).await;
5247                    return;
5248                }
5249            }
5250            #[cfg(all(unix, not(target_os = "macos")))]
5251            Action::TrackLoadLv2Plugin {
5252                ref track_name,
5253                ref plugin_uri,
5254                instance_id,
5255            } => {
5256                if self
5257                    .reject_if_track_frozen(track_name, "LV2 plugin loading")
5258                    .await
5259                {
5260                    return;
5261                }
5262                let track = match self.track_handle_or_err(track_name) {
5263                    Ok(track) => track,
5264                    Err(e) => {
5265                        self.notify_clients(Err(e)).await;
5266                        return;
5267                    }
5268                };
5269                let track = track.lock();
5270                if track.audio.processing {
5271                    self.notify_clients(Err(format!(
5272                        "Track '{}' is currently processing audio; stop playback before loading LV2 plugins",
5273                        track_name
5274                    )))
5275                    .await;
5276                    return;
5277                }
5278                if let Err(e) = track.load_lv2_plugin(plugin_uri, instance_id) {
5279                    self.notify_clients(Err(e)).await;
5280                    return;
5281                }
5282            }
5283            #[cfg(all(unix, not(target_os = "macos")))]
5284            Action::TrackUnloadLv2Plugin {
5285                ref track_name,
5286                ref plugin_uri,
5287            } => {
5288                if self
5289                    .reject_if_track_frozen(track_name, "LV2 plugin unloading")
5290                    .await
5291                {
5292                    return;
5293                }
5294                let track = match self.track_handle_or_err(track_name) {
5295                    Ok(track) => track,
5296                    Err(e) => {
5297                        self.notify_clients(Err(e)).await;
5298                        return;
5299                    }
5300                };
5301                let track = track.lock();
5302                if track.audio.processing {
5303                    self.notify_clients(Err(format!(
5304                        "Track '{}' is currently processing audio; stop playback before unloading LV2 plugins",
5305                        track_name
5306                    )))
5307                    .await;
5308                    return;
5309                }
5310                if let Err(e) = track.unload_lv2_plugin(plugin_uri) {
5311                    self.notify_clients(Err(e)).await;
5312                    return;
5313                }
5314            }
5315            #[cfg(all(unix, not(target_os = "macos")))]
5316            Action::TrackUnloadLv2PluginInstance {
5317                ref track_name,
5318                instance_id,
5319            } => {
5320                if self
5321                    .reject_if_track_frozen(track_name, "LV2 plugin unloading")
5322                    .await
5323                {
5324                    return;
5325                }
5326                let track = match self.track_handle_or_err(track_name) {
5327                    Ok(track) => track,
5328                    Err(e) => {
5329                        self.notify_clients(Err(e)).await;
5330                        return;
5331                    }
5332                };
5333                let track = track.lock();
5334                if track.audio.processing {
5335                    self.notify_clients(Err(format!(
5336                        "Track '{}' is currently processing audio; stop playback before unloading LV2 plugins",
5337                        track_name
5338                    )))
5339                    .await;
5340                    return;
5341                }
5342                if let Err(e) = track.unload_lv2_plugin_instance(instance_id) {
5343                    self.notify_clients(Err(e)).await;
5344                    return;
5345                }
5346            }
5347            #[cfg(all(unix, not(target_os = "macos")))]
5348            Action::TrackShowLv2Gui {
5349                ref track_name,
5350                instance_id,
5351            } => {
5352                let track = match self.track_handle_or_err(track_name) {
5353                    Ok(track) => track,
5354                    Err(e) => {
5355                        self.notify_clients(Err(e)).await;
5356                        return;
5357                    }
5358                };
5359                if let Err(e) = track.lock().show_lv2_gui(instance_id) {
5360                    self.notify_clients(Err(e)).await;
5361                    return;
5362                }
5363            }
5364            Action::TrackSetClapParameter {
5365                ref track_name,
5366                instance_id,
5367                param_id,
5368                value,
5369            } => {
5370                if self
5371                    .reject_if_track_frozen(track_name, "CLAP parameter changes")
5372                    .await
5373                {
5374                    return;
5375                }
5376                match self.track_handle_or_err(track_name) {
5377                    Ok(track) => {
5378                        if let Err(e) =
5379                            track
5380                                .lock()
5381                                .set_clap_parameter(instance_id, param_id, value)
5382                        {
5383                            self.notify_clients(Err(e)).await;
5384                            return;
5385                        }
5386                        self.notify_clients(Ok(a.clone())).await;
5387                    }
5388                    Err(e) => {
5389                        self.notify_clients(Err(e)).await;
5390                    }
5391                }
5392            }
5393            Action::ClipSetClapParameter {
5394                ref track_name,
5395                clip_idx,
5396                instance_id,
5397                param_id,
5398                value,
5399            } => {
5400                if self
5401                    .reject_if_track_frozen(track_name, "CLAP parameter changes")
5402                    .await
5403                {
5404                    return;
5405                }
5406                match self.track_handle_or_err(track_name) {
5407                    Ok(track) => {
5408                        if let Err(e) = track.lock().clip_set_clap_parameter(
5409                            clip_idx,
5410                            instance_id,
5411                            param_id,
5412                            value,
5413                        ) {
5414                            self.notify_clients(Err(e)).await;
5415                            return;
5416                        }
5417                        self.notify_clients(Ok(a.clone())).await;
5418                    }
5419                    Err(e) => {
5420                        self.notify_clients(Err(e)).await;
5421                    }
5422                }
5423            }
5424            Action::TrackSetClapParameterAt {
5425                ref track_name,
5426                instance_id,
5427                param_id,
5428                value,
5429                frame,
5430            } => {
5431                if self
5432                    .reject_if_track_frozen(track_name, "CLAP parameter changes")
5433                    .await
5434                {
5435                    return;
5436                }
5437                match self.track_handle_or_err(track_name) {
5438                    Ok(track) => {
5439                        if let Err(e) =
5440                            track
5441                                .lock()
5442                                .set_clap_parameter_at(instance_id, param_id, value, frame)
5443                        {
5444                            self.notify_clients(Err(e)).await;
5445                            return;
5446                        }
5447                        self.notify_clients(Ok(a.clone())).await;
5448                    }
5449                    Err(e) => {
5450                        self.notify_clients(Err(e)).await;
5451                    }
5452                }
5453            }
5454            Action::TrackBeginClapParameterEdit {
5455                ref track_name,
5456                instance_id,
5457                param_id,
5458                frame,
5459            } => {
5460                if self
5461                    .reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
5462                    .await
5463                {
5464                    return;
5465                }
5466                match self.track_handle_or_err(track_name) {
5467                    Ok(track) => {
5468                        if let Err(e) =
5469                            track
5470                                .lock()
5471                                .begin_clap_parameter_edit(instance_id, param_id, frame)
5472                        {
5473                            self.notify_clients(Err(e)).await;
5474                            return;
5475                        }
5476                        self.notify_clients(Ok(a.clone())).await;
5477                    }
5478                    Err(e) => {
5479                        self.notify_clients(Err(e)).await;
5480                    }
5481                }
5482            }
5483            Action::TrackEndClapParameterEdit {
5484                ref track_name,
5485                instance_id,
5486                param_id,
5487                frame,
5488            } => {
5489                if self
5490                    .reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
5491                    .await
5492                {
5493                    return;
5494                }
5495                match self.track_handle_or_err(track_name) {
5496                    Ok(track) => {
5497                        if let Err(e) =
5498                            track
5499                                .lock()
5500                                .end_clap_parameter_edit(instance_id, param_id, frame)
5501                        {
5502                            self.notify_clients(Err(e)).await;
5503                            return;
5504                        }
5505                        self.notify_clients(Ok(a.clone())).await;
5506                    }
5507                    Err(e) => {
5508                        self.notify_clients(Err(e)).await;
5509                    }
5510                }
5511            }
5512            Action::TrackGetClapParameters {
5513                ref track_name,
5514                instance_id,
5515            } => match self.track_handle_or_err(track_name) {
5516                Ok(track) => match track.lock().get_clap_parameters(instance_id) {
5517                    Ok(parameters) => {
5518                        self.notify_clients(Ok(Action::TrackClapParameters {
5519                            track_name: track_name.clone(),
5520                            instance_id,
5521                            parameters,
5522                        }))
5523                        .await;
5524                    }
5525                    Err(e) => {
5526                        self.notify_clients(Err(e)).await;
5527                    }
5528                },
5529                Err(e) => {
5530                    self.notify_clients(Err(e)).await;
5531                }
5532            },
5533            Action::TrackClapParameters { .. } => {}
5534            Action::TrackClapSnapshotState {
5535                ref track_name,
5536                instance_id,
5537            } => match self.track_handle_or_err(track_name) {
5538                Ok(track) => {
5539                    let plugin_path = track
5540                        .lock()
5541                        .clap_plugins
5542                        .iter()
5543                        .find(|instance| instance.id == instance_id)
5544                        .map(|instance| instance.processor.lock().path().to_string())
5545                        .unwrap_or_default();
5546                    match track.lock().clap_snapshot_state(instance_id) {
5547                        Ok(state) => {
5548                            self.notify_clients(Ok(Action::TrackClapStateSnapshot {
5549                                track_name: track_name.clone(),
5550                                instance_id,
5551                                plugin_path,
5552                                state,
5553                            }))
5554                            .await;
5555                        }
5556                        Err(e) => {
5557                            self.notify_clients(Err(e)).await;
5558                        }
5559                    }
5560                }
5561                Err(e) => {
5562                    self.notify_clients(Err(e)).await;
5563                }
5564            },
5565            Action::ClipClapSnapshotState {
5566                ref track_name,
5567                clip_idx,
5568                instance_id,
5569            } => match self.track_handle_or_err(track_name) {
5570                Ok(track) => match track.lock().clip_clap_snapshot_state(clip_idx, instance_id) {
5571                    Ok((plugin_path, state)) => {
5572                        self.notify_clients(Ok(Action::ClipClapStateSnapshot {
5573                            track_name: track_name.clone(),
5574                            clip_idx,
5575                            instance_id,
5576                            plugin_path,
5577                            state,
5578                        }))
5579                        .await;
5580                    }
5581                    Err(e) => {
5582                        self.notify_clients(Err(e)).await;
5583                    }
5584                },
5585                Err(e) => {
5586                    self.notify_clients(Err(e)).await;
5587                }
5588            },
5589            Action::TrackClapStateSnapshot { .. } => {}
5590            Action::ClipClapStateSnapshot { .. } => {}
5591            Action::TrackClapRestoreState {
5592                ref track_name,
5593                instance_id,
5594                ref state,
5595            } => {
5596                if self
5597                    .reject_if_track_frozen(track_name, "CLAP state restore")
5598                    .await
5599                {
5600                    return;
5601                }
5602                let track = match self.track_handle_or_err(track_name) {
5603                    Ok(track) => track,
5604                    Err(e) => {
5605                        self.notify_clients(Err(e)).await;
5606                        return;
5607                    }
5608                };
5609                let track = track.lock();
5610                if track.audio.processing {
5611                    self.notify_clients(Err(format!(
5612                        "Track '{}' is currently processing audio; stop playback before restoring CLAP state",
5613                        track_name
5614                    )))
5615                    .await;
5616                    return;
5617                }
5618                if let Err(e) = track.clap_restore_state(instance_id, state) {
5619                    self.notify_clients(Err(e)).await;
5620                    return;
5621                }
5622            }
5623            Action::ClipClapRestoreState {
5624                ref track_name,
5625                clip_idx,
5626                instance_id,
5627                ref state,
5628            } => {
5629                if self
5630                    .reject_if_track_frozen(track_name, "CLAP state restore")
5631                    .await
5632                {
5633                    return;
5634                }
5635                let track = match self.track_handle_or_err(track_name) {
5636                    Ok(track) => track,
5637                    Err(e) => {
5638                        self.notify_clients(Err(e)).await;
5639                        return;
5640                    }
5641                };
5642                let track = track.lock();
5643                if track.audio.processing {
5644                    self.notify_clients(Err(format!(
5645                        "Track '{}' is currently processing audio; stop playback before restoring CLAP state",
5646                        track_name
5647                    )))
5648                    .await;
5649                    return;
5650                }
5651                if let Err(e) = track.clip_clap_restore_state(clip_idx, instance_id, state) {
5652                    self.notify_clients(Err(e)).await;
5653                    return;
5654                }
5655            }
5656            Action::TrackSnapshotAllClapStates { ref track_name } => {
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                for (instance_id, plugin_path, state) in track.lock().clap_snapshot_all_states() {
5665                    self.notify_clients(Ok(Action::TrackClapStateSnapshot {
5666                        track_name: track_name.clone(),
5667                        instance_id,
5668                        plugin_path,
5669                        state,
5670                    }))
5671                    .await;
5672                }
5673                self.notify_clients(Ok(Action::TrackSnapshotAllClapStatesDone {
5674                    track_name: track_name.clone(),
5675                }))
5676                .await;
5677            }
5678            Action::TrackSnapshotAllClapStatesDone { .. } => {}
5679            Action::TrackGetVst3Graph { ref track_name } => {
5680                match self.track_handle_or_err(track_name) {
5681                    Ok(track) => {
5682                        let t = track.lock();
5683                        let plugins = t.vst3_graph_plugins();
5684                        let connections = t.vst3_graph_connections();
5685                        self.notify_clients(Ok(Action::TrackVst3Graph {
5686                            track_name: track_name.clone(),
5687                            plugins,
5688                            connections,
5689                        }))
5690                        .await;
5691                    }
5692                    Err(e) => {
5693                        self.notify_clients(Err(e)).await;
5694                    }
5695                }
5696            }
5697            Action::TrackVst3Graph { .. } => {}
5698            Action::TrackSetVst3Parameter {
5699                ref track_name,
5700                instance_id,
5701                param_id,
5702                value,
5703            } => {
5704                if self
5705                    .reject_if_track_frozen(track_name, "VST3 parameter changes")
5706                    .await
5707                {
5708                    return;
5709                }
5710                match self.track_handle_or_err(track_name) {
5711                    Ok(track) => {
5712                        if let Err(e) =
5713                            track
5714                                .lock()
5715                                .set_vst3_parameter(instance_id, param_id, value)
5716                        {
5717                            self.notify_clients(Err(e)).await;
5718                            return;
5719                        }
5720                        self.notify_clients(Ok(a.clone())).await;
5721                    }
5722                    Err(e) => {
5723                        self.notify_clients(Err(e)).await;
5724                    }
5725                }
5726            }
5727            Action::TrackSetPluginBypassed {
5728                ref track_name,
5729                instance_id,
5730                ref format,
5731                bypassed,
5732            } => match self.track_handle_or_err(track_name) {
5733                Ok(track) => {
5734                    let result = match format.as_str() {
5735                        "CLAP" => track.lock().set_clap_plugin_bypassed(instance_id, bypassed),
5736                        "VST3" => track.lock().set_vst3_plugin_bypassed(instance_id, bypassed),
5737                        #[cfg(all(unix, not(target_os = "macos")))]
5738                        "LV2" => track.lock().set_lv2_plugin_bypassed(instance_id, bypassed),
5739                        _ => Err(format!("Unknown plugin format for bypass: {format}")),
5740                    };
5741                    if let Err(e) = result {
5742                        self.notify_clients(Err(e)).await;
5743                        return;
5744                    }
5745                    self.notify_clients(Ok(a.clone())).await;
5746                }
5747                Err(e) => {
5748                    self.notify_clients(Err(e)).await;
5749                }
5750            },
5751            Action::TrackGetVst3Parameters {
5752                ref track_name,
5753                instance_id,
5754            } => match self.track_handle_or_err(track_name) {
5755                Ok(track) => match track.lock().get_vst3_parameters(instance_id) {
5756                    Ok(parameters) => {
5757                        self.notify_clients(Ok(Action::TrackVst3Parameters {
5758                            track_name: track_name.clone(),
5759                            instance_id,
5760                            parameters,
5761                        }))
5762                        .await;
5763                    }
5764                    Err(e) => {
5765                        self.notify_clients(Err(e)).await;
5766                    }
5767                },
5768                Err(e) => {
5769                    self.notify_clients(Err(e)).await;
5770                }
5771            },
5772            Action::TrackVst3Parameters { .. } => {}
5773            Action::TrackVst3SnapshotState {
5774                ref track_name,
5775                instance_id,
5776            } => match self.track_handle_or_err(track_name) {
5777                Ok(track) => match track.lock().vst3_snapshot_state(instance_id) {
5778                    Ok(state) => {
5779                        self.notify_clients(Ok(Action::TrackVst3StateSnapshot {
5780                            track_name: track_name.clone(),
5781                            instance_id,
5782                            state,
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::ClipVst3SnapshotState {
5795                ref track_name,
5796                clip_idx,
5797                instance_id,
5798            } => match self.track_handle_or_err(track_name) {
5799                Ok(track) => match track.lock().clip_vst3_snapshot_state(clip_idx, instance_id) {
5800                    Ok(state) => {
5801                        self.notify_clients(Ok(Action::ClipVst3StateSnapshot {
5802                            track_name: track_name.clone(),
5803                            clip_idx,
5804                            instance_id,
5805                            state,
5806                        }))
5807                        .await;
5808                    }
5809                    Err(e) => {
5810                        self.notify_clients(Err(e)).await;
5811                    }
5812                },
5813                Err(e) => {
5814                    self.notify_clients(Err(e)).await;
5815                }
5816            },
5817            Action::TrackVst3StateSnapshot { .. } => {}
5818            Action::ClipVst3StateSnapshot { .. } => {}
5819            Action::TrackVst3RestoreState {
5820                ref track_name,
5821                instance_id,
5822                ref state,
5823            } => match self.track_handle_or_err(track_name) {
5824                Ok(track) => {
5825                    if let Err(e) = track.lock().vst3_restore_state(instance_id, state) {
5826                        self.notify_clients(Err(e)).await;
5827                        return;
5828                    }
5829                    self.notify_clients(Ok(a.clone())).await;
5830                }
5831                Err(e) => {
5832                    self.notify_clients(Err(e)).await;
5833                }
5834            },
5835            Action::TrackConnectVst3Audio {
5836                ref track_name,
5837                ref from_node,
5838                from_port,
5839                ref to_node,
5840                to_port,
5841            } => {
5842                if self
5843                    .reject_if_track_frozen(track_name, "VST3 routing changes")
5844                    .await
5845                {
5846                    return;
5847                }
5848                match self.track_handle_or_err(track_name) {
5849                    Ok(track) => {
5850                        if let Err(e) = track
5851                            .lock()
5852                            .connect_vst3_audio(from_node, from_port, to_node, to_port)
5853                        {
5854                            self.notify_clients(Err(e)).await;
5855                            return;
5856                        }
5857                        self.notify_clients(Ok(a.clone())).await;
5858                    }
5859                    Err(e) => {
5860                        self.notify_clients(Err(e)).await;
5861                    }
5862                }
5863            }
5864            Action::TrackDisconnectVst3Audio {
5865                ref track_name,
5866                ref from_node,
5867                from_port,
5868                ref to_node,
5869                to_port,
5870            } => {
5871                if self
5872                    .reject_if_track_frozen(track_name, "VST3 routing changes")
5873                    .await
5874                {
5875                    return;
5876                }
5877                match self.track_handle_or_err(track_name) {
5878                    Ok(track) => {
5879                        if let Err(e) = track
5880                            .lock()
5881                            .disconnect_vst3_audio(from_node, from_port, to_node, to_port)
5882                        {
5883                            self.notify_clients(Err(e)).await;
5884                            return;
5885                        }
5886                        self.notify_clients(Ok(a.clone())).await;
5887                    }
5888                    Err(e) => {
5889                        self.notify_clients(Err(e)).await;
5890                    }
5891                }
5892            }
5893            Action::ClipMove {
5894                ref kind,
5895                ref from,
5896                ref to,
5897                copy,
5898            } => {
5899                if let Some(from_track_handle) = self.state.lock().tracks.get(&from.track_name)
5900                    && let Some(to_track_handle) = self.state.lock().tracks.get(&to.track_name)
5901                {
5902                    let from_track = from_track_handle.lock();
5903                    let to_track = to_track_handle.lock();
5904                    match kind {
5905                        Kind::Audio => {
5906                            if from.clip_index >= from_track.audio.clips.len() {
5907                                self.notify_clients(Err(format!(
5908                                    "Clip index {} is too high, as track {} has only {} clips!",
5909                                    from.clip_index,
5910                                    from_track.name.clone(),
5911                                    from_track.audio.clips.len(),
5912                                )))
5913                                .await;
5914                                return;
5915                            }
5916                            if from_track.audio.ins.len() != to_track.audio.ins.len() {
5917                                self.notify_clients(Err(format!(
5918                                    "Cannot move/copy audio clip from '{}' ({} inputs) to '{}' ({} inputs)",
5919                                    from_track.name,
5920                                    from_track.audio.ins.len(),
5921                                    to_track.name,
5922                                    to_track.audio.ins.len()
5923                                )))
5924                                .await;
5925                                return;
5926                            }
5927                            let clip_copy = from_track.audio.clips[from.clip_index].clone();
5928                            if !copy {
5929                                from_track.audio.clips.remove(from.clip_index);
5930                            }
5931                            let mut clip_copy = clip_copy;
5932                            clip_copy.start = to.sample_offset;
5933                            let max_lane = to_track.audio.ins.len().saturating_sub(1);
5934                            clip_copy.input_channel = to.input_channel.min(max_lane);
5935                            to_track.audio.clips.push(clip_copy);
5936                        }
5937                        Kind::MIDI => {
5938                            if from.clip_index >= from_track.midi.clips.len() {
5939                                self.notify_clients(Err(format!(
5940                                    "Clip index {} is too high, as track {} has only {} clips!",
5941                                    from.clip_index,
5942                                    from_track.name.clone(),
5943                                    from_track.midi.clips.len(),
5944                                )))
5945                                .await;
5946                                return;
5947                            }
5948                            let clip_copy = from_track.midi.clips[from.clip_index].clone();
5949                            if !copy {
5950                                from_track.midi.clips.remove(from.clip_index);
5951                            }
5952                            let mut clip_copy = clip_copy;
5953                            clip_copy.start = to.sample_offset;
5954                            let max_lane = to_track.midi.ins.len().saturating_sub(1);
5955                            clip_copy.input_channel = to.input_channel.min(max_lane);
5956                            to_track.midi.clips.push(clip_copy);
5957                        }
5958                    }
5959                }
5960            }
5961            Action::AddClip {
5962                ref name,
5963                ref track_name,
5964                start,
5965                length,
5966                offset,
5967                input_channel,
5968                muted,
5969                ref peaks_file,
5970                kind,
5971                fade_enabled,
5972                fade_in_samples,
5973                fade_out_samples,
5974                ref source_name,
5975                source_offset,
5976                source_length,
5977                ref preview_name,
5978                ref pitch_correction_points,
5979                pitch_correction_frame_likeness,
5980                pitch_correction_inertia_ms,
5981                pitch_correction_formant_compensation,
5982                ref plugin_graph_json,
5983            } => {
5984                self.add_clip_to_track(ClipAddRequest {
5985                    name,
5986                    track_name,
5987                    start,
5988                    length,
5989                    offset,
5990                    input_channel,
5991                    muted,
5992                    peaks_file: peaks_file.clone(),
5993                    kind,
5994                    fade_enabled,
5995                    fade_in_samples,
5996                    fade_out_samples,
5997                    source_name: source_name.clone(),
5998                    source_offset,
5999                    source_length,
6000                    preview_name: preview_name.clone(),
6001                    pitch_correction_points: pitch_correction_points.clone(),
6002                    pitch_correction_frame_likeness,
6003                    pitch_correction_inertia_ms,
6004                    pitch_correction_formant_compensation,
6005                    plugin_graph_json: plugin_graph_json.clone(),
6006                });
6007            }
6008            Action::AddGroupedClip {
6009                ref track_name,
6010                kind,
6011                ref audio_clip,
6012                ref midi_clip,
6013            } => {
6014                self.add_grouped_clip_to_track(
6015                    track_name,
6016                    kind,
6017                    audio_clip.clone(),
6018                    midi_clip.clone(),
6019                );
6020            }
6021            Action::RemoveClip {
6022                ref track_name,
6023                kind,
6024                ref clip_indices,
6025            } => {
6026                self.remove_clips_from_track(track_name, kind, clip_indices);
6027            }
6028            Action::RenameClip {
6029                ref track_name,
6030                kind,
6031                clip_index,
6032                ref new_name,
6033            } => {
6034                self.rename_clip_references(track_name, kind, clip_index, new_name);
6035            }
6036            Action::SetClipSourceName {
6037                ref track_name,
6038                kind,
6039                clip_index,
6040                ref name,
6041            } => {
6042                self.set_clip_source_name(track_name, clip_index, kind, name.clone());
6043            }
6044            Action::SetClipFade {
6045                ref track_name,
6046                clip_index,
6047                kind,
6048                fade_enabled,
6049                fade_in_samples,
6050                fade_out_samples,
6051            } => {
6052                self.set_clip_fade(
6053                    track_name,
6054                    clip_index,
6055                    kind,
6056                    fade_enabled,
6057                    fade_in_samples,
6058                    fade_out_samples,
6059                );
6060            }
6061            Action::SetClipBounds {
6062                ref track_name,
6063                clip_index,
6064                kind,
6065                start,
6066                length,
6067                offset,
6068            } => {
6069                self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
6070            }
6071            Action::SyncClipBounds {
6072                ref track_name,
6073                clip_index,
6074                kind,
6075                start,
6076                length,
6077                offset,
6078            } => {
6079                self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
6080            }
6081            Action::SetClipMuted {
6082                ref track_name,
6083                clip_index,
6084                kind,
6085                muted,
6086            } => {
6087                self.set_clip_muted(track_name, clip_index, kind, muted);
6088            }
6089            Action::SetClipPluginGraphJson {
6090                ref track_name,
6091                clip_index,
6092                ref plugin_graph_json,
6093            } => {
6094                self.set_clip_plugin_graph_json(track_name, clip_index, plugin_graph_json.clone());
6095            }
6096            Action::SetClipPitchCorrection {
6097                ref track_name,
6098                clip_index,
6099                ref preview_name,
6100                ref source_name,
6101                source_offset,
6102                source_length,
6103                ref pitch_correction_points,
6104                pitch_correction_frame_likeness,
6105                pitch_correction_inertia_ms,
6106                pitch_correction_formant_compensation,
6107            } => {
6108                self.set_clip_pitch_correction(
6109                    track_name,
6110                    clip_index,
6111                    preview_name.clone(),
6112                    source_name.clone(),
6113                    source_offset,
6114                    source_length,
6115                    pitch_correction_points.clone(),
6116                    pitch_correction_frame_likeness,
6117                    pitch_correction_inertia_ms,
6118                    pitch_correction_formant_compensation,
6119                );
6120            }
6121            Action::Connect {
6122                ref from_track,
6123                from_port,
6124                ref to_track,
6125                to_port,
6126                kind,
6127            } => {
6128                match kind {
6129                    Kind::Audio => {
6130                        let from_audio_io = if from_track == "hw:in" {
6131                            self.hw_input_audio_port(from_port)
6132                        } else {
6133                            self.state
6134                                .lock()
6135                                .tracks
6136                                .get(from_track)
6137                                .and_then(|t| t.lock().audio.outs.get(from_port).cloned())
6138                        };
6139                        let to_audio_io = if to_track == "hw:out" {
6140                            self.hw_output_audio_port(to_port)
6141                        } else {
6142                            self.state
6143                                .lock()
6144                                .tracks
6145                                .get(to_track)
6146                                .and_then(|t| t.lock().audio.ins.get(to_port).cloned())
6147                        };
6148                        match (from_audio_io, to_audio_io) {
6149                            (Some(source), Some(target)) => {
6150                                if from_track != "hw:in"
6151                                    && to_track != "hw:out"
6152                                    && self.check_if_leads_to_kind(
6153                                        Kind::Audio,
6154                                        to_track,
6155                                        from_track,
6156                                    )
6157                                {
6158                                    self.notify_clients(Err(
6159                                        "Circular routing is not allowed!".into()
6160                                    ))
6161                                    .await;
6162                                    return;
6163                                }
6164                                crate::audio::io::AudioIO::connect(&source, &target);
6165                            }
6166                            (None, _) => {
6167                                self.notify_clients(Err(format!(
6168                                    "Source track '{}' not found",
6169                                    from_track
6170                                )))
6171                                .await;
6172                                return;
6173                            }
6174                            (_, None) => {
6175                                self.notify_clients(Err(format!(
6176                                    "Destination track '{}' not found",
6177                                    to_track
6178                                )))
6179                                .await;
6180                                return;
6181                            }
6182                        }
6183                    }
6184                    Kind::MIDI => {
6185                        let from_hw_in_device = Self::midi_hw_in_device(from_track);
6186                        let to_hw_out_device = Self::midi_hw_out_device(to_track);
6187                        let from_is_invalid_hw = Self::midi_hw_out_device(from_track).is_some();
6188                        let to_is_invalid_hw = Self::midi_hw_in_device(to_track).is_some();
6189
6190                        if from_is_invalid_hw || to_is_invalid_hw {
6191                            self.notify_clients(Err(
6192                                "Invalid MIDI hardware connection direction".to_string()
6193                            ))
6194                            .await;
6195                            return;
6196                        }
6197
6198                        if from_hw_in_device.is_none()
6199                            && to_hw_out_device.is_none()
6200                            && self.check_if_leads_to_kind(Kind::MIDI, to_track, from_track)
6201                        {
6202                            self.notify_clients(Err("Circular routing is not allowed!".into()))
6203                                .await;
6204                            return;
6205                        }
6206
6207                        let state = self.state.lock();
6208                        let from_track_handle = state.tracks.get(from_track);
6209                        let to_track_handle = state.tracks.get(to_track);
6210
6211                        if let (Some(from_device), Some(to_device)) =
6212                            (from_hw_in_device, to_hw_out_device)
6213                        {
6214                            let route = MidiHwThruRoute {
6215                                from_device: from_device.to_string(),
6216                                to_device: to_device.to_string(),
6217                            };
6218                            if !self.midi_hw_thru_routes.iter().any(|r| r == &route) {
6219                                self.midi_hw_thru_routes.push(route);
6220                            }
6221                        } else if let Some(device) = from_hw_in_device {
6222                            if let Some(t_t) = to_track_handle {
6223                                if t_t.lock().midi.ins.get(to_port).is_none() {
6224                                    self.notify_clients(Err(format!(
6225                                        "MIDI input port {} not found on track '{}'",
6226                                        to_port, to_track
6227                                    )))
6228                                    .await;
6229                                    return;
6230                                }
6231                                let route = MidiHwInRoute {
6232                                    device: device.to_string(),
6233                                    to_track: to_track.to_string(),
6234                                    to_port,
6235                                };
6236                                if !self.midi_hw_in_routes.iter().any(|r| r == &route) {
6237                                    self.midi_hw_in_routes.push(route);
6238                                }
6239                            } else {
6240                                self.notify_clients(Err(format!(
6241                                    "MIDI destination track not found: {}",
6242                                    to_track
6243                                )))
6244                                .await;
6245                                return;
6246                            }
6247                        } else if let Some(device) = to_hw_out_device {
6248                            if let Some(f_t) = from_track_handle {
6249                                if f_t.lock().midi.outs.get(from_port).is_none() {
6250                                    self.notify_clients(Err(format!(
6251                                        "MIDI output port {} not found on track '{}'",
6252                                        from_port, from_track
6253                                    )))
6254                                    .await;
6255                                    return;
6256                                }
6257                                let route = MidiHwOutRoute {
6258                                    from_track: from_track.to_string(),
6259                                    from_port,
6260                                    device: device.to_string(),
6261                                };
6262                                if !self.midi_hw_out_routes.iter().any(|r| r == &route) {
6263                                    self.midi_hw_out_routes.push(route);
6264                                }
6265                            } else {
6266                                self.notify_clients(Err(format!(
6267                                    "MIDI source track not found: {}",
6268                                    from_track
6269                                )))
6270                                .await;
6271                                return;
6272                            }
6273                        } else {
6274                            match (from_track_handle, to_track_handle) {
6275                                (Some(f_t), Some(t_t)) => {
6276                                    let to_in_res = t_t.lock().midi.ins.get(to_port).cloned();
6277                                    if let Some(to_in) = to_in_res {
6278                                        let from_track = f_t.lock();
6279                                        if let Err(e) =
6280                                            from_track.midi.connect_out(from_port, to_in)
6281                                        {
6282                                            self.notify_clients(Err(e)).await;
6283                                            return;
6284                                        }
6285                                        from_track.invalidate_midi_route_cache();
6286                                    } else {
6287                                        self.notify_clients(Err(format!(
6288                                            "MIDI input port {} not found on track '{}'",
6289                                            to_port, to_track
6290                                        )))
6291                                        .await;
6292                                        return;
6293                                    }
6294                                }
6295                                _ => {
6296                                    self.notify_clients(Err(format!(
6297                                        "MIDI tracks not found: {} or {}",
6298                                        from_track, to_track
6299                                    )))
6300                                    .await;
6301                                    return;
6302                                }
6303                            }
6304                        }
6305                    }
6306                };
6307            }
6308            Action::Disconnect {
6309                ref from_track,
6310                from_port,
6311                ref to_track,
6312                to_port,
6313                kind,
6314            } => {
6315                if kind == Kind::Audio {
6316                    if let Err(e) = self.disconnect_audio_route_and_notify(a.clone()).await {
6317                        self.notify_clients(Err(e)).await;
6318                    }
6319                } else if kind == Kind::MIDI {
6320                    let from_hw_in_device = Self::midi_hw_in_device(from_track);
6321                    let to_hw_out_device = Self::midi_hw_out_device(to_track);
6322
6323                    if let (Some(from_device), Some(to_device)) =
6324                        (from_hw_in_device, to_hw_out_device)
6325                    {
6326                        let before = self.midi_hw_thru_routes.len();
6327                        self.midi_hw_thru_routes.retain(|r| {
6328                            !(r.from_device == from_device && r.to_device == to_device)
6329                        });
6330                        if self.midi_hw_thru_routes.len() < before {
6331                            self.notify_clients(Ok(a.clone())).await;
6332                        } else {
6333                            self.notify_clients(Err(format!(
6334                                "Disconnect failed: MIDI route not found ({} -> {})",
6335                                from_track, to_track
6336                            )))
6337                            .await;
6338                        }
6339                        return;
6340                    }
6341
6342                    if let Some(device) = from_hw_in_device {
6343                        let before = self.midi_hw_in_routes.len();
6344                        self.midi_hw_in_routes.retain(|r| {
6345                            !(r.device == device && r.to_track == *to_track && r.to_port == to_port)
6346                        });
6347                        if self.midi_hw_in_routes.len() < before {
6348                            self.notify_clients(Ok(a.clone())).await;
6349                        } else {
6350                            self.notify_clients(Err(format!(
6351                                "Disconnect failed: MIDI route not found ({} -> {})",
6352                                from_track, to_track
6353                            )))
6354                            .await;
6355                        }
6356                        return;
6357                    }
6358
6359                    if let Some(device) = to_hw_out_device {
6360                        let before = self.midi_hw_out_routes.len();
6361                        self.midi_hw_out_routes.retain(|r| {
6362                            !(r.from_track == *from_track
6363                                && r.from_port == from_port
6364                                && r.device == device)
6365                        });
6366                        if self.midi_hw_out_routes.len() < before {
6367                            self.notify_clients(Ok(a.clone())).await;
6368                        } else {
6369                            self.notify_clients(Err(format!(
6370                                "Disconnect failed: MIDI route not found ({} -> {})",
6371                                from_track, to_track
6372                            )))
6373                            .await;
6374                        }
6375                        return;
6376                    }
6377
6378                    let state = self.state.lock();
6379                    if let (Some(f_t), Some(t_t)) =
6380                        (state.tracks.get(from_track), state.tracks.get(to_track))
6381                        && let Some(to_in) = t_t.lock().midi.ins.get(to_port).cloned()
6382                    {
6383                        let from_track = f_t.lock();
6384                        if let Err(e) = from_track.midi.disconnect_out(from_port, &to_in) {
6385                            self.notify_clients(Err(e)).await;
6386                        } else {
6387                            from_track.invalidate_midi_route_cache();
6388                            self.notify_clients(Ok(a.clone())).await;
6389                        }
6390                    } else {
6391                        self.notify_clients(Err(format!(
6392                            "Disconnect failed: MIDI ports not found ({} -> {})",
6393                            from_track, to_track
6394                        )))
6395                        .await;
6396                    }
6397                }
6398            }
6399
6400            Action::OpenAudioDevice {
6401                ref device,
6402                ref input_device,
6403                sample_rate_hz,
6404                bits,
6405                exclusive,
6406                period_frames,
6407                nperiods,
6408                sync_mode,
6409            } => {
6410                #[cfg(unix)]
6411                {
6412                    let request = AudioOpenRequest {
6413                        device,
6414                        input_device: input_device.as_deref(),
6415                        sample_rate_hz,
6416                        bits,
6417                        exclusive,
6418                        period_frames,
6419                        nperiods,
6420                        sync_mode,
6421                    };
6422                    if self.maybe_open_jack_runtime(request).await.is_some() {
6423                        return;
6424                    }
6425                }
6426                let hw_opts = Self::build_hw_options(exclusive, period_frames, nperiods, sync_mode);
6427                let open_result = self
6428                    .open_non_jack_audio_device(
6429                        device,
6430                        input_device.as_deref(),
6431                        sample_rate_hz,
6432                        bits,
6433                        hw_opts,
6434                    )
6435                    .await;
6436                match open_result {
6437                    Ok(()) => {}
6438                    Err(e) => {
6439                        self.notify_clients(Err(e)).await;
6440                        return;
6441                    }
6442                }
6443                self.finalize_open_audio_device().await;
6444            }
6445            Action::JackAddAudioInputPort => {
6446                #[cfg(unix)]
6447                {
6448                    if let Some(jack) = self.jack_runtime.clone() {
6449                        let (input_channels, output_channels, rate) = {
6450                            let jack = jack.lock();
6451                            if let Err(e) = jack.add_audio_input_port() {
6452                                self.notify_clients(Err(e)).await;
6453                                return;
6454                            }
6455                            (
6456                                jack.input_channels(),
6457                                jack.output_channels(),
6458                                jack.sample_rate,
6459                            )
6460                        };
6461                        self.publish_hw_infos(input_channels, output_channels, rate)
6462                            .await;
6463                        self.notify_clients(Ok(a.clone())).await;
6464                    } else {
6465                        self.notify_clients(Err(
6466                            "JACK runtime is not active; open the JACK backend first".to_string(),
6467                        ))
6468                        .await;
6469                    }
6470                }
6471                #[cfg(not(unix))]
6472                {
6473                    self.notify_clients(Err(
6474                        "JACK backend is not available on this platform build".to_string(),
6475                    ))
6476                    .await;
6477                }
6478            }
6479            Action::JackRemoveAudioInputPort(_removed_port) => {
6480                #[cfg(unix)]
6481                {
6482                    let removed_port = _removed_port;
6483                    if let Some(jack) = self.jack_runtime.clone() {
6484                        let (removed_port, removed_io) = {
6485                            let jack = jack.lock();
6486                            let removed_port = Some(removed_port);
6487                            let removed_io =
6488                                removed_port.and_then(|port| jack.input_audio_port(port));
6489                            match (removed_port, removed_io) {
6490                                (Some(port), Some(io)) => (port, io),
6491                                _ => {
6492                                    self.notify_clients(Err(
6493                                        "JACK audio input port index is out of range".to_string(),
6494                                    ))
6495                                    .await;
6496                                    return;
6497                                }
6498                            }
6499                        };
6500                        let reindex_notifications =
6501                            self.reindex_notifications_for_removed_hw_input(removed_port);
6502                        for disconnect in
6503                            self.disconnect_actions_for_removed_hw_input(removed_port, &removed_io)
6504                        {
6505                            if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await
6506                            {
6507                                self.notify_clients(Err(e)).await;
6508                                return;
6509                            }
6510                        }
6511                        let (input_channels, output_channels, rate) = {
6512                            let jack = jack.lock();
6513                            if let Err(e) = jack.remove_audio_input_port(removed_port) {
6514                                self.notify_clients(Err(e)).await;
6515                                return;
6516                            }
6517                            (
6518                                jack.input_channels(),
6519                                jack.output_channels(),
6520                                jack.sample_rate,
6521                            )
6522                        };
6523                        for action in reindex_notifications {
6524                            self.notify_clients(Ok(action)).await;
6525                        }
6526                        self.publish_hw_infos(input_channels, output_channels, rate)
6527                            .await;
6528                        self.notify_clients(Ok(a.clone())).await;
6529                    } else {
6530                        self.notify_clients(Err(
6531                            "JACK runtime is not active; open the JACK backend first".to_string(),
6532                        ))
6533                        .await;
6534                    }
6535                }
6536                #[cfg(not(unix))]
6537                {
6538                    self.notify_clients(Err(
6539                        "JACK backend is not available on this platform build".to_string(),
6540                    ))
6541                    .await;
6542                }
6543            }
6544            Action::JackAddAudioOutputPort => {
6545                #[cfg(unix)]
6546                {
6547                    if let Some(jack) = self.jack_runtime.clone() {
6548                        let (input_channels, output_channels, rate) = {
6549                            let jack = jack.lock();
6550                            if let Err(e) = jack.add_audio_output_port() {
6551                                self.notify_clients(Err(e)).await;
6552                                return;
6553                            }
6554                            (
6555                                jack.input_channels(),
6556                                jack.output_channels(),
6557                                jack.sample_rate,
6558                            )
6559                        };
6560                        self.publish_hw_infos(input_channels, output_channels, rate)
6561                            .await;
6562                        self.notify_clients(Ok(a.clone())).await;
6563                    } else {
6564                        self.notify_clients(Err(
6565                            "JACK runtime is not active; open the JACK backend first".to_string(),
6566                        ))
6567                        .await;
6568                    }
6569                }
6570                #[cfg(not(unix))]
6571                {
6572                    self.notify_clients(Err(
6573                        "JACK backend is not available on this platform build".to_string(),
6574                    ))
6575                    .await;
6576                }
6577            }
6578            Action::JackRemoveAudioOutputPort(_removed_port) => {
6579                #[cfg(unix)]
6580                {
6581                    let removed_port = _removed_port;
6582                    if let Some(jack) = self.jack_runtime.clone() {
6583                        let (removed_port, removed_io) = {
6584                            let jack = jack.lock();
6585                            let removed_port = Some(removed_port);
6586                            let removed_io =
6587                                removed_port.and_then(|port| jack.output_audio_port(port));
6588                            match (removed_port, removed_io) {
6589                                (Some(port), Some(io)) => (port, io),
6590                                _ => {
6591                                    self.notify_clients(Err(
6592                                        "JACK audio output port index is out of range".to_string(),
6593                                    ))
6594                                    .await;
6595                                    return;
6596                                }
6597                            }
6598                        };
6599                        let reindex_notifications =
6600                            self.reindex_notifications_for_removed_hw_output(removed_port);
6601                        for disconnect in
6602                            self.disconnect_actions_for_removed_hw_output(removed_port, &removed_io)
6603                        {
6604                            if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await
6605                            {
6606                                self.notify_clients(Err(e)).await;
6607                                return;
6608                            }
6609                        }
6610                        let (input_channels, output_channels, rate) = {
6611                            let jack = jack.lock();
6612                            if let Err(e) = jack.remove_audio_output_port(removed_port) {
6613                                self.notify_clients(Err(e)).await;
6614                                return;
6615                            }
6616                            (
6617                                jack.input_channels(),
6618                                jack.output_channels(),
6619                                jack.sample_rate,
6620                            )
6621                        };
6622                        for action in reindex_notifications {
6623                            self.notify_clients(Ok(action)).await;
6624                        }
6625                        self.publish_hw_infos(input_channels, output_channels, rate)
6626                            .await;
6627                        self.notify_clients(Ok(a.clone())).await;
6628                    } else {
6629                        self.notify_clients(Err(
6630                            "JACK runtime is not active; open the JACK backend first".to_string(),
6631                        ))
6632                        .await;
6633                    }
6634                }
6635                #[cfg(not(unix))]
6636                {
6637                    self.notify_clients(Err(
6638                        "JACK backend is not available on this platform build".to_string(),
6639                    ))
6640                    .await;
6641                }
6642            }
6643            Action::OpenMidiInputDevice(ref device) => {
6644                let midi_hub = self.midi_hub.lock();
6645                if let Err(e) = midi_hub.open_input(device) {
6646                    self.notify_clients(Err(e)).await;
6647                    return;
6648                }
6649            }
6650            Action::OpenMidiOutputDevice(ref device) => {
6651                let midi_hub = self.midi_hub.lock();
6652                if let Err(e) = midi_hub.open_output(device) {
6653                    self.notify_clients(Err(e)).await;
6654                    return;
6655                }
6656            }
6657            Action::RequestSessionDiagnostics => {
6658                let (
6659                    track_count,
6660                    frozen_track_count,
6661                    audio_clip_count,
6662                    midi_clip_count,
6663                    lv2_instance_count,
6664                    vst3_instance_count,
6665                    clap_instance_count,
6666                ) = {
6667                    let tracks = &self.state.lock().tracks;
6668                    let mut track_count = 0usize;
6669                    let mut frozen_track_count = 0usize;
6670                    let mut audio_clip_count = 0usize;
6671                    let mut midi_clip_count = 0usize;
6672                    #[cfg(all(unix, not(target_os = "macos")))]
6673                    let mut lv2_instance_count = 0usize;
6674                    #[cfg(not(all(unix, not(target_os = "macos"))))]
6675                    let lv2_instance_count = 0usize;
6676                    let mut vst3_instance_count = 0usize;
6677                    let mut clap_instance_count = 0usize;
6678                    for track in tracks.values() {
6679                        let t = track.lock();
6680                        track_count += 1;
6681                        if t.frozen {
6682                            frozen_track_count += 1;
6683                        }
6684                        audio_clip_count += t.audio.clips.len();
6685                        midi_clip_count += t.midi.clips.len();
6686                        #[cfg(all(unix, not(target_os = "macos")))]
6687                        {
6688                            lv2_instance_count += t.lv2_plugins.len();
6689                        }
6690                        vst3_instance_count += t.vst3_plugins.len();
6691                        clap_instance_count += t.clap_plugins.len();
6692                    }
6693                    (
6694                        track_count,
6695                        frozen_track_count,
6696                        audio_clip_count,
6697                        midi_clip_count,
6698                        lv2_instance_count,
6699                        vst3_instance_count,
6700                        clap_instance_count,
6701                    )
6702                };
6703                #[cfg(not(all(unix, not(target_os = "macos"))))]
6704                let _lv2_instance_count = lv2_instance_count;
6705                let pending_hw_midi_events = self.pending_hw_midi_events.len()
6706                    + self
6707                        .pending_hw_midi_events_by_device
6708                        .values()
6709                        .map(std::vec::Vec::len)
6710                        .sum::<usize>();
6711                let sample_rate_hz = if let Some(hw) = &self.hw_driver {
6712                    hw.lock().sample_rate() as usize
6713                } else {
6714                    #[cfg(unix)]
6715                    {
6716                        self.jack_runtime
6717                            .as_ref()
6718                            .map(|j| j.lock().sample_rate)
6719                            .unwrap_or(0)
6720                    }
6721                    #[cfg(not(unix))]
6722                    0
6723                };
6724                let cycle_samples = self.current_cycle_samples();
6725                self.notify_clients(Ok(Action::SessionDiagnosticsReport {
6726                    track_count,
6727                    frozen_track_count,
6728                    audio_clip_count,
6729                    midi_clip_count,
6730                    #[cfg(all(unix, not(target_os = "macos")))]
6731                    lv2_instance_count,
6732                    vst3_instance_count,
6733                    clap_instance_count,
6734                    pending_requests: self.pending_requests.len(),
6735                    workers_total: self.workers.len(),
6736                    workers_ready: self.ready_workers.len(),
6737                    pending_hw_midi_events,
6738                    playing: self.playing,
6739                    transport_sample: self.transport_sample,
6740                    tempo_bpm: self.tempo_bpm,
6741                    sample_rate_hz,
6742                    cycle_samples,
6743                }))
6744                .await;
6745            }
6746            Action::RequestMidiLearnMappingsReport => {
6747                let mut lines = Vec::<String>::new();
6748                let fmt_binding = |b: &crate::message::MidiLearnBinding| {
6749                    let device = b.device.as_deref().unwrap_or("*");
6750                    format!("{device} CH{} CC{}", b.channel + 1, b.cc)
6751                };
6752                if let Some(b) = self.global_midi_learn_play_pause.as_ref() {
6753                    lines.push(format!("Global PlayPause: {}", fmt_binding(b)));
6754                }
6755                if let Some(b) = self.global_midi_learn_stop.as_ref() {
6756                    lines.push(format!("Global Stop: {}", fmt_binding(b)));
6757                }
6758                if let Some(b) = self.global_midi_learn_record_toggle.as_ref() {
6759                    lines.push(format!("Global RecordToggle: {}", fmt_binding(b)));
6760                }
6761                for (track_name, track) in self.state.lock().tracks.iter() {
6762                    let t = track.lock();
6763                    if let Some(b) = t.midi_learn_volume.as_ref() {
6764                        lines.push(format!("{} Volume: {}", track_name, fmt_binding(b)));
6765                    }
6766                    if let Some(b) = t.midi_learn_balance.as_ref() {
6767                        lines.push(format!("{} Balance: {}", track_name, fmt_binding(b)));
6768                    }
6769                    if let Some(b) = t.midi_learn_mute.as_ref() {
6770                        lines.push(format!("{} Mute: {}", track_name, fmt_binding(b)));
6771                    }
6772                    if let Some(b) = t.midi_learn_solo.as_ref() {
6773                        lines.push(format!("{} Solo: {}", track_name, fmt_binding(b)));
6774                    }
6775                    if let Some(b) = t.midi_learn_arm.as_ref() {
6776                        lines.push(format!("{} Arm: {}", track_name, fmt_binding(b)));
6777                    }
6778                    if let Some(b) = t.midi_learn_input_monitor.as_ref() {
6779                        lines.push(format!("{} InputMonitor: {}", track_name, fmt_binding(b)));
6780                    }
6781                    if let Some(b) = t.midi_learn_disk_monitor.as_ref() {
6782                        lines.push(format!("{} DiskMonitor: {}", track_name, fmt_binding(b)));
6783                    }
6784                }
6785                if lines.is_empty() {
6786                    lines.push("No MIDI learn mappings configured".to_string());
6787                }
6788                self.notify_clients(Ok(Action::MidiLearnMappingsReport { lines }))
6789                    .await;
6790            }
6791            Action::ClearAllMidiLearnBindings => {
6792                self.pending_midi_learn = None;
6793                self.pending_global_midi_learn = None;
6794                self.global_midi_learn_play_pause = None;
6795                self.global_midi_learn_stop = None;
6796                self.global_midi_learn_record_toggle = None;
6797                self.midi_cc_gate.clear();
6798                for track in self.state.lock().tracks.values() {
6799                    let t = track.lock();
6800                    t.midi_learn_volume = None;
6801                    t.midi_learn_balance = None;
6802                    t.midi_learn_mute = None;
6803                    t.midi_learn_solo = None;
6804                    t.midi_learn_arm = None;
6805                    t.midi_learn_input_monitor = None;
6806                    t.midi_learn_disk_monitor = None;
6807                }
6808            }
6809            #[cfg(all(unix, not(target_os = "macos")))]
6810            Action::TrackLv2PluginControls { .. } => {}
6811            #[cfg(all(unix, not(target_os = "macos")))]
6812            Action::ClipLv2PluginControls { .. } => {}
6813            #[cfg(all(unix, not(target_os = "macos")))]
6814            Action::TrackLv2Midnam { .. } => {}
6815            Action::TrackClapNoteNames { .. } => {}
6816            Action::SessionDiagnosticsReport { .. } => {}
6817            Action::MidiLearnMappingsReport { .. } => {}
6818            Action::HWInfo { .. } => {}
6819            Action::HistoryState { .. } => {}
6820            Action::Undo => {}
6821            Action::Redo => {}
6822            Action::ApplyGroupedActions(_) => {}
6823            _ => {}
6824        }
6825
6826        if let Some(inverse) = inverse_actions {
6827            if let Some(group) = self.history_group.as_mut() {
6828                group.forward_actions.push(action_to_process.clone());
6829                group.inverse_actions.splice(0..0, inverse);
6830            } else {
6831                self.history.record(UndoEntry {
6832                    forward_actions: vec![action_to_process.clone()],
6833                    inverse_actions: inverse,
6834                });
6835            }
6836        }
6837
6838        self.notify_clients(Ok(action_to_process)).await;
6839    }
6840
6841    pub async fn work(&mut self) {
6842        while let Some(message) = self.rx.recv().await {
6843            match message {
6844                Message::Ready(id) => {
6845                    self.ready_workers.push(id);
6846                }
6847                Message::Finished {
6848                    worker_id,
6849                    track_name,
6850                    output_linear,
6851                    process_epoch,
6852                    parameter_updates,
6853                } => {
6854                    self.ready_workers.push(worker_id);
6855                    self.track_processing_started_at.remove(&track_name);
6856                    if process_epoch != self.track_process_epoch {
6857                        if let Some(track) = self.state.lock().tracks.get(&track_name).cloned() {
6858                            let t = track.lock();
6859                            t.audio.finished = false;
6860                            t.audio.processing = false;
6861                        }
6862                        continue;
6863                    }
6864                    self.track_meter_linear_by_track
6865                        .insert(track_name, output_linear);
6866                    for action in parameter_updates {
6867                        self.notify_clients(Ok(action)).await;
6868                    }
6869                    self.force_stalled_track_completions();
6870                    let all_finished = self.send_tracks().await;
6871                    if all_finished {
6872                        self.on_all_tracks_finished().await;
6873                    }
6874                }
6875                Message::Channel(s) => {
6876                    self.clients.push(s);
6877                }
6878
6879                Message::Request(a) => match a {
6880                    Action::TrackOfflineBounceCancel { track_name } => {
6881                        if let Some(job) = self.offline_bounce_jobs.get(&track_name) {
6882                            job.cancel.store(true, Ordering::Relaxed);
6883                        }
6884                    }
6885                    Action::TrackOfflineBounceCancelAll => {
6886                        for job in self.offline_bounce_jobs.values() {
6887                            job.cancel.store(true, Ordering::Relaxed);
6888                        }
6889                    }
6890                    _ if !self.offline_bounce_jobs.is_empty() => {
6891                        self.pending_requests.push_back(a);
6892                    }
6893                    Action::OpenAudioDevice { .. }
6894                    | Action::OpenMidiInputDevice(_)
6895                    | Action::OpenMidiOutputDevice(_)
6896                    | Action::RequestMeterSnapshot
6897                    | Action::Quit
6898                    | Action::Play
6899                    | Action::Pause
6900                    | Action::Stop
6901                    | Action::TransportPosition(_)
6902                    | Action::JumpToEnd
6903                    | Action::SetLoopEnabled(_)
6904                    | Action::SetLoopRange(_)
6905                    | Action::SetPunchEnabled(_)
6906                    | Action::SetPunchRange(_)
6907                    | Action::SetMetronomeEnabled(_)
6908                    | Action::SetTempo(_)
6909                    | Action::SetTimeSignature { .. }
6910                    | Action::SetOscEnabled(_)
6911                    | Action::SetClipPlaybackEnabled(_)
6912                    | Action::SetRecordEnabled(_)
6913                    | Action::SetSessionPath(_)
6914                    | Action::ClearHistory
6915                    | Action::BeginSessionRestore
6916                    | Action::PianoKey { .. }
6917                    | Action::ModifyMidiNotes { .. }
6918                    | Action::ModifyMidiControllers { .. }
6919                    | Action::DeleteMidiControllers { .. }
6920                    | Action::InsertMidiControllers { .. }
6921                    | Action::DeleteMidiNotes { .. }
6922                    | Action::InsertMidiNotes { .. }
6923                    | Action::SetMidiSysExEvents { .. } => {
6924                        self.handle_request(a).await;
6925                    }
6926                    #[cfg(all(unix, not(target_os = "macos")))]
6927                    Action::ListLv2Plugins => {
6928                        self.handle_request(a).await;
6929                    }
6930                    Action::ListVst3Plugins => {
6931                        self.handle_request(a).await;
6932                    }
6933                    Action::ListClapPlugins => {
6934                        self.handle_request(a).await;
6935                    }
6936                    Action::ListClapPluginsWithCapabilities => {
6937                        self.handle_request(a).await;
6938                    }
6939                    _ => {
6940                        self.pending_requests.push_back(a);
6941                        if self.can_schedule_hw_cycle() {
6942                            self.request_hw_cycle().await;
6943                        } else {
6944                            while let Some(next) = self.pending_requests.pop_front() {
6945                                self.handle_request(next).await;
6946                            }
6947                        }
6948                    }
6949                },
6950                Message::OfflineBounceFinished { result } => {
6951                    if let Ok(Action::TrackOfflineBounce { track_name, .. }) = &result {
6952                        self.offline_bounce_jobs.remove(track_name);
6953                    }
6954                    self.notify_clients(result).await;
6955                    if self.offline_bounce_jobs.is_empty() {
6956                        while let Some(next) = self.pending_requests.pop_front() {
6957                            self.handle_request(next).await;
6958                        }
6959                    }
6960                }
6961                Message::HWFinished => {
6962                    if !self.awaiting_hwfinished {
6963                        continue;
6964                    }
6965                    self.handling_hwfinished = true;
6966                    self.awaiting_hwfinished = false;
6967                    #[cfg(unix)]
6968                    {
6969                        if let Some(jack) = &self.jack_runtime {
6970                            if !self.pending_hw_midi_out_events.is_empty() {
6971                                let out_events =
6972                                    std::mem::take(&mut self.pending_hw_midi_out_events);
6973                                jack.lock().write_events(&out_events);
6974                            }
6975                            let mut in_events = vec![];
6976                            jack.lock().read_events_into(&mut in_events);
6977                            if !in_events.is_empty() {
6978                                self.pending_hw_midi_events.extend(in_events);
6979                            }
6980                        }
6981                    }
6982                    #[cfg(unix)]
6983                    if self.jack_runtime.is_some() {
6984                        self.sync_from_jack_transport().await;
6985                    }
6986                    while let Some(a) = self.pending_requests.pop_front() {
6987                        self.handle_request(a).await;
6988                    }
6989                    self.apply_mute_solo_policy();
6990                    self.append_recorded_cycle();
6991                    self.flush_completed_recordings().await;
6992                    let hw_in_routes = self.midi_hw_in_routes.clone();
6993                    let pending_hw_in_by_device = self.pending_hw_midi_events_by_device.clone();
6994                    let mut reconfigured_tracks = Vec::new();
6995                    for (track_name, track) in self.state.lock().tracks.iter() {
6996                        let track_lock = track.lock();
6997                        if self.jack_runtime_is_some() {
6998                            if !self.pending_hw_midi_events.is_empty() {
6999                                track_lock.push_hw_midi_events(&self.pending_hw_midi_events);
7000                            }
7001                        } else {
7002                            for route in hw_in_routes.iter().filter(|r| &r.to_track == track_name) {
7003                                if let Some(events) = pending_hw_in_by_device.get(&route.device) {
7004                                    track_lock.push_hw_midi_events_to_port(route.to_port, events);
7005                                }
7006                            }
7007                        }
7008                        if track_lock.setup() {
7009                            reconfigured_tracks.push(track_name.clone());
7010                        }
7011                    }
7012                    self.publish_track_meters().await;
7013                    for track_name in reconfigured_tracks {
7014                        let track = self.state.lock().tracks.get(&track_name).cloned();
7015                        if let Some(track) = track {
7016                            let (plugins, connections) = {
7017                                let track_lock = track.lock();
7018                                (
7019                                    track_lock.plugin_graph_plugins(),
7020                                    track_lock.plugin_graph_connections(),
7021                                )
7022                            };
7023                            self.notify_clients(Ok(Action::TrackPluginGraph {
7024                                track_name: track_name.clone(),
7025                                plugins,
7026                                connections,
7027                            }))
7028                            .await;
7029                        }
7030                    }
7031                    self.pending_hw_midi_events.clear();
7032                    self.pending_hw_midi_events_by_device.clear();
7033                    if self.playing {
7034                        if self.transport_panic_flush_pending {
7035                            self.transport_panic_flush_pending = false;
7036                        } else if self.transport_restart_pending {
7037                            self.transport_restart_pending = false;
7038                        } else {
7039                            let next = self
7040                                .transport_sample
7041                                .saturating_add(self.current_cycle_samples());
7042                            let normalized = self.normalize_transport_sample(next);
7043                            let wrapped = normalized != next;
7044                            self.transport_sample = normalized;
7045                            if wrapped {
7046                                self.notify_clients(Ok(Action::TransportPosition(
7047                                    self.transport_sample,
7048                                )))
7049                                .await;
7050                            }
7051                        }
7052                    }
7053                    if self.send_tracks().await && self.hw_worker.is_some() {
7054                        self.request_hw_cycle().await;
7055                    }
7056                    #[cfg(unix)]
7057                    {
7058                        if self.jack_runtime.is_some() {
7059                            self.awaiting_hwfinished = true;
7060                        }
7061                    }
7062                    self.handling_hwfinished = false;
7063                }
7064                Message::HWMidiEvents(events) => {
7065                    for hw_event in events {
7066                        let thru_targets: Vec<String> = self
7067                            .midi_hw_thru_routes
7068                            .iter()
7069                            .filter(|route| route.from_device == hw_event.device)
7070                            .map(|route| route.to_device.clone())
7071                            .collect();
7072                        for device in thru_targets {
7073                            self.pending_hw_midi_out_events_by_device.push(HwMidiEvent {
7074                                device,
7075                                event: hw_event.event.clone(),
7076                            });
7077                        }
7078                        if hw_event.event.data.len() >= 3 {
7079                            let status = hw_event.event.data[0];
7080                            if status & 0xF0 == 0xB0 {
7081                                let channel = status & 0x0F;
7082                                let cc = hw_event.event.data[1];
7083                                let value = hw_event.event.data[2];
7084                                self.handle_incoming_hw_cc(&hw_event.device, channel, cc, value)
7085                                    .await;
7086                            }
7087                        }
7088                        self.pending_hw_midi_events_by_device
7089                            .entry(hw_event.device)
7090                            .or_default()
7091                            .push(hw_event.event);
7092                    }
7093                }
7094                _ => {}
7095            }
7096        }
7097    }
7098
7099    fn collect_hw_midi_output_events(&self) -> Vec<MidiEvent> {
7100        let mut events = vec![];
7101        for track in self.state.lock().tracks.values() {
7102            events.extend(
7103                track
7104                    .lock()
7105                    .take_hw_midi_out_events()
7106                    .into_iter()
7107                    .map(|evt| evt.event),
7108            );
7109        }
7110        events.sort_by_key(|a| a.frame);
7111        events
7112    }
7113
7114    fn collect_hw_midi_output_events_by_device(&mut self) -> Vec<HwMidiEvent> {
7115        let mut events = Vec::<HwMidiEvent>::new();
7116        let routes = self.midi_hw_out_routes.clone();
7117        let mut events_by_track = HashMap::<String, Vec<crate::track::HwMidiOutEvent>>::new();
7118        {
7119            let state = self.state.lock();
7120            for route in &routes {
7121                if events_by_track.contains_key(&route.from_track) {
7122                    continue;
7123                }
7124                let Some(track) = state.tracks.get(&route.from_track) else {
7125                    continue;
7126                };
7127                events_by_track.insert(
7128                    route.from_track.clone(),
7129                    track.lock().take_hw_midi_out_events(),
7130                );
7131            }
7132        }
7133
7134        for route in routes {
7135            let Some(track_events) = events_by_track.get(&route.from_track) else {
7136                continue;
7137            };
7138            for hw_event in track_events
7139                .iter()
7140                .filter(|evt| evt.port == route.from_port)
7141            {
7142                self.update_active_hw_notes_for_track(
7143                    &route.from_track,
7144                    &route.device,
7145                    &hw_event.event.data,
7146                );
7147                events.push(HwMidiEvent {
7148                    device: route.device.clone(),
7149                    event: hw_event.event.clone(),
7150                });
7151            }
7152        }
7153        events.sort_by(|a, b| {
7154            a.event
7155                .frame
7156                .cmp(&b.event.frame)
7157                .then_with(|| a.device.cmp(&b.device))
7158        });
7159        events
7160    }
7161}
7162
7163#[cfg(test)]
7164mod tests {
7165    use super::*;
7166    use crate::mutex::UnsafeMutex;
7167    use tokio::sync::mpsc::channel;
7168    use tokio::time::{Duration as TokioDuration, timeout};
7169
7170    #[test]
7171    #[cfg(unix)]
7172    fn jack_transport_sync_decision_starts_and_syncs_position_on_external_play() {
7173        let decision = Engine::jack_transport_sync_decision(false, 128, true, 256, 64);
7174
7175        assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Start));
7176        assert_eq!(decision.position_sync, Some(256));
7177    }
7178
7179    #[test]
7180    #[cfg(unix)]
7181    fn jack_transport_sync_decision_stops_and_syncs_position_on_external_stop() {
7182        let decision = Engine::jack_transport_sync_decision(true, 512, false, 96, 64);
7183
7184        assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Stop));
7185        assert_eq!(decision.position_sync, Some(96));
7186    }
7187
7188    #[test]
7189    #[cfg(unix)]
7190    fn jack_transport_sync_decision_ignores_small_rolling_drift() {
7191        let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1040, 64);
7192
7193        assert_eq!(decision.play_sync, None);
7194        assert_eq!(decision.position_sync, None);
7195    }
7196
7197    #[test]
7198    #[cfg(unix)]
7199    fn jack_transport_sync_decision_syncs_large_rolling_jump() {
7200        let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1200, 64);
7201
7202        assert_eq!(decision.play_sync, None);
7203        assert_eq!(decision.position_sync, Some(1200));
7204    }
7205
7206    #[test]
7207    #[cfg(unix)]
7208    fn jack_transport_sync_decision_syncs_locate_while_stopped() {
7209        let decision = Engine::jack_transport_sync_decision(false, 400, false, 900, 64);
7210
7211        assert_eq!(decision.play_sync, None);
7212        assert_eq!(decision.position_sync, Some(900));
7213    }
7214
7215    fn make_engine_with_client() -> (Engine, tokio::sync::mpsc::Receiver<Message>) {
7216        let (engine_tx, engine_rx) = channel(16);
7217        let mut engine = Engine::new(engine_rx, engine_tx);
7218        let (client_tx, client_rx) = channel(16);
7219        engine.clients.push(client_tx);
7220        (engine, client_rx)
7221    }
7222
7223    fn insert_track(engine: &mut Engine, track: Track) {
7224        engine.state.lock().tracks.insert(
7225            track.name.clone(),
7226            Arc::new(UnsafeMutex::new(Box::new(track))),
7227        );
7228    }
7229
7230    fn osc_packet(address: &str) -> Vec<u8> {
7231        fn push_padded_osc_string(packet: &mut Vec<u8>, value: &str) {
7232            packet.extend_from_slice(value.as_bytes());
7233            packet.push(0);
7234            while !packet.len().is_multiple_of(4) {
7235                packet.push(0);
7236            }
7237        }
7238
7239        let mut packet = Vec::new();
7240        push_padded_osc_string(&mut packet, address);
7241        push_padded_osc_string(&mut packet, ",");
7242        packet
7243    }
7244
7245    #[tokio::test]
7246    async fn set_osc_enabled_starts_and_stops_server() {
7247        let (mut engine, _client_rx) = make_engine_with_client();
7248
7249        engine
7250            .set_osc_enabled_with(true, |tx| OscServer::start_on_addr(tx, "127.0.0.1:0"))
7251            .expect("start osc server on ephemeral port");
7252        assert!(engine.osc_server.is_some());
7253
7254        engine
7255            .set_osc_enabled_with(false, OscServer::start)
7256            .expect("stop osc server");
7257        assert!(engine.osc_server.is_none());
7258    }
7259
7260    #[tokio::test]
7261    async fn osc_server_forwards_transport_packets_to_engine_channel() {
7262        let (tx, mut rx) = channel(4);
7263        let mut server =
7264            OscServer::start_on_addr(tx, "127.0.0.1:0").expect("start osc test server");
7265        let socket = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind sender socket");
7266        let packet = osc_packet("/transport/play");
7267        socket
7268            .send_to(&packet, server.listen_addr())
7269            .expect("send osc packet");
7270
7271        let message = timeout(TokioDuration::from_secs(1), rx.recv())
7272            .await
7273            .expect("packet delivery timeout")
7274            .expect("osc message");
7275        match message {
7276            Message::Request(Action::Play) => {}
7277            other => panic!("unexpected osc message: {other:?}"),
7278        }
7279
7280        server.stop();
7281    }
7282
7283    #[tokio::test]
7284    async fn track_offline_bounce_rejects_zero_length_requests() {
7285        let (mut engine, mut client_rx) = make_engine_with_client();
7286        insert_track(
7287            &mut engine,
7288            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
7289        );
7290
7291        engine
7292            .handle_request(Action::TrackOfflineBounce {
7293                track_name: "track".to_string(),
7294                output_path: "/tmp/out.wav".to_string(),
7295                start_sample: 0,
7296                length_samples: 0,
7297                automation_lanes: vec![],
7298                apply_fader: false,
7299            })
7300            .await;
7301
7302        match client_rx.recv().await.expect("response") {
7303            Message::Response(Err(err)) => {
7304                assert!(err.contains("has no renderable content for offline bounce"));
7305            }
7306            other => panic!("unexpected message: {other:?}"),
7307        }
7308    }
7309
7310    #[tokio::test]
7311    async fn track_offline_bounce_rejects_when_same_track_is_active() {
7312        let (mut engine, mut client_rx) = make_engine_with_client();
7313        engine.offline_bounce_jobs.insert(
7314            "other".to_string(),
7315            OfflineBounceJob {
7316                cancel: Arc::new(AtomicBool::new(false)),
7317            },
7318        );
7319
7320        engine
7321            .handle_request(Action::TrackOfflineBounce {
7322                track_name: "other".to_string(),
7323                output_path: "/tmp/out.wav".to_string(),
7324                start_sample: 0,
7325                length_samples: 128,
7326                automation_lanes: vec![],
7327                apply_fader: false,
7328            })
7329            .await;
7330
7331        match client_rx.recv().await.expect("response") {
7332            Message::Response(Err(err)) => {
7333                assert!(err.contains("already in progress"));
7334            }
7335            other => panic!("unexpected message: {other:?}"),
7336        }
7337    }
7338
7339    #[tokio::test]
7340    async fn track_offline_bounce_allows_different_track_concurrently() {
7341        let (mut engine, _client_rx) = make_engine_with_client();
7342        insert_track(
7343            &mut engine,
7344            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
7345        );
7346        engine.offline_bounce_jobs.insert(
7347            "other".to_string(),
7348            OfflineBounceJob {
7349                cancel: Arc::new(AtomicBool::new(false)),
7350            },
7351        );
7352
7353        engine
7354            .handle_request(Action::TrackOfflineBounce {
7355                track_name: "track".to_string(),
7356                output_path: "/tmp/out.wav".to_string(),
7357                start_sample: 0,
7358                length_samples: 128,
7359                automation_lanes: vec![],
7360                apply_fader: false,
7361            })
7362            .await;
7363
7364        assert!(engine.offline_bounce_jobs.contains_key("other"));
7365        assert_eq!(engine.pending_requests.len(), 1);
7366    }
7367
7368    #[tokio::test]
7369    async fn reject_if_track_frozen_sends_error_and_blocks_operation() {
7370        let (mut engine, mut client_rx) = make_engine_with_client();
7371        let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
7372        track.set_frozen(true);
7373        insert_track(&mut engine, track);
7374
7375        let rejected = engine
7376            .reject_if_track_frozen("track", "arming/disarming")
7377            .await;
7378
7379        assert!(rejected);
7380        match client_rx.recv().await.expect("response") {
7381            Message::Response(Err(err)) => {
7382                assert_eq!(err, "Track 'track' is frozen; arming/disarming is blocked");
7383            }
7384            other => panic!("unexpected message: {other:?}"),
7385        }
7386    }
7387
7388    #[tokio::test]
7389    async fn undo_restores_original_clip_bounds_after_stretch_style_group() {
7390        let (mut engine, _client_rx) = make_engine_with_client();
7391        let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
7392        let mut clip = AudioClip::new("audio/original.wav".to_string(), 100, 220);
7393        clip.offset = 12;
7394        clip.fade_in_samples = 20;
7395        clip.fade_out_samples = 30;
7396        track.audio.clips.push(clip);
7397        insert_track(&mut engine, track);
7398
7399        engine.handle_request(Action::BeginHistoryGroup).await;
7400        engine
7401            .handle_request(Action::SetClipBounds {
7402                track_name: "track".to_string(),
7403                clip_index: 0,
7404                kind: Kind::Audio,
7405                start: 120,
7406                length: 180,
7407                offset: 0,
7408            })
7409            .await;
7410        engine
7411            .handle_request(Action::SetClipSourceName {
7412                track_name: "track".to_string(),
7413                clip_index: 0,
7414                kind: Kind::Audio,
7415                name: "audio/stretched.wav".to_string(),
7416            })
7417            .await;
7418        engine
7419            .handle_request(Action::SetClipFade {
7420                track_name: "track".to_string(),
7421                clip_index: 0,
7422                kind: Kind::Audio,
7423                fade_enabled: true,
7424                fade_in_samples: 12,
7425                fade_out_samples: 12,
7426            })
7427            .await;
7428        engine.handle_request(Action::EndHistoryGroup).await;
7429
7430        engine.handle_request(Action::Undo).await;
7431
7432        let state = engine.state.lock();
7433        let track = state.tracks.get("track").expect("track exists").lock();
7434        let clip = track.audio.clips.first().expect("clip exists");
7435        assert_eq!(clip.name, "audio/original.wav");
7436        assert_eq!(clip.start, 100);
7437        assert_eq!(clip.end, 220);
7438        assert_eq!(clip.end.saturating_sub(clip.start), 120);
7439        assert_eq!(clip.offset, 12);
7440    }
7441
7442    #[tokio::test]
7443    async fn track_offline_bounce_queues_when_no_worker_is_ready() {
7444        let (mut engine, _client_rx) = make_engine_with_client();
7445        insert_track(
7446            &mut engine,
7447            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
7448        );
7449
7450        engine
7451            .handle_request(Action::TrackOfflineBounce {
7452                track_name: "track".to_string(),
7453                output_path: "/tmp/out.wav".to_string(),
7454                start_sample: 0,
7455                length_samples: 128,
7456                automation_lanes: vec![],
7457                apply_fader: false,
7458            })
7459            .await;
7460
7461        assert!(engine.offline_bounce_jobs.is_empty());
7462        assert_eq!(engine.pending_requests.len(), 1);
7463        assert!(matches!(
7464            engine.pending_requests.front(),
7465            Some(Action::TrackOfflineBounce { track_name, length_samples, .. })
7466                if track_name == "track" && *length_samples == 128
7467        ));
7468    }
7469
7470    #[tokio::test]
7471    async fn track_offline_bounce_returns_missing_track_error() {
7472        let (mut engine, mut client_rx) = make_engine_with_client();
7473
7474        engine
7475            .handle_request(Action::TrackOfflineBounce {
7476                track_name: "missing".to_string(),
7477                output_path: "/tmp/out.wav".to_string(),
7478                start_sample: 0,
7479                length_samples: 128,
7480                automation_lanes: vec![],
7481                apply_fader: false,
7482            })
7483            .await;
7484
7485        match client_rx.recv().await.expect("response") {
7486            Message::Response(Err(err)) => {
7487                assert_eq!(err, "Track not found: missing");
7488            }
7489            other => panic!("unexpected message: {other:?}"),
7490        }
7491    }
7492
7493    #[tokio::test]
7494    async fn track_offline_bounce_clears_job_when_worker_send_fails() {
7495        let (mut engine, mut client_rx) = make_engine_with_client();
7496        insert_track(
7497            &mut engine,
7498            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
7499        );
7500        let (worker_tx, worker_rx) = channel(1);
7501        drop(worker_rx);
7502        engine
7503            .workers
7504            .push(WorkerData::new(worker_tx, tokio::spawn(async {})));
7505        engine.ready_workers.push(0);
7506
7507        engine
7508            .handle_request(Action::TrackOfflineBounce {
7509                track_name: "track".to_string(),
7510                output_path: "/tmp/out.wav".to_string(),
7511                start_sample: 0,
7512                length_samples: 128,
7513                automation_lanes: vec![],
7514                apply_fader: false,
7515            })
7516            .await;
7517
7518        assert!(engine.offline_bounce_jobs.is_empty());
7519        match client_rx.recv().await.expect("response") {
7520            Message::Response(Err(err)) => {
7521                assert!(err.contains("Failed to schedule offline bounce"));
7522            }
7523            other => panic!("unexpected message: {other:?}"),
7524        }
7525    }
7526}