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