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.notify_clients(Ok(Action::Quit)).await;
4707                self.ready_realtime_workers.clear();
4708                self.ready_refill_workers.clear();
4709                while !self.workers.is_empty() {
4710                    let worker = self.workers.remove(0);
4711                    if let Err(e) = worker.tx.send(Message::Request(a.clone())).await {
4712                        error!("Error sending quit message to worker: {e}");
4713                    }
4714                    worker
4715                        .handle
4716                        .await
4717                        .unwrap_or_else(|e| error!("Error waiting for worker to quit: {e}"));
4718                }
4719
4720                if let Some(worker) = self.hw_worker.take() {
4721                    if let Some(hw) = &self.hw_driver {
4722                        hw.lock().request_stop();
4723                    }
4724                    let mut panic_events = self.note_off_events_for_all_active_tracks();
4725                    panic_events.extend(self.panic_events_for_all_hw_midi_outputs());
4726                    if !panic_events.is_empty() {
4727                        if let Err(e) = worker.tx.send(Message::ClearHWMidiOutEvents).await {
4728                            error!("Error clearing HW MIDI queue during quit {e}");
4729                        }
4730                        self.midi_hub
4731                            .lock()
4732                            .write_events_blocking(&panic_events, Duration::from_millis(250));
4733                    }
4734                    if let Err(e) = worker.tx.send(Message::Request(a.clone())).await {
4735                        error!("Error sending quit message to HW worker: {e}");
4736                    }
4737                    worker
4738                        .handle
4739                        .await
4740                        .unwrap_or_else(|e| error!("Error waiting for HW worker to quit: {e}"));
4741                }
4742                #[cfg(unix)]
4743                {
4744                    self.jack_runtime = None;
4745                }
4746                self.osc_server = None;
4747                return;
4748            }
4749            Action::AddTrack {
4750                ref name,
4751                audio_ins,
4752                midi_ins,
4753                audio_outs,
4754                midi_outs,
4755            } => {
4756                let tracks = &mut self.state.lock().tracks;
4757                if tracks.contains_key(name) {
4758                    self.notify_clients(Err(format!("Track {} already exists", name)))
4759                        .await;
4760                    return;
4761                }
4762                let maybe_hw = if let Some(oss) = &self.hw_driver {
4763                    let hw = oss.lock();
4764                    Some((hw.cycle_samples(), hw.sample_rate() as f64))
4765                } else {
4766                    #[cfg(unix)]
4767                    if let Some(jack) = &self.jack_runtime {
4768                        let j = jack.lock();
4769                        Some((j.buffer_size, j.sample_rate as f64))
4770                    } else {
4771                        None
4772                    }
4773                    #[cfg(not(unix))]
4774                    None
4775                };
4776
4777                if let Some((chsamples, sample_rate)) = maybe_hw {
4778                    tracks.insert(
4779                        name.clone(),
4780                        Arc::new(UnsafeMutex::new(Box::new(Track::new(
4781                            name.clone(),
4782                            audio_ins,
4783                            audio_outs,
4784                            midi_ins,
4785                            midi_outs,
4786                            chsamples,
4787                            sample_rate,
4788                        )))),
4789                    );
4790                    if let Some(track) = tracks.get(name) {
4791                        let t = track.lock();
4792                        t.ensure_default_audio_passthrough();
4793                        t.ensure_default_midi_passthrough();
4794                        t.set_clip_playback_enabled(self.clip_playback_enabled);
4795                        t.set_transport_timing(self.tempo_bpm, self.tsig_num, self.tsig_denom);
4796                        t.set_session_base_dir(self.session_dir.clone());
4797                        t.set_hybrid_enabled(self.hybrid_enabled);
4798                    }
4799                } else {
4800                    self.notify_clients(Err(
4801                        "Engine needs to open audio device before adding audio track".to_string(),
4802                    ))
4803                    .await;
4804                }
4805            }
4806            Action::TrackAddAudioInput(ref name) => {
4807                let track = match self.track_handle_or_err(name) {
4808                    Ok(track) => track,
4809                    Err(e) => {
4810                        self.notify_clients(Err(e)).await;
4811                        return;
4812                    }
4813                };
4814                if let Err(e) = track.lock().add_audio_input() {
4815                    self.notify_clients(Err(e)).await;
4816                    return;
4817                }
4818            }
4819            Action::TrackAddAudioOutput(ref name) => {
4820                let track = match self.track_handle_or_err(name) {
4821                    Ok(track) => track,
4822                    Err(e) => {
4823                        self.notify_clients(Err(e)).await;
4824                        return;
4825                    }
4826                };
4827                if let Err(e) = track.lock().add_audio_output() {
4828                    self.notify_clients(Err(e)).await;
4829                    return;
4830                }
4831            }
4832            Action::TrackRemoveAudioInput(ref name) => {
4833                let track = match self.track_handle_or_err(name) {
4834                    Ok(track) => track,
4835                    Err(e) => {
4836                        self.notify_clients(Err(e)).await;
4837                        return;
4838                    }
4839                };
4840                if let Err(e) = track.lock().remove_audio_input() {
4841                    self.notify_clients(Err(e)).await;
4842                    return;
4843                }
4844            }
4845            Action::TrackRemoveAudioOutput(ref name) => {
4846                let track = match self.track_handle_or_err(name) {
4847                    Ok(track) => track,
4848                    Err(e) => {
4849                        self.notify_clients(Err(e)).await;
4850                        return;
4851                    }
4852                };
4853                let (hw_outputs, track_inputs) = {
4854                    let state = self.state.lock();
4855                    let hw_outputs = self.all_hw_output_audio_ports();
4856                    let track_inputs = state
4857                        .tracks
4858                        .iter()
4859                        .filter(|(track_name, _)| *track_name != name)
4860                        .flat_map(|(_, handle)| handle.lock().audio.ins.clone())
4861                        .collect::<Vec<_>>();
4862                    (hw_outputs, track_inputs)
4863                };
4864                if let Err(e) = track.lock().remove_audio_output(&hw_outputs, &track_inputs) {
4865                    self.notify_clients(Err(e)).await;
4866                    return;
4867                }
4868            }
4869            Action::RenameTrack {
4870                ref old_name,
4871                ref new_name,
4872            } => {
4873                if self.state.lock().tracks.contains_key(new_name) {
4874                    self.notify_clients(Err(format!("Track '{}' already exists", new_name)))
4875                        .await;
4876                    return;
4877                }
4878
4879                let Some(track) = self.state.lock().tracks.remove(old_name) else {
4880                    self.notify_clients(Err(format!("Track '{}' not found", old_name)))
4881                        .await;
4882                    return;
4883                };
4884
4885                track.lock().name = new_name.clone();
4886                self.state.lock().tracks.insert(new_name.clone(), track);
4887                for other in self.state.lock().tracks.values() {
4888                    let other = other.lock();
4889                    if other.vca_master.as_deref() == Some(old_name.as_str()) {
4890                        other.set_vca_master(Some(new_name.clone()));
4891                    }
4892                    if other.parent_track.as_deref() == Some(old_name.as_str()) {
4893                        other.parent_track = Some(new_name.clone());
4894                    }
4895                }
4896
4897                if let Some(recording) = self.audio_recordings.remove(old_name) {
4898                    self.audio_recordings.insert(new_name.clone(), recording);
4899                }
4900                if let Some(recording) = self.midi_recordings.remove(old_name) {
4901                    self.midi_recordings.insert(new_name.clone(), recording);
4902                }
4903
4904                for route in &mut self.midi_hw_in_routes {
4905                    if route.to_track == *old_name {
4906                        route.to_track = new_name.clone();
4907                    }
4908                }
4909                for route in &mut self.midi_hw_out_routes {
4910                    if route.from_track == *old_name {
4911                        route.from_track = new_name.clone();
4912                    }
4913                }
4914                if let Some((armed_track, target, device)) = self.pending_midi_learn.clone()
4915                    && armed_track == *old_name
4916                {
4917                    self.pending_midi_learn = Some((new_name.clone(), target, device));
4918                }
4919
4920                self.notify_clients(Ok(Action::RenameTrack {
4921                    old_name: old_name.clone(),
4922                    new_name: new_name.clone(),
4923                }))
4924                .await;
4925            }
4926            Action::RemoveTrack(ref name) => {
4927                let children: Vec<String> = {
4928                    let state = self.state.lock();
4929                    state
4930                        .tracks
4931                        .iter()
4932                        .filter_map(|(n, t)| {
4933                            if t.lock().parent_track.as_deref() == Some(name.as_str()) {
4934                                Some(n.clone())
4935                            } else {
4936                                None
4937                            }
4938                        })
4939                        .collect()
4940                };
4941                if let Some(removed_track) = self.state.lock().tracks.get(name).cloned() {
4942                    for child_name in children {
4943                        if let Some(child) = self.state.lock().tracks.get(&child_name).cloned() {
4944                            let removed = removed_track.lock();
4945                            child.lock().disconnect_outputs_from_parent(removed);
4946                            child.lock().parent_track = None;
4947                        }
4948                    }
4949                }
4950                self.state.lock().tracks.remove(name);
4951                self.audio_recordings.remove(name);
4952                self.midi_recordings.remove(name);
4953                self.midi_hw_in_routes.retain(|r| r.to_track != *name);
4954                self.midi_hw_out_routes.retain(|r| r.from_track != *name);
4955                if self
4956                    .pending_midi_learn
4957                    .as_ref()
4958                    .is_some_and(|(track_name, _, _)| track_name == name)
4959                {
4960                    self.pending_midi_learn = None;
4961                }
4962                for track in self.state.lock().tracks.values() {
4963                    let track = track.lock();
4964                    if track.vca_master.as_deref() == Some(name.as_str()) {
4965                        track.set_vca_master(None);
4966                    }
4967                }
4968            }
4969            Action::TrackLevel(ref name, level) => {
4970                if name == "hw:out" {
4971                    self.hw_out_level_db = level;
4972                } else if let Some(track) = self.state.lock().tracks.get(name) {
4973                    let previous = track.lock().level();
4974                    track.lock().set_level(level);
4975                    let delta = level - previous;
4976                    if delta.abs() > f32::EPSILON {
4977                        for follower_name in self.vca_followers(name) {
4978                            if let Some(follower) = self.state.lock().tracks.get(&follower_name) {
4979                                let next = (follower.lock().level() + delta).clamp(-90.0, 20.0);
4980                                follower.lock().set_level(next);
4981                                self.notify_clients(Ok(Action::TrackLevel(
4982                                    follower_name.clone(),
4983                                    next,
4984                                )))
4985                                .await;
4986                            }
4987                        }
4988                    }
4989                }
4990            }
4991            Action::TrackBalance(ref name, balance) => {
4992                if name == "hw:out" {
4993                    self.hw_out_balance = balance.clamp(-1.0, 1.0);
4994                } else if let Some(track) = self.state.lock().tracks.get(name) {
4995                    track.lock().set_balance(balance);
4996                }
4997            }
4998            Action::TrackAutomationLevel(ref name, level) => {
4999                if let Some(track) = self.state.lock().tracks.get(name) {
5000                    let previous = track.lock().level();
5001                    track.lock().set_level(level);
5002                    let delta = level - previous;
5003                    if delta.abs() > f32::EPSILON {
5004                        for follower_name in self.vca_followers(name) {
5005                            if let Some(follower) = self.state.lock().tracks.get(&follower_name) {
5006                                let next = (follower.lock().level() + delta).clamp(-90.0, 20.0);
5007                                follower.lock().set_level(next);
5008                                self.notify_clients(Ok(Action::TrackAutomationLevel(
5009                                    follower_name.clone(),
5010                                    next,
5011                                )))
5012                                .await;
5013                            }
5014                        }
5015                    }
5016                }
5017            }
5018            Action::TrackAutomationBalance(ref name, balance) => {
5019                if let Some(track) = self.state.lock().tracks.get(name) {
5020                    track.lock().set_balance(balance);
5021                }
5022            }
5023            Action::TrackAutomationMute(ref name, muted) => {
5024                if let Some(track) = self.state.lock().tracks.get(name) {
5025                    track.lock().set_muted(muted);
5026                    for follower_name in self.vca_followers(name) {
5027                        if let Some(follower) = self.state.lock().tracks.get(&follower_name) {
5028                            follower.lock().set_muted(muted);
5029                            self.notify_clients(Ok(Action::TrackAutomationMute(
5030                                follower_name.clone(),
5031                                muted,
5032                            )))
5033                            .await;
5034                        }
5035                    }
5036                }
5037            }
5038            Action::RequestMeterSnapshot => {
5039                self.notify_clients(Ok(Action::MeterSnapshot {
5040                    hw_out_db: self.latest_hw_out_meter_db.clone(),
5041                    track_meters: self.latest_track_meter_snapshot.clone(),
5042                }))
5043                .await;
5044                return;
5045            }
5046            Action::TrackMeters { .. } => {}
5047            Action::MeterSnapshot { .. } => {}
5048            Action::TrackToggleArm(ref name) => {
5049                if self.reject_if_track_frozen(name, "arming/disarming").await {
5050                    return;
5051                }
5052                if let Some(track) = self.state.lock().tracks.get(name).cloned() {
5053                    track.lock().arm();
5054                    let armed = track.lock().armed;
5055                    if !armed && self.audio_recordings.contains_key(name) {
5056                        self.flush_track_recording(name).await;
5057                    }
5058                } else {
5059                    tracing::warn!(
5060                        "TrackToggleArm for '{}' but track not found in engine",
5061                        name
5062                    );
5063                }
5064            }
5065            Action::TrackToggleMute(ref name) => {
5066                if name == "hw:out" {
5067                    self.hw_out_muted = !self.hw_out_muted;
5068                } else if let Some(track) = self.state.lock().tracks.get(name) {
5069                    track.lock().mute();
5070                    let muted = track.lock().muted;
5071                    for follower_name in self.vca_followers(name) {
5072                        if let Some(follower) = self.state.lock().tracks.get(&follower_name)
5073                            && follower.lock().muted != muted
5074                        {
5075                            follower.lock().set_muted(muted);
5076                            self.notify_clients(Ok(Action::TrackToggleMute(follower_name.clone())))
5077                                .await;
5078                        }
5079                    }
5080                }
5081            }
5082            Action::TrackTogglePhase(ref name) => {
5083                if let Some(track) = self.state.lock().tracks.get(name) {
5084                    track.lock().invert_phase();
5085                }
5086            }
5087            Action::TrackToggleSolo(ref name) => {
5088                if name == "hw:out" {
5089                    return;
5090                }
5091                if let Some(track) = self.state.lock().tracks.get(name) {
5092                    track.lock().solo();
5093                    let soloed = track.lock().soloed;
5094                    for follower_name in self.vca_followers(name) {
5095                        if let Some(follower) = self.state.lock().tracks.get(&follower_name)
5096                            && follower.lock().soloed != soloed
5097                        {
5098                            follower.lock().solo();
5099                            self.notify_clients(Ok(Action::TrackToggleSolo(follower_name.clone())))
5100                                .await;
5101                        }
5102                    }
5103                }
5104            }
5105            Action::TrackToggleMaster(ref name) => {
5106                if let Some(track) = self.state.lock().tracks.get(name) {
5107                    let blocked = {
5108                        let t = track.lock();
5109                        t.vca_master.is_some() || !self.vca_followers(name).is_empty()
5110                    };
5111                    if blocked {
5112                        self.notify_clients(Err(format!(
5113                            "Track '{}' cannot be promoted to Master while part of a VCA group",
5114                            name
5115                        )))
5116                        .await;
5117                        return;
5118                    }
5119                    track.lock().toggle_master();
5120                }
5121            }
5122            Action::TrackToggleInputMonitor(ref name) => {
5123                if let Some(track) = self.state.lock().tracks.get(name) {
5124                    track.lock().toggle_input_monitor();
5125                }
5126            }
5127            Action::TrackToggleDiskMonitor(ref name) => {
5128                if let Some(track) = self.state.lock().tracks.get(name) {
5129                    track.lock().toggle_disk_monitor();
5130                }
5131            }
5132            Action::TrackSetColor {
5133                ref track_name,
5134                color,
5135            } => {
5136                if let Some(track) = self.state.lock().tracks.get(track_name) {
5137                    track.lock().color = color;
5138                }
5139            }
5140            Action::TrackArmMidiLearn {
5141                ref track_name,
5142                target,
5143            } => {
5144                if let Err(e) = self.track_handle_or_err(track_name) {
5145                    self.notify_clients(Err(e)).await;
5146                    return;
5147                }
5148                self.pending_midi_learn = Some((track_name.clone(), target, None));
5149            }
5150            Action::GlobalArmMidiLearn { target } => {
5151                self.pending_global_midi_learn = Some(target);
5152            }
5153            Action::TrackSetMidiLearnBinding {
5154                ref track_name,
5155                target,
5156                ref binding,
5157            } => {
5158                if let Some(binding) = binding.as_ref() {
5159                    let conflicts = self.midi_learn_slot_conflicts(
5160                        binding,
5161                        Some(MidiLearnSlot::Track(track_name.clone(), target)),
5162                    );
5163                    if !conflicts.is_empty() {
5164                        self.notify_clients(Err(format!(
5165                            "MIDI learn conflict for '{}' {:?}: {}",
5166                            track_name,
5167                            target,
5168                            conflicts.join(", ")
5169                        )))
5170                        .await;
5171                        return;
5172                    }
5173                }
5174                let track = match self.track_handle_or_err(track_name) {
5175                    Ok(track) => track,
5176                    Err(e) => {
5177                        self.notify_clients(Err(e)).await;
5178                        return;
5179                    }
5180                };
5181                match target {
5182                    crate::message::TrackMidiLearnTarget::Volume => {
5183                        track.lock().midi_learn_volume = binding.clone();
5184                    }
5185                    crate::message::TrackMidiLearnTarget::Balance => {
5186                        track.lock().midi_learn_balance = binding.clone();
5187                    }
5188                    crate::message::TrackMidiLearnTarget::Mute => {
5189                        track.lock().midi_learn_mute = binding.clone();
5190                    }
5191                    crate::message::TrackMidiLearnTarget::Solo => {
5192                        track.lock().midi_learn_solo = binding.clone();
5193                    }
5194                    crate::message::TrackMidiLearnTarget::Arm => {
5195                        track.lock().midi_learn_arm = binding.clone();
5196                    }
5197                    crate::message::TrackMidiLearnTarget::InputMonitor => {
5198                        track.lock().midi_learn_input_monitor = binding.clone();
5199                    }
5200                    crate::message::TrackMidiLearnTarget::DiskMonitor => {
5201                        track.lock().midi_learn_disk_monitor = binding.clone();
5202                    }
5203                }
5204            }
5205            Action::SetGlobalMidiLearnBinding {
5206                target,
5207                ref binding,
5208            } => {
5209                if let Some(binding) = binding.as_ref() {
5210                    let conflicts = self
5211                        .midi_learn_slot_conflicts(binding, Some(MidiLearnSlot::Global(target)));
5212                    if !conflicts.is_empty() {
5213                        self.notify_clients(Err(format!(
5214                            "Global MIDI learn conflict for {:?}: {}",
5215                            target,
5216                            conflicts.join(", ")
5217                        )))
5218                        .await;
5219                        return;
5220                    }
5221                }
5222                match target {
5223                    crate::message::GlobalMidiLearnTarget::PlayPause => {
5224                        self.global_midi_learn_play_pause = binding.clone();
5225                    }
5226                    crate::message::GlobalMidiLearnTarget::Stop => {
5227                        self.global_midi_learn_stop = binding.clone();
5228                    }
5229                    crate::message::GlobalMidiLearnTarget::RecordToggle => {
5230                        self.global_midi_learn_record_toggle = binding.clone();
5231                    }
5232                }
5233            }
5234            Action::TrackSetVcaMaster {
5235                ref track_name,
5236                ref master_track,
5237            } => {
5238                let track = match self.track_handle_or_err(track_name) {
5239                    Ok(track) => track,
5240                    Err(e) => {
5241                        self.notify_clients(Err(e)).await;
5242                        return;
5243                    }
5244                };
5245                if track.lock().is_master {
5246                    self.notify_clients(Err(format!(
5247                        "Master track '{}' cannot be part of a VCA group",
5248                        track_name
5249                    )))
5250                    .await;
5251                    return;
5252                }
5253                if let Some(master_name) = master_track
5254                    && master_name == track_name
5255                {
5256                    self.notify_clients(Err("Track cannot be its own VCA master".to_string()))
5257                        .await;
5258                    return;
5259                }
5260                if let Some(master_name) = master_track
5261                    && let Some(master) = self.state.lock().tracks.get(master_name)
5262                    && master.lock().is_master
5263                {
5264                    self.notify_clients(Err(format!(
5265                        "Track '{}' cannot be grouped to Master track '{}'",
5266                        track_name, master_name
5267                    )))
5268                    .await;
5269                    return;
5270                }
5271                track.lock().set_vca_master(master_track.clone());
5272            }
5273            Action::TrackSetFolder {
5274                ref track_name,
5275                is_folder,
5276            } => {
5277                let track = match self.track_handle_or_err(track_name) {
5278                    Ok(track) => track,
5279                    Err(e) => {
5280                        self.notify_clients(Err(e)).await;
5281                        return;
5282                    }
5283                };
5284                track.lock().is_folder = is_folder;
5285                self.notify_clients(Ok(Action::TrackSetFolder {
5286                    track_name: track_name.clone(),
5287                    is_folder,
5288                }))
5289                .await;
5290            }
5291            Action::TrackSetParent {
5292                ref track_name,
5293                ref parent_name,
5294            } => {
5295                let track = match self.track_handle_or_err(track_name) {
5296                    Ok(track) => track,
5297                    Err(e) => {
5298                        self.notify_clients(Err(e)).await;
5299                        return;
5300                    }
5301                };
5302                if parent_name.as_deref() == Some(track_name.as_str()) {
5303                    self.notify_clients(Err("Track cannot be its own parent".to_string()))
5304                        .await;
5305                    return;
5306                }
5307
5308                let old_parent = {
5309                    let t = track.lock();
5310                    t.parent_track.clone()
5311                };
5312                if let Some(ref old) = old_parent
5313                    && let Some(old_track_arc) = self.state.lock().tracks.get(old).cloned()
5314                {
5315                    let old_track = old_track_arc.lock();
5316                    track.lock().disconnect_outputs_from_parent(old_track);
5317                }
5318
5319                if let Some(new_parent) = parent_name
5320                    && let Some(parent_track_arc) =
5321                        self.state.lock().tracks.get(new_parent).cloned()
5322                {
5323                    let parent_track = parent_track_arc.lock();
5324                    track.lock().connect_outputs_to_parent(parent_track);
5325                }
5326                track.lock().parent_track = parent_name.clone();
5327                self.notify_clients(Ok(Action::TrackSetParent {
5328                    track_name: track_name.clone(),
5329                    parent_name: parent_name.clone(),
5330                }))
5331                .await;
5332            }
5333            Action::TrackToggleFolder { ref track_name } => {
5334                let track = match self.track_handle_or_err(track_name) {
5335                    Ok(track) => track,
5336                    Err(e) => {
5337                        self.notify_clients(Err(e)).await;
5338                        return;
5339                    }
5340                };
5341                {
5342                    let t = track.lock();
5343                    t.folder_open = !t.folder_open;
5344                }
5345                self.notify_clients(Ok(Action::TrackToggleFolder {
5346                    track_name: track_name.clone(),
5347                }))
5348                .await;
5349
5350                self.notify_clients(Ok(Action::TrackSetFolder {
5351                    track_name: track_name.clone(),
5352                    is_folder: track.lock().is_folder,
5353                }))
5354                .await;
5355            }
5356            Action::TrackSetMidiLaneChannel {
5357                ref track_name,
5358                lane,
5359                channel,
5360            } => {
5361                let track = match self.track_handle_or_err(track_name) {
5362                    Ok(track) => track,
5363                    Err(e) => {
5364                        self.notify_clients(Err(e)).await;
5365                        return;
5366                    }
5367                };
5368                track.lock().set_midi_lane_channel(lane, channel);
5369            }
5370            Action::TrackSetFrozen {
5371                ref track_name,
5372                frozen,
5373            } => {
5374                let track = match self.track_handle_or_err(track_name) {
5375                    Ok(track) => track,
5376                    Err(e) => {
5377                        self.notify_clients(Err(e)).await;
5378                        return;
5379                    }
5380                };
5381                track.lock().set_frozen(frozen);
5382            }
5383            Action::TrackOfflineBounce {
5384                track_name,
5385                output_path,
5386                start_sample,
5387                length_samples,
5388                automation_lanes,
5389                apply_fader,
5390            } => {
5391                if self.offline_bounce_jobs.contains_key(&track_name) {
5392                    self.notify_clients(Err(format!(
5393                        "Offline bounce for track '{}' is already in progress",
5394                        track_name
5395                    )))
5396                    .await;
5397                    return;
5398                }
5399                if let Err(e) = self.track_handle_or_err(&track_name) {
5400                    self.notify_clients(Err(e)).await;
5401                    return;
5402                }
5403                if length_samples == 0 {
5404                    self.notify_clients(Err(format!(
5405                        "Track '{}' has no renderable content for offline bounce",
5406                        track_name
5407                    )))
5408                    .await;
5409                    return;
5410                }
5411                let Some(worker_index) = self.take_ready_worker_index(WorkerClass::Refill) else {
5412                    self.pending_requests
5413                        .push_front(Action::TrackOfflineBounce {
5414                            track_name,
5415                            output_path,
5416                            start_sample,
5417                            length_samples,
5418                            automation_lanes,
5419                            apply_fader,
5420                        });
5421                    return;
5422                };
5423                let cancel = Arc::new(AtomicBool::new(false));
5424                self.offline_bounce_jobs.insert(
5425                    track_name.clone(),
5426                    OfflineBounceJob {
5427                        cancel: cancel.clone(),
5428                    },
5429                );
5430                let track_name_clone = track_name.clone();
5431                let worker = &self.workers[worker_index];
5432                let job = crate::message::OfflineBounceWork {
5433                    state: self.state.clone(),
5434                    track_name,
5435                    output_path,
5436                    start_sample,
5437                    length_samples,
5438                    tempo_bpm: self.tempo_bpm,
5439                    tsig_num: self.tsig_num,
5440                    tsig_denom: self.tsig_denom,
5441                    automation_lanes,
5442                    cancel,
5443                    apply_fader,
5444                };
5445                if let Err(e) = worker.tx.send(Message::ProcessOfflineBounce(job)).await {
5446                    self.offline_bounce_jobs.remove(&track_name_clone);
5447                    self.notify_clients(Err(format!("Failed to schedule offline bounce: {e}")))
5448                        .await;
5449                }
5450                return;
5451            }
5452            Action::TrackOfflineBounceCancel { .. } => {}
5453            Action::TrackOfflineBounceCancelAll => {}
5454            Action::TrackOfflineBounceCanceled { .. } => {}
5455            Action::TrackOfflineBounceProgress { .. } => {}
5456            Action::PianoKey {
5457                ref track_name,
5458                note,
5459                velocity,
5460                on,
5461            } => {
5462                if let Some(track) = self.state.lock().tracks.get(track_name) {
5463                    let status = if on { 0x90 } else { 0x80 };
5464                    let event = MidiEvent::new(0, vec![status, note.min(127), velocity.min(127)]);
5465                    track.lock().push_hw_midi_events(&[event]);
5466                }
5467            }
5468            Action::ModifyMidiNotes { .. }
5469            | Action::ModifyMidiControllers { .. }
5470            | Action::DeleteMidiControllers { .. }
5471            | Action::InsertMidiControllers { .. }
5472            | Action::DeleteMidiNotes { .. }
5473            | Action::InsertMidiNotes { .. } => {
5474                if let Err(e) = self.apply_midi_edit_action(&action_to_process) {
5475                    self.notify_clients(Err(e)).await;
5476                    return;
5477                }
5478            }
5479            Action::SetMidiSysExEvents { .. } => {
5480                if let Err(e) = self.apply_midi_edit_action(&action_to_process) {
5481                    self.notify_clients(Err(e)).await;
5482                    return;
5483                }
5484            }
5485            Action::TrackClearDefaultPassthrough { ref track_name } => {
5486                if self
5487                    .reject_if_track_frozen(track_name, "plugin graph editing")
5488                    .await
5489                {
5490                    return;
5491                }
5492                let track = match self.track_handle_or_err(track_name) {
5493                    Ok(track) => track,
5494                    Err(e) => {
5495                        self.notify_clients(Err(e)).await;
5496                        return;
5497                    }
5498                };
5499                track.lock().clear_default_passthrough();
5500            }
5501            #[cfg(all(unix, not(target_os = "macos")))]
5502            Action::ClipSetLv2PluginState { ref track_name, .. } => {
5503                self.notify_clients(Err(format!(
5504                    "Track '{}': clip LV2 plugin state changes are not supported",
5505                    track_name
5506                )))
5507                .await;
5508            }
5509            Action::TrackGetClapNoteNames { ref track_name } => {
5510                let track = match self.track_handle_or_err(track_name) {
5511                    Ok(track) => track,
5512                    Err(e) => {
5513                        self.notify_clients(Err(e)).await;
5514                        return;
5515                    }
5516                };
5517                let note_names = track.lock().get_clap_note_names();
5518                self.notify_clients(Ok(Action::TrackClapNoteNames {
5519                    track_name: track_name.clone(),
5520                    note_names,
5521                }))
5522                .await;
5523            }
5524            Action::TrackGetPluginGraph { ref track_name } => {
5525                let track = match self.track_handle_or_err(track_name) {
5526                    Ok(track) => track,
5527                    Err(e) => {
5528                        self.notify_clients(Err(e)).await;
5529                        return;
5530                    }
5531                };
5532                let (plugins, connections) = {
5533                    let track = track.lock();
5534                    (
5535                        track.plugin_graph_plugins(),
5536                        track.plugin_graph_connections(),
5537                    )
5538                };
5539                self.notify_clients(Ok(Action::TrackPluginGraph {
5540                    track_name: track_name.clone(),
5541                    plugins,
5542                    connections,
5543                }))
5544                .await;
5545                return;
5546            }
5547            Action::TrackPluginGraph { .. } => {}
5548            Action::TrackConnectPluginAudio {
5549                ref track_name,
5550                ref from_node,
5551                from_port,
5552                ref to_node,
5553                to_port,
5554            } => {
5555                if self
5556                    .reject_if_track_frozen(track_name, "plugin routing changes")
5557                    .await
5558                {
5559                    return;
5560                }
5561                let track = match self.track_handle_or_err(track_name) {
5562                    Ok(track) => track,
5563                    Err(e) => {
5564                        self.notify_clients(Err(e)).await;
5565                        return;
5566                    }
5567                };
5568                if let Err(e) = track.lock().connect_plugin_audio(
5569                    from_node.clone(),
5570                    from_port,
5571                    to_node.clone(),
5572                    to_port,
5573                ) {
5574                    self.notify_clients(Err(e)).await;
5575                    return;
5576                }
5577            }
5578            Action::TrackConnectPluginMidi {
5579                ref track_name,
5580                ref from_node,
5581                from_port,
5582                ref to_node,
5583                to_port,
5584            } => {
5585                if self
5586                    .reject_if_track_frozen(track_name, "plugin routing changes")
5587                    .await
5588                {
5589                    return;
5590                }
5591                let track = match self.track_handle_or_err(track_name) {
5592                    Ok(track) => track,
5593                    Err(e) => {
5594                        self.notify_clients(Err(e)).await;
5595                        return;
5596                    }
5597                };
5598                if let Err(e) = track.lock().connect_plugin_midi(
5599                    from_node.clone(),
5600                    from_port,
5601                    to_node.clone(),
5602                    to_port,
5603                ) {
5604                    self.notify_clients(Err(e)).await;
5605                    return;
5606                }
5607            }
5608            Action::TrackDisconnectPluginAudio {
5609                ref track_name,
5610                ref from_node,
5611                from_port,
5612                ref to_node,
5613                to_port,
5614            } => {
5615                if self
5616                    .reject_if_track_frozen(track_name, "plugin routing changes")
5617                    .await
5618                {
5619                    return;
5620                }
5621                let track = match self.track_handle_or_err(track_name) {
5622                    Ok(track) => track,
5623                    Err(e) => {
5624                        self.notify_clients(Err(e)).await;
5625                        return;
5626                    }
5627                };
5628                if let Err(e) = track.lock().disconnect_plugin_audio(
5629                    from_node.clone(),
5630                    from_port,
5631                    to_node.clone(),
5632                    to_port,
5633                ) {
5634                    self.notify_clients(Err(e)).await;
5635                    return;
5636                }
5637            }
5638            Action::TrackDisconnectPluginMidi {
5639                ref track_name,
5640                ref from_node,
5641                from_port,
5642                ref to_node,
5643                to_port,
5644            } => {
5645                if self
5646                    .reject_if_track_frozen(track_name, "plugin routing changes")
5647                    .await
5648                {
5649                    return;
5650                }
5651                let track = match self.track_handle_or_err(track_name) {
5652                    Ok(track) => track,
5653                    Err(e) => {
5654                        self.notify_clients(Err(e)).await;
5655                        return;
5656                    }
5657                };
5658                if let Err(e) = track.lock().disconnect_plugin_midi(
5659                    from_node.clone(),
5660                    from_port,
5661                    to_node.clone(),
5662                    to_port,
5663                ) {
5664                    self.notify_clients(Err(e)).await;
5665                    return;
5666                }
5667            }
5668            #[cfg(all(unix, not(target_os = "macos")))]
5669            Action::ListLv2Plugins => {
5670                match crate::plugins::scan_plugins::<crate::plugins::types::Lv2PluginInfo>("lv2") {
5671                    Ok(plugins) => {
5672                        self.notify_clients(Ok(Action::Lv2Plugins(plugins))).await;
5673                    }
5674                    Err(e) => {
5675                        self.notify_clients(Err(e)).await;
5676                    }
5677                }
5678                return;
5679            }
5680            #[cfg(all(unix, not(target_os = "macos")))]
5681            Action::Lv2Plugins(_) => {}
5682            Action::ListVst3Plugins => {
5683                match crate::plugins::scan_plugins::<crate::plugins::types::Vst3PluginInfo>("vst3")
5684                {
5685                    Ok(plugins) => {
5686                        self.notify_clients(Ok(Action::Vst3Plugins(plugins))).await;
5687                    }
5688                    Err(e) => {
5689                        self.notify_clients(Err(e)).await;
5690                    }
5691                }
5692                return;
5693            }
5694            Action::Vst3Plugins(_) => {}
5695            Action::ListClapPlugins => {
5696                match crate::plugins::scan_plugins::<crate::plugins::types::ClapPluginInfo>("clap")
5697                {
5698                    Ok(plugins) => {
5699                        self.notify_clients(Ok(Action::ClapPlugins(plugins))).await;
5700                    }
5701                    Err(e) => {
5702                        self.notify_clients(Err(e)).await;
5703                    }
5704                }
5705                return;
5706            }
5707            Action::ListClapPluginsWithCapabilities => {
5708                match crate::plugins::scan_plugins::<crate::plugins::types::ClapPluginInfo>("clap")
5709                {
5710                    Ok(plugins) => {
5711                        self.notify_clients(Ok(Action::ClapPlugins(plugins))).await;
5712                    }
5713                    Err(e) => {
5714                        self.notify_clients(Err(e)).await;
5715                    }
5716                }
5717                return;
5718            }
5719            Action::ClapPlugins(_) => {}
5720            Action::TrackLoadClapPlugin {
5721                ref track_name,
5722                ref plugin_path,
5723                instance_id,
5724            } => {
5725                if self
5726                    .reject_if_track_frozen(track_name, "CLAP plugin loading")
5727                    .await
5728                {
5729                    return;
5730                }
5731                let track = match self.track_handle_or_err(track_name) {
5732                    Ok(track) => track,
5733                    Err(e) => {
5734                        self.notify_clients(Err(e)).await;
5735                        return;
5736                    }
5737                };
5738                let track = track.lock();
5739                if track.audio.processing {
5740                    self.notify_clients(Err(format!(
5741                        "Track '{}' is currently processing audio; stop playback before loading CLAP plugins",
5742                        track_name
5743                    )))
5744                    .await;
5745                    return;
5746                }
5747                if let Err(e) = track.load_clap_plugin(plugin_path, instance_id) {
5748                    self.notify_clients(Err(e)).await;
5749                    return;
5750                }
5751                self.notify_clients(Ok(Action::Log {
5752                    source: "engine".to_string(),
5753                    message: format!("CLAP plugin loaded on track '{track_name}': {plugin_path}"),
5754                }))
5755                .await;
5756                if let Some(instance) = track.clap_plugins.last()
5757                    && let Some(stderr) = instance.processor.lock().take_stderr()
5758                {
5759                    let source = format!("clap:{plugin_path}");
5760                    self.spawn_plugin_host_stderr_reader(stderr, source);
5761                    self.notify_clients(Ok(Action::Log {
5762                        source: "engine".to_string(),
5763                        message: format!(
5764                            "Attached stderr reader for CLAP plugin on track '{track_name}'"
5765                        ),
5766                    }))
5767                    .await;
5768                }
5769            }
5770            Action::TrackUnloadClapPlugin {
5771                ref track_name,
5772                ref plugin_path,
5773            } => {
5774                if self
5775                    .reject_if_track_frozen(track_name, "CLAP plugin unloading")
5776                    .await
5777                {
5778                    return;
5779                }
5780                let track = match self.track_handle_or_err(track_name) {
5781                    Ok(track) => track,
5782                    Err(e) => {
5783                        self.notify_clients(Err(e)).await;
5784                        return;
5785                    }
5786                };
5787                let track = track.lock();
5788                if track.audio.processing {
5789                    self.notify_clients(Err(format!(
5790                        "Track '{}' is currently processing audio; stop playback before unloading CLAP plugins",
5791                        track_name
5792                    )))
5793                    .await;
5794                    return;
5795                }
5796                if let Err(e) = track.unload_clap_plugin(plugin_path) {
5797                    self.notify_clients(Err(e)).await;
5798                    return;
5799                }
5800            }
5801            Action::TrackUnloadClapPluginInstance {
5802                ref track_name,
5803                instance_id,
5804            } => {
5805                if self
5806                    .reject_if_track_frozen(track_name, "CLAP plugin unloading")
5807                    .await
5808                {
5809                    return;
5810                }
5811                let track = match self.track_handle_or_err(track_name) {
5812                    Ok(track) => track,
5813                    Err(e) => {
5814                        self.notify_clients(Err(e)).await;
5815                        return;
5816                    }
5817                };
5818                let track = track.lock();
5819                if track.audio.processing {
5820                    self.notify_clients(Err(format!(
5821                        "Track '{}' is currently processing audio; stop playback before unloading CLAP plugins",
5822                        track_name
5823                    )))
5824                    .await;
5825                    return;
5826                }
5827                if let Err(e) = track.unload_clap_plugin_instance(instance_id) {
5828                    self.notify_clients(Err(e)).await;
5829                    return;
5830                }
5831            }
5832            Action::TrackShowClapGui {
5833                ref track_name,
5834                instance_id,
5835            } => {
5836                let track = match self.track_handle_or_err(track_name) {
5837                    Ok(track) => track,
5838                    Err(e) => {
5839                        self.notify_clients(Err(e)).await;
5840                        return;
5841                    }
5842                };
5843                if let Err(e) = track.lock().show_clap_gui(instance_id) {
5844                    self.notify_clients(Err(e)).await;
5845                    return;
5846                }
5847            }
5848            Action::TrackLoadVst3Plugin {
5849                ref track_name,
5850                ref plugin_path,
5851                instance_id,
5852            } => {
5853                if self
5854                    .reject_if_track_frozen(track_name, "VST3 plugin loading")
5855                    .await
5856                {
5857                    return;
5858                }
5859                let track = match self.track_handle_or_err(track_name) {
5860                    Ok(track) => track,
5861                    Err(e) => {
5862                        self.notify_clients(Err(e)).await;
5863                        return;
5864                    }
5865                };
5866                let track = track.lock();
5867                if track.audio.processing {
5868                    self.notify_clients(Err(format!(
5869                        "Track '{}' is currently processing audio; stop playback before loading VST3 plugins",
5870                        track_name
5871                    )))
5872                    .await;
5873                    return;
5874                }
5875                if let Err(e) = track.load_vst3_plugin(plugin_path, instance_id) {
5876                    self.notify_clients(Err(e)).await;
5877                    return;
5878                }
5879                if let Some(instance) = track.vst3_plugins.last()
5880                    && let Some(stderr) = instance.processor.lock().take_stderr()
5881                {
5882                    let source = format!("vst3:{plugin_path}");
5883                    self.spawn_plugin_host_stderr_reader(stderr, source);
5884                }
5885            }
5886            Action::TrackUnloadVst3Plugin {
5887                ref track_name,
5888                ref plugin_path,
5889            } => {
5890                if self
5891                    .reject_if_track_frozen(track_name, "VST3 plugin unloading")
5892                    .await
5893                {
5894                    return;
5895                }
5896                let track = match self.track_handle_or_err(track_name) {
5897                    Ok(track) => track,
5898                    Err(e) => {
5899                        self.notify_clients(Err(e)).await;
5900                        return;
5901                    }
5902                };
5903                let track = track.lock();
5904                if track.audio.processing {
5905                    self.notify_clients(Err(format!(
5906                        "Track '{}' is currently processing audio; stop playback before unloading VST3 plugins",
5907                        track_name
5908                    )))
5909                    .await;
5910                    return;
5911                }
5912                if let Err(e) = track.unload_vst3_plugin(plugin_path) {
5913                    self.notify_clients(Err(e)).await;
5914                    return;
5915                }
5916            }
5917            Action::TrackUnloadVst3PluginInstance {
5918                ref track_name,
5919                instance_id,
5920            } => {
5921                if self
5922                    .reject_if_track_frozen(track_name, "VST3 plugin unloading")
5923                    .await
5924                {
5925                    return;
5926                }
5927                let track = match self.track_handle_or_err(track_name) {
5928                    Ok(track) => track,
5929                    Err(e) => {
5930                        self.notify_clients(Err(e)).await;
5931                        return;
5932                    }
5933                };
5934                let track = track.lock();
5935                if track.audio.processing {
5936                    self.notify_clients(Err(format!(
5937                        "Track '{}' is currently processing audio; stop playback before unloading VST3 plugins",
5938                        track_name
5939                    )))
5940                    .await;
5941                    return;
5942                }
5943                if let Err(e) = track.unload_vst3_plugin_instance(instance_id) {
5944                    self.notify_clients(Err(e)).await;
5945                    return;
5946                }
5947            }
5948            Action::TrackShowVst3Gui {
5949                ref track_name,
5950                instance_id,
5951            } => {
5952                let track = match self.track_handle_or_err(track_name) {
5953                    Ok(track) => track,
5954                    Err(e) => {
5955                        self.notify_clients(Err(e)).await;
5956                        return;
5957                    }
5958                };
5959                if let Err(e) = track.lock().show_vst3_gui(instance_id) {
5960                    self.notify_clients(Err(e)).await;
5961                    return;
5962                }
5963            }
5964            #[cfg(all(unix, not(target_os = "macos")))]
5965            Action::TrackLoadLv2Plugin {
5966                ref track_name,
5967                ref plugin_uri,
5968                instance_id,
5969            } => {
5970                if self
5971                    .reject_if_track_frozen(track_name, "LV2 plugin loading")
5972                    .await
5973                {
5974                    return;
5975                }
5976                let track = match self.track_handle_or_err(track_name) {
5977                    Ok(track) => track,
5978                    Err(e) => {
5979                        self.notify_clients(Err(e)).await;
5980                        return;
5981                    }
5982                };
5983                let track = track.lock();
5984                if track.audio.processing {
5985                    self.notify_clients(Err(format!(
5986                        "Track '{}' is currently processing audio; stop playback before loading LV2 plugins",
5987                        track_name
5988                    )))
5989                    .await;
5990                    return;
5991                }
5992                if let Err(e) = track.load_lv2_plugin(plugin_uri, instance_id) {
5993                    self.notify_clients(Err(e)).await;
5994                    return;
5995                }
5996                if let Some(instance) = track.lv2_plugins.last()
5997                    && let Some(stderr) = instance.processor.lock().take_stderr()
5998                {
5999                    let source = format!("lv2:{plugin_uri}");
6000                    self.spawn_plugin_host_stderr_reader(stderr, source);
6001                }
6002            }
6003            #[cfg(all(unix, not(target_os = "macos")))]
6004            Action::TrackUnloadLv2Plugin {
6005                ref track_name,
6006                ref plugin_uri,
6007            } => {
6008                if self
6009                    .reject_if_track_frozen(track_name, "LV2 plugin unloading")
6010                    .await
6011                {
6012                    return;
6013                }
6014                let track = match self.track_handle_or_err(track_name) {
6015                    Ok(track) => track,
6016                    Err(e) => {
6017                        self.notify_clients(Err(e)).await;
6018                        return;
6019                    }
6020                };
6021                let track = track.lock();
6022                if track.audio.processing {
6023                    self.notify_clients(Err(format!(
6024                        "Track '{}' is currently processing audio; stop playback before unloading LV2 plugins",
6025                        track_name
6026                    )))
6027                    .await;
6028                    return;
6029                }
6030                if let Err(e) = track.unload_lv2_plugin(plugin_uri) {
6031                    self.notify_clients(Err(e)).await;
6032                    return;
6033                }
6034            }
6035            #[cfg(all(unix, not(target_os = "macos")))]
6036            Action::TrackUnloadLv2PluginInstance {
6037                ref track_name,
6038                instance_id,
6039            } => {
6040                if self
6041                    .reject_if_track_frozen(track_name, "LV2 plugin unloading")
6042                    .await
6043                {
6044                    return;
6045                }
6046                let track = match self.track_handle_or_err(track_name) {
6047                    Ok(track) => track,
6048                    Err(e) => {
6049                        self.notify_clients(Err(e)).await;
6050                        return;
6051                    }
6052                };
6053                let track = track.lock();
6054                if track.audio.processing {
6055                    self.notify_clients(Err(format!(
6056                        "Track '{}' is currently processing audio; stop playback before unloading LV2 plugins",
6057                        track_name
6058                    )))
6059                    .await;
6060                    return;
6061                }
6062                if let Err(e) = track.unload_lv2_plugin_instance(instance_id) {
6063                    self.notify_clients(Err(e)).await;
6064                    return;
6065                }
6066            }
6067            #[cfg(all(unix, not(target_os = "macos")))]
6068            Action::TrackShowLv2Gui {
6069                ref track_name,
6070                instance_id,
6071            } => {
6072                let track = match self.track_handle_or_err(track_name) {
6073                    Ok(track) => track,
6074                    Err(e) => {
6075                        self.notify_clients(Err(e)).await;
6076                        return;
6077                    }
6078                };
6079                if let Err(e) = track.lock().show_lv2_gui(instance_id) {
6080                    self.notify_clients(Err(e)).await;
6081                    return;
6082                }
6083            }
6084            Action::TrackSetClapParameter {
6085                ref track_name,
6086                instance_id,
6087                param_id,
6088                value,
6089            } => {
6090                if self
6091                    .reject_if_track_frozen(track_name, "CLAP parameter changes")
6092                    .await
6093                {
6094                    return;
6095                }
6096                match self.track_handle_or_err(track_name) {
6097                    Ok(track) => {
6098                        if let Err(e) =
6099                            track
6100                                .lock()
6101                                .set_clap_parameter(instance_id, param_id, value)
6102                        {
6103                            self.notify_clients(Err(e)).await;
6104                            return;
6105                        }
6106                        self.notify_clients(Ok(a.clone())).await;
6107                    }
6108                    Err(e) => {
6109                        self.notify_clients(Err(e)).await;
6110                    }
6111                }
6112            }
6113            Action::ClipSetClapParameter {
6114                ref track_name,
6115                clip_idx,
6116                instance_id,
6117                param_id,
6118                value,
6119            } => {
6120                if self
6121                    .reject_if_track_frozen(track_name, "CLAP parameter changes")
6122                    .await
6123                {
6124                    return;
6125                }
6126                match self.track_handle_or_err(track_name) {
6127                    Ok(track) => {
6128                        if let Err(e) = track.lock().clip_set_clap_parameter(
6129                            clip_idx,
6130                            instance_id,
6131                            param_id,
6132                            value,
6133                        ) {
6134                            self.notify_clients(Err(e)).await;
6135                            return;
6136                        }
6137                        self.notify_clients(Ok(a.clone())).await;
6138                    }
6139                    Err(e) => {
6140                        self.notify_clients(Err(e)).await;
6141                    }
6142                }
6143            }
6144            Action::TrackSetClapParameterAt {
6145                ref track_name,
6146                instance_id,
6147                param_id,
6148                value,
6149                frame,
6150            } => {
6151                if self
6152                    .reject_if_track_frozen(track_name, "CLAP parameter changes")
6153                    .await
6154                {
6155                    return;
6156                }
6157                match self.track_handle_or_err(track_name) {
6158                    Ok(track) => {
6159                        if let Err(e) =
6160                            track
6161                                .lock()
6162                                .set_clap_parameter_at(instance_id, param_id, value, frame)
6163                        {
6164                            self.notify_clients(Err(e)).await;
6165                            return;
6166                        }
6167                        self.notify_clients(Ok(a.clone())).await;
6168                    }
6169                    Err(e) => {
6170                        self.notify_clients(Err(e)).await;
6171                    }
6172                }
6173            }
6174            Action::TrackBeginClapParameterEdit {
6175                ref track_name,
6176                instance_id,
6177                param_id,
6178                frame,
6179            } => {
6180                if self
6181                    .reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
6182                    .await
6183                {
6184                    return;
6185                }
6186                match self.track_handle_or_err(track_name) {
6187                    Ok(track) => {
6188                        if let Err(e) =
6189                            track
6190                                .lock()
6191                                .begin_clap_parameter_edit(instance_id, param_id, frame)
6192                        {
6193                            self.notify_clients(Err(e)).await;
6194                            return;
6195                        }
6196                        self.notify_clients(Ok(a.clone())).await;
6197                    }
6198                    Err(e) => {
6199                        self.notify_clients(Err(e)).await;
6200                    }
6201                }
6202            }
6203            Action::TrackEndClapParameterEdit {
6204                ref track_name,
6205                instance_id,
6206                param_id,
6207                frame,
6208            } => {
6209                if self
6210                    .reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
6211                    .await
6212                {
6213                    return;
6214                }
6215                match self.track_handle_or_err(track_name) {
6216                    Ok(track) => {
6217                        if let Err(e) =
6218                            track
6219                                .lock()
6220                                .end_clap_parameter_edit(instance_id, param_id, frame)
6221                        {
6222                            self.notify_clients(Err(e)).await;
6223                            return;
6224                        }
6225                        self.notify_clients(Ok(a.clone())).await;
6226                    }
6227                    Err(e) => {
6228                        self.notify_clients(Err(e)).await;
6229                    }
6230                }
6231            }
6232            Action::TrackGetClapParameters {
6233                ref track_name,
6234                instance_id,
6235            } => match self.track_handle_or_err(track_name) {
6236                Ok(track) => match track.lock().get_clap_parameters(instance_id) {
6237                    Ok(parameters) => {
6238                        self.notify_clients(Ok(Action::TrackClapParameters {
6239                            track_name: track_name.clone(),
6240                            instance_id,
6241                            parameters,
6242                        }))
6243                        .await;
6244                    }
6245                    Err(e) => {
6246                        self.notify_clients(Err(e)).await;
6247                    }
6248                },
6249                Err(e) => {
6250                    self.notify_clients(Err(e)).await;
6251                }
6252            },
6253            Action::TrackClapParameters { .. } => {}
6254            Action::TrackClapSnapshotState {
6255                ref track_name,
6256                instance_id,
6257            } => match self.track_handle_or_err(track_name) {
6258                Ok(track) => {
6259                    let plugin_path = track
6260                        .lock()
6261                        .clap_plugins
6262                        .iter()
6263                        .find(|instance| instance.id == instance_id)
6264                        .map(|instance| instance.processor.lock().path().to_string())
6265                        .unwrap_or_default();
6266                    match track.lock().clap_snapshot_state(instance_id) {
6267                        Ok(state) => {
6268                            self.notify_clients(Ok(Action::TrackClapStateSnapshot {
6269                                track_name: track_name.clone(),
6270                                instance_id,
6271                                plugin_path,
6272                                state,
6273                            }))
6274                            .await;
6275                        }
6276                        Err(e) => {
6277                            self.notify_clients(Err(e)).await;
6278                        }
6279                    }
6280                }
6281                Err(e) => {
6282                    self.notify_clients(Err(e)).await;
6283                }
6284            },
6285            Action::ClipClapSnapshotState {
6286                ref track_name,
6287                clip_idx,
6288                instance_id,
6289            } => match self.track_handle_or_err(track_name) {
6290                Ok(track) => match track.lock().clip_clap_snapshot_state(clip_idx, instance_id) {
6291                    Ok((plugin_path, state)) => {
6292                        self.notify_clients(Ok(Action::ClipClapStateSnapshot {
6293                            track_name: track_name.clone(),
6294                            clip_idx,
6295                            instance_id,
6296                            plugin_path,
6297                            state,
6298                        }))
6299                        .await;
6300                    }
6301                    Err(e) => {
6302                        self.notify_clients(Err(e)).await;
6303                    }
6304                },
6305                Err(e) => {
6306                    self.notify_clients(Err(e)).await;
6307                }
6308            },
6309            Action::TrackClapStateSnapshot { .. } => {}
6310            Action::ClipClapStateSnapshot { .. } => {}
6311            Action::TrackClapRestoreState {
6312                ref track_name,
6313                instance_id,
6314                ref state,
6315            } => {
6316                if self
6317                    .reject_if_track_frozen(track_name, "CLAP state restore")
6318                    .await
6319                {
6320                    return;
6321                }
6322                let track = match self.track_handle_or_err(track_name) {
6323                    Ok(track) => track,
6324                    Err(e) => {
6325                        self.notify_clients(Err(e)).await;
6326                        return;
6327                    }
6328                };
6329                let track = track.lock();
6330                if track.audio.processing {
6331                    self.notify_clients(Err(format!(
6332                        "Track '{}' is currently processing audio; stop playback before restoring CLAP state",
6333                        track_name
6334                    )))
6335                    .await;
6336                    return;
6337                }
6338                if let Err(e) = track.clap_restore_state(instance_id, state) {
6339                    self.notify_clients(Err(e)).await;
6340                    return;
6341                }
6342            }
6343            Action::ClipClapRestoreState {
6344                ref track_name,
6345                clip_idx,
6346                instance_id,
6347                ref state,
6348            } => {
6349                if self
6350                    .reject_if_track_frozen(track_name, "CLAP state restore")
6351                    .await
6352                {
6353                    return;
6354                }
6355                let track = match self.track_handle_or_err(track_name) {
6356                    Ok(track) => track,
6357                    Err(e) => {
6358                        self.notify_clients(Err(e)).await;
6359                        return;
6360                    }
6361                };
6362                let track = track.lock();
6363                if track.audio.processing {
6364                    self.notify_clients(Err(format!(
6365                        "Track '{}' is currently processing audio; stop playback before restoring CLAP state",
6366                        track_name
6367                    )))
6368                    .await;
6369                    return;
6370                }
6371                if let Err(e) = track.clip_clap_restore_state(clip_idx, instance_id, state) {
6372                    self.notify_clients(Err(e)).await;
6373                    return;
6374                }
6375            }
6376            Action::TrackSnapshotAllClapStates { ref track_name } => {
6377                let track = match self.track_handle_or_err(track_name) {
6378                    Ok(track) => track,
6379                    Err(e) => {
6380                        self.notify_clients(Err(e)).await;
6381                        return;
6382                    }
6383                };
6384                for (instance_id, plugin_path, state) in track.lock().clap_snapshot_all_states() {
6385                    self.notify_clients(Ok(Action::TrackClapStateSnapshot {
6386                        track_name: track_name.clone(),
6387                        instance_id,
6388                        plugin_path,
6389                        state,
6390                    }))
6391                    .await;
6392                }
6393                self.notify_clients(Ok(Action::TrackSnapshotAllClapStatesDone {
6394                    track_name: track_name.clone(),
6395                }))
6396                .await;
6397            }
6398            Action::TrackSnapshotAllClapStatesDone { .. } => {}
6399            Action::TrackGetVst3Graph { ref track_name } => {
6400                match self.track_handle_or_err(track_name) {
6401                    Ok(track) => {
6402                        let t = track.lock();
6403                        let plugins = t.vst3_graph_plugins();
6404                        let connections = t.vst3_graph_connections();
6405                        self.notify_clients(Ok(Action::TrackVst3Graph {
6406                            track_name: track_name.clone(),
6407                            plugins,
6408                            connections,
6409                        }))
6410                        .await;
6411                    }
6412                    Err(e) => {
6413                        self.notify_clients(Err(e)).await;
6414                    }
6415                }
6416            }
6417            Action::TrackVst3Graph { .. } => {}
6418            Action::TrackSetVst3Parameter {
6419                ref track_name,
6420                instance_id,
6421                param_id,
6422                value,
6423            } => {
6424                if self
6425                    .reject_if_track_frozen(track_name, "VST3 parameter changes")
6426                    .await
6427                {
6428                    return;
6429                }
6430                match self.track_handle_or_err(track_name) {
6431                    Ok(track) => {
6432                        if let Err(e) =
6433                            track
6434                                .lock()
6435                                .set_vst3_parameter(instance_id, param_id, value)
6436                        {
6437                            self.notify_clients(Err(e)).await;
6438                            return;
6439                        }
6440                        self.notify_clients(Ok(a.clone())).await;
6441                    }
6442                    Err(e) => {
6443                        self.notify_clients(Err(e)).await;
6444                    }
6445                }
6446            }
6447            Action::TrackSetPluginBypassed {
6448                ref track_name,
6449                instance_id,
6450                ref format,
6451                bypassed,
6452            } => match self.track_handle_or_err(track_name) {
6453                Ok(track) => {
6454                    let result = match format.as_str() {
6455                        "CLAP" => track.lock().set_clap_plugin_bypassed(instance_id, bypassed),
6456                        "VST3" => track.lock().set_vst3_plugin_bypassed(instance_id, bypassed),
6457                        #[cfg(all(unix, not(target_os = "macos")))]
6458                        "LV2" => track.lock().set_lv2_plugin_bypassed(instance_id, bypassed),
6459                        _ => Err(format!("Unknown plugin format for bypass: {format}")),
6460                    };
6461                    if let Err(e) = result {
6462                        self.notify_clients(Err(e)).await;
6463                        return;
6464                    }
6465                    self.notify_clients(Ok(a.clone())).await;
6466                }
6467                Err(e) => {
6468                    self.notify_clients(Err(e)).await;
6469                }
6470            },
6471            Action::TrackGetVst3Parameters {
6472                ref track_name,
6473                instance_id,
6474            } => match self.track_handle_or_err(track_name) {
6475                Ok(track) => match track.lock().get_vst3_parameters(instance_id) {
6476                    Ok(parameters) => {
6477                        self.notify_clients(Ok(Action::TrackVst3Parameters {
6478                            track_name: track_name.clone(),
6479                            instance_id,
6480                            parameters,
6481                        }))
6482                        .await;
6483                    }
6484                    Err(e) => {
6485                        self.notify_clients(Err(e)).await;
6486                    }
6487                },
6488                Err(e) => {
6489                    self.notify_clients(Err(e)).await;
6490                }
6491            },
6492            Action::TrackVst3Parameters { .. } => {}
6493            Action::TrackVst3SnapshotState {
6494                ref track_name,
6495                instance_id,
6496            } => match self.track_handle_or_err(track_name) {
6497                Ok(track) => match track.lock().vst3_snapshot_state(instance_id) {
6498                    Ok(state) => {
6499                        self.notify_clients(Ok(Action::TrackVst3StateSnapshot {
6500                            track_name: track_name.clone(),
6501                            instance_id,
6502                            state,
6503                        }))
6504                        .await;
6505                    }
6506                    Err(e) => {
6507                        self.notify_clients(Err(e)).await;
6508                    }
6509                },
6510                Err(e) => {
6511                    self.notify_clients(Err(e)).await;
6512                }
6513            },
6514            Action::ClipVst3SnapshotState {
6515                ref track_name,
6516                clip_idx,
6517                instance_id,
6518            } => match self.track_handle_or_err(track_name) {
6519                Ok(track) => match track.lock().clip_vst3_snapshot_state(clip_idx, instance_id) {
6520                    Ok(state) => {
6521                        self.notify_clients(Ok(Action::ClipVst3StateSnapshot {
6522                            track_name: track_name.clone(),
6523                            clip_idx,
6524                            instance_id,
6525                            state,
6526                        }))
6527                        .await;
6528                    }
6529                    Err(e) => {
6530                        self.notify_clients(Err(e)).await;
6531                    }
6532                },
6533                Err(e) => {
6534                    self.notify_clients(Err(e)).await;
6535                }
6536            },
6537            Action::TrackVst3StateSnapshot { .. } => {}
6538            Action::ClipVst3StateSnapshot { .. } => {}
6539            Action::TrackVst3RestoreState {
6540                ref track_name,
6541                instance_id,
6542                ref state,
6543            } => match self.track_handle_or_err(track_name) {
6544                Ok(track) => {
6545                    if let Err(e) = track.lock().vst3_restore_state(instance_id, state) {
6546                        self.notify_clients(Err(e)).await;
6547                        return;
6548                    }
6549                    self.notify_clients(Ok(a.clone())).await;
6550                }
6551                Err(e) => {
6552                    self.notify_clients(Err(e)).await;
6553                }
6554            },
6555            Action::TrackConnectVst3Audio {
6556                ref track_name,
6557                ref from_node,
6558                from_port,
6559                ref to_node,
6560                to_port,
6561            } => {
6562                if self
6563                    .reject_if_track_frozen(track_name, "VST3 routing changes")
6564                    .await
6565                {
6566                    return;
6567                }
6568                match self.track_handle_or_err(track_name) {
6569                    Ok(track) => {
6570                        if let Err(e) = track
6571                            .lock()
6572                            .connect_vst3_audio(from_node, from_port, to_node, to_port)
6573                        {
6574                            self.notify_clients(Err(e)).await;
6575                            return;
6576                        }
6577                        self.notify_clients(Ok(a.clone())).await;
6578                    }
6579                    Err(e) => {
6580                        self.notify_clients(Err(e)).await;
6581                    }
6582                }
6583            }
6584            Action::TrackDisconnectVst3Audio {
6585                ref track_name,
6586                ref from_node,
6587                from_port,
6588                ref to_node,
6589                to_port,
6590            } => {
6591                if self
6592                    .reject_if_track_frozen(track_name, "VST3 routing changes")
6593                    .await
6594                {
6595                    return;
6596                }
6597                match self.track_handle_or_err(track_name) {
6598                    Ok(track) => {
6599                        if let Err(e) = track
6600                            .lock()
6601                            .disconnect_vst3_audio(from_node, from_port, to_node, to_port)
6602                        {
6603                            self.notify_clients(Err(e)).await;
6604                            return;
6605                        }
6606                        self.notify_clients(Ok(a.clone())).await;
6607                    }
6608                    Err(e) => {
6609                        self.notify_clients(Err(e)).await;
6610                    }
6611                }
6612            }
6613            Action::ClipMove {
6614                ref kind,
6615                ref from,
6616                ref to,
6617                copy,
6618            } => {
6619                if let Some(from_track_handle) = self.state.lock().tracks.get(&from.track_name)
6620                    && let Some(to_track_handle) = self.state.lock().tracks.get(&to.track_name)
6621                {
6622                    let from_track = from_track_handle.lock();
6623                    let to_track = to_track_handle.lock();
6624                    match kind {
6625                        Kind::Audio => {
6626                            if from.clip_index >= from_track.audio.clips.len() {
6627                                self.notify_clients(Err(format!(
6628                                    "Clip index {} is too high, as track {} has only {} clips!",
6629                                    from.clip_index,
6630                                    from_track.name.clone(),
6631                                    from_track.audio.clips.len(),
6632                                )))
6633                                .await;
6634                                return;
6635                            }
6636                            if from_track.audio.ins.len() != to_track.audio.ins.len() {
6637                                self.notify_clients(Err(format!(
6638                                    "Cannot move/copy audio clip from '{}' ({} inputs) to '{}' ({} inputs)",
6639                                    from_track.name,
6640                                    from_track.audio.ins.len(),
6641                                    to_track.name,
6642                                    to_track.audio.ins.len()
6643                                )))
6644                                .await;
6645                                return;
6646                            }
6647                            let clip_copy = from_track.audio.clips[from.clip_index].clone();
6648                            if !copy {
6649                                from_track.audio.clips.remove(from.clip_index);
6650                            }
6651                            let mut clip_copy = clip_copy;
6652                            clip_copy.start = to.sample_offset;
6653                            let max_lane = to_track.audio.ins.len().saturating_sub(1);
6654                            clip_copy.input_channel = to.input_channel.min(max_lane);
6655                            to_track.audio.clips.push(clip_copy);
6656                        }
6657                        Kind::MIDI => {
6658                            if from.clip_index >= from_track.midi.clips.len() {
6659                                self.notify_clients(Err(format!(
6660                                    "Clip index {} is too high, as track {} has only {} clips!",
6661                                    from.clip_index,
6662                                    from_track.name.clone(),
6663                                    from_track.midi.clips.len(),
6664                                )))
6665                                .await;
6666                                return;
6667                            }
6668                            let clip_copy = from_track.midi.clips[from.clip_index].clone();
6669                            if !copy {
6670                                from_track.midi.clips.remove(from.clip_index);
6671                            }
6672                            let mut clip_copy = clip_copy;
6673                            clip_copy.start = to.sample_offset;
6674                            let max_lane = to_track.midi.ins.len().saturating_sub(1);
6675                            clip_copy.input_channel = to.input_channel.min(max_lane);
6676                            to_track.midi.clips.push(clip_copy);
6677                        }
6678                    }
6679                }
6680            }
6681            Action::AddClip {
6682                ref name,
6683                ref track_name,
6684                start,
6685                length,
6686                offset,
6687                input_channel,
6688                muted,
6689                ref peaks_file,
6690                kind,
6691                fade_enabled,
6692                fade_in_samples,
6693                fade_out_samples,
6694                ref source_name,
6695                source_offset,
6696                source_length,
6697                ref preview_name,
6698                ref pitch_correction_points,
6699                pitch_correction_frame_likeness,
6700                pitch_correction_inertia_ms,
6701                pitch_correction_formant_compensation,
6702                ref plugin_graph_json,
6703            } => {
6704                self.add_clip_to_track(ClipAddRequest {
6705                    name,
6706                    track_name,
6707                    start,
6708                    length,
6709                    offset,
6710                    input_channel,
6711                    muted,
6712                    peaks_file: peaks_file.clone(),
6713                    kind,
6714                    fade_enabled,
6715                    fade_in_samples,
6716                    fade_out_samples,
6717                    source_name: source_name.clone(),
6718                    source_offset,
6719                    source_length,
6720                    preview_name: preview_name.clone(),
6721                    pitch_correction_points: pitch_correction_points.clone(),
6722                    pitch_correction_frame_likeness,
6723                    pitch_correction_inertia_ms,
6724                    pitch_correction_formant_compensation,
6725                    plugin_graph_json: plugin_graph_json.clone(),
6726                });
6727                if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
6728                    let track_name = track_name.clone();
6729                    tokio::task::spawn_blocking(move || {
6730                        track.lock().preload_clips();
6731                        tracing::debug!("Preloaded clips for track '{}' after AddClip", track_name);
6732                    });
6733                }
6734            }
6735            Action::AddGroupedClip {
6736                ref track_name,
6737                kind,
6738                ref audio_clip,
6739                ref midi_clip,
6740            } => {
6741                self.add_grouped_clip_to_track(
6742                    track_name,
6743                    kind,
6744                    audio_clip.clone(),
6745                    midi_clip.clone(),
6746                );
6747                if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
6748                    let track_name = track_name.clone();
6749                    tokio::task::spawn_blocking(move || {
6750                        track.lock().preload_clips();
6751                        tracing::debug!(
6752                            "Preloaded clips for track '{}' after AddGroupedClip",
6753                            track_name
6754                        );
6755                    });
6756                }
6757            }
6758            Action::RemoveClip {
6759                ref track_name,
6760                kind,
6761                ref clip_indices,
6762            } => {
6763                self.remove_clips_from_track(track_name, kind, clip_indices);
6764            }
6765            Action::RenameClip {
6766                ref track_name,
6767                kind,
6768                clip_index,
6769                ref new_name,
6770            } => {
6771                self.rename_clip_references(track_name, kind, clip_index, new_name);
6772            }
6773            Action::SetClipSourceName {
6774                ref track_name,
6775                kind,
6776                clip_index,
6777                ref name,
6778            } => {
6779                self.set_clip_source_name(track_name, clip_index, kind, name.clone());
6780            }
6781            Action::SetClipFade {
6782                ref track_name,
6783                clip_index,
6784                kind,
6785                fade_enabled,
6786                fade_in_samples,
6787                fade_out_samples,
6788            } => {
6789                self.set_clip_fade(
6790                    track_name,
6791                    clip_index,
6792                    kind,
6793                    fade_enabled,
6794                    fade_in_samples,
6795                    fade_out_samples,
6796                );
6797            }
6798            Action::SetClipBounds {
6799                ref track_name,
6800                clip_index,
6801                kind,
6802                start,
6803                length,
6804                offset,
6805            } => {
6806                self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
6807            }
6808            Action::SyncClipBounds {
6809                ref track_name,
6810                clip_index,
6811                kind,
6812                start,
6813                length,
6814                offset,
6815            } => {
6816                self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
6817            }
6818            Action::SetClipMuted {
6819                ref track_name,
6820                clip_index,
6821                kind,
6822                muted,
6823            } => {
6824                self.set_clip_muted(track_name, clip_index, kind, muted);
6825            }
6826            Action::SetClipPluginGraphJson {
6827                ref track_name,
6828                clip_index,
6829                ref plugin_graph_json,
6830            } => {
6831                self.set_clip_plugin_graph_json(track_name, clip_index, plugin_graph_json.clone());
6832            }
6833            Action::SetClipPitchCorrection {
6834                ref track_name,
6835                clip_index,
6836                ref preview_name,
6837                ref source_name,
6838                source_offset,
6839                source_length,
6840                ref pitch_correction_points,
6841                pitch_correction_frame_likeness,
6842                pitch_correction_inertia_ms,
6843                pitch_correction_formant_compensation,
6844            } => {
6845                self.set_clip_pitch_correction(
6846                    track_name,
6847                    clip_index,
6848                    preview_name.clone(),
6849                    source_name.clone(),
6850                    source_offset,
6851                    source_length,
6852                    pitch_correction_points.clone(),
6853                    pitch_correction_frame_likeness,
6854                    pitch_correction_inertia_ms,
6855                    pitch_correction_formant_compensation,
6856                );
6857            }
6858            Action::Connect {
6859                ref from_track,
6860                from_port,
6861                ref to_track,
6862                to_port,
6863                kind,
6864            } => {
6865                match kind {
6866                    Kind::Audio => {
6867                        let from_audio_io = if from_track == "hw:in" {
6868                            self.hw_input_audio_port(from_port)
6869                        } else {
6870                            self.state
6871                                .lock()
6872                                .tracks
6873                                .get(from_track)
6874                                .and_then(|t| t.lock().audio.outs.get(from_port).cloned())
6875                        };
6876                        let to_audio_io = if to_track == "hw:out" {
6877                            self.hw_output_audio_port(to_port)
6878                        } else {
6879                            self.state
6880                                .lock()
6881                                .tracks
6882                                .get(to_track)
6883                                .and_then(|t| t.lock().audio.ins.get(to_port).cloned())
6884                        };
6885                        match (from_audio_io, to_audio_io) {
6886                            (Some(source), Some(target)) => {
6887                                if from_track != "hw:in"
6888                                    && to_track != "hw:out"
6889                                    && self.check_if_leads_to_kind(
6890                                        Kind::Audio,
6891                                        to_track,
6892                                        from_track,
6893                                    )
6894                                {
6895                                    self.notify_clients(Err(
6896                                        "Circular routing is not allowed!".into()
6897                                    ))
6898                                    .await;
6899                                    return;
6900                                }
6901                                crate::audio::io::AudioIO::connect(&source, &target);
6902                            }
6903                            (None, _) => {
6904                                self.notify_clients(Err(format!(
6905                                    "Source track '{}' not found",
6906                                    from_track
6907                                )))
6908                                .await;
6909                                return;
6910                            }
6911                            (_, None) => {
6912                                self.notify_clients(Err(format!(
6913                                    "Destination track '{}' not found",
6914                                    to_track
6915                                )))
6916                                .await;
6917                                return;
6918                            }
6919                        }
6920                    }
6921                    Kind::MIDI => {
6922                        let from_hw_in_device = Self::midi_hw_in_device(from_track);
6923                        let to_hw_out_device = Self::midi_hw_out_device(to_track);
6924                        let from_is_invalid_hw = Self::midi_hw_out_device(from_track).is_some();
6925                        let to_is_invalid_hw = Self::midi_hw_in_device(to_track).is_some();
6926
6927                        if from_is_invalid_hw || to_is_invalid_hw {
6928                            self.notify_clients(Err(
6929                                "Invalid MIDI hardware connection direction".to_string()
6930                            ))
6931                            .await;
6932                            return;
6933                        }
6934
6935                        if from_hw_in_device.is_none()
6936                            && to_hw_out_device.is_none()
6937                            && self.check_if_leads_to_kind(Kind::MIDI, to_track, from_track)
6938                        {
6939                            self.notify_clients(Err("Circular routing is not allowed!".into()))
6940                                .await;
6941                            return;
6942                        }
6943
6944                        let state = self.state.lock();
6945                        let from_track_handle = state.tracks.get(from_track);
6946                        let to_track_handle = state.tracks.get(to_track);
6947
6948                        if let (Some(from_device), Some(to_device)) =
6949                            (from_hw_in_device, to_hw_out_device)
6950                        {
6951                            let route = MidiHwThruRoute {
6952                                from_device: from_device.to_string(),
6953                                to_device: to_device.to_string(),
6954                            };
6955                            if !self.midi_hw_thru_routes.iter().any(|r| r == &route) {
6956                                self.midi_hw_thru_routes.push(route);
6957                            }
6958                        } else if let Some(device) = from_hw_in_device {
6959                            if let Some(t_t) = to_track_handle {
6960                                if t_t.lock().midi.ins.get(to_port).is_none() {
6961                                    self.notify_clients(Err(format!(
6962                                        "MIDI input port {} not found on track '{}'",
6963                                        to_port, to_track
6964                                    )))
6965                                    .await;
6966                                    return;
6967                                }
6968                                let route = MidiHwInRoute {
6969                                    device: device.to_string(),
6970                                    to_track: to_track.to_string(),
6971                                    to_port,
6972                                };
6973                                if !self.midi_hw_in_routes.iter().any(|r| r == &route) {
6974                                    self.midi_hw_in_routes.push(route);
6975                                }
6976                            } else {
6977                                self.notify_clients(Err(format!(
6978                                    "MIDI destination track not found: {}",
6979                                    to_track
6980                                )))
6981                                .await;
6982                                return;
6983                            }
6984                        } else if let Some(device) = to_hw_out_device {
6985                            if let Some(f_t) = from_track_handle {
6986                                if f_t.lock().midi.outs.get(from_port).is_none() {
6987                                    self.notify_clients(Err(format!(
6988                                        "MIDI output port {} not found on track '{}'",
6989                                        from_port, from_track
6990                                    )))
6991                                    .await;
6992                                    return;
6993                                }
6994                                let route = MidiHwOutRoute {
6995                                    from_track: from_track.to_string(),
6996                                    from_port,
6997                                    device: device.to_string(),
6998                                };
6999                                if !self.midi_hw_out_routes.iter().any(|r| r == &route) {
7000                                    self.midi_hw_out_routes.push(route);
7001                                }
7002                            } else {
7003                                self.notify_clients(Err(format!(
7004                                    "MIDI source track not found: {}",
7005                                    from_track
7006                                )))
7007                                .await;
7008                                return;
7009                            }
7010                        } else {
7011                            match (from_track_handle, to_track_handle) {
7012                                (Some(f_t), Some(t_t)) => {
7013                                    let to_in_res = t_t.lock().midi.ins.get(to_port).cloned();
7014                                    if let Some(to_in) = to_in_res {
7015                                        let from_track = f_t.lock();
7016                                        if let Err(e) =
7017                                            from_track.midi.connect_out(from_port, to_in)
7018                                        {
7019                                            self.notify_clients(Err(e)).await;
7020                                            return;
7021                                        }
7022                                        from_track.invalidate_midi_route_cache();
7023                                    } else {
7024                                        self.notify_clients(Err(format!(
7025                                            "MIDI input port {} not found on track '{}'",
7026                                            to_port, to_track
7027                                        )))
7028                                        .await;
7029                                        return;
7030                                    }
7031                                }
7032                                _ => {
7033                                    self.notify_clients(Err(format!(
7034                                        "MIDI tracks not found: {} or {}",
7035                                        from_track, to_track
7036                                    )))
7037                                    .await;
7038                                    return;
7039                                }
7040                            }
7041                        }
7042                    }
7043                };
7044            }
7045            Action::Disconnect {
7046                ref from_track,
7047                from_port,
7048                ref to_track,
7049                to_port,
7050                kind,
7051            } => {
7052                if kind == Kind::Audio {
7053                    if let Err(e) = self.disconnect_audio_route_and_notify(a.clone()).await {
7054                        self.notify_clients(Err(e)).await;
7055                    }
7056                } else if kind == Kind::MIDI {
7057                    let from_hw_in_device = Self::midi_hw_in_device(from_track);
7058                    let to_hw_out_device = Self::midi_hw_out_device(to_track);
7059
7060                    if let (Some(from_device), Some(to_device)) =
7061                        (from_hw_in_device, to_hw_out_device)
7062                    {
7063                        let before = self.midi_hw_thru_routes.len();
7064                        self.midi_hw_thru_routes.retain(|r| {
7065                            !(r.from_device == from_device && r.to_device == to_device)
7066                        });
7067                        if self.midi_hw_thru_routes.len() < before {
7068                            self.notify_clients(Ok(a.clone())).await;
7069                        } else {
7070                            self.notify_clients(Err(format!(
7071                                "Disconnect failed: MIDI route not found ({} -> {})",
7072                                from_track, to_track
7073                            )))
7074                            .await;
7075                        }
7076                        return;
7077                    }
7078
7079                    if let Some(device) = from_hw_in_device {
7080                        let before = self.midi_hw_in_routes.len();
7081                        self.midi_hw_in_routes.retain(|r| {
7082                            !(r.device == device && r.to_track == *to_track && r.to_port == to_port)
7083                        });
7084                        if self.midi_hw_in_routes.len() < before {
7085                            self.notify_clients(Ok(a.clone())).await;
7086                        } else {
7087                            self.notify_clients(Err(format!(
7088                                "Disconnect failed: MIDI route not found ({} -> {})",
7089                                from_track, to_track
7090                            )))
7091                            .await;
7092                        }
7093                        return;
7094                    }
7095
7096                    if let Some(device) = to_hw_out_device {
7097                        let before = self.midi_hw_out_routes.len();
7098                        self.midi_hw_out_routes.retain(|r| {
7099                            !(r.from_track == *from_track
7100                                && r.from_port == from_port
7101                                && r.device == device)
7102                        });
7103                        if self.midi_hw_out_routes.len() < before {
7104                            self.notify_clients(Ok(a.clone())).await;
7105                        } else {
7106                            self.notify_clients(Err(format!(
7107                                "Disconnect failed: MIDI route not found ({} -> {})",
7108                                from_track, to_track
7109                            )))
7110                            .await;
7111                        }
7112                        return;
7113                    }
7114
7115                    let state = self.state.lock();
7116                    if let (Some(f_t), Some(t_t)) =
7117                        (state.tracks.get(from_track), state.tracks.get(to_track))
7118                        && let Some(to_in) = t_t.lock().midi.ins.get(to_port).cloned()
7119                    {
7120                        let from_track = f_t.lock();
7121                        if let Err(e) = from_track.midi.disconnect_out(from_port, &to_in) {
7122                            self.notify_clients(Err(e)).await;
7123                        } else {
7124                            from_track.invalidate_midi_route_cache();
7125                            self.notify_clients(Ok(a.clone())).await;
7126                        }
7127                    } else {
7128                        self.notify_clients(Err(format!(
7129                            "Disconnect failed: MIDI ports not found ({} -> {})",
7130                            from_track, to_track
7131                        )))
7132                        .await;
7133                    }
7134                }
7135            }
7136
7137            Action::OpenAudioDevice {
7138                ref device,
7139                ref input_device,
7140                sample_rate_hz,
7141                bits,
7142                exclusive,
7143                period_frames,
7144                realtime_frames,
7145                low_watermark_frames,
7146                nperiods,
7147                sync_mode,
7148                hybrid_enabled,
7149                actual_period_frames: _,
7150                input_channels: _,
7151                output_channels: _,
7152                bytes_per_frame: _,
7153            } => {
7154                #[cfg(unix)]
7155                {
7156                    let request = AudioOpenRequest {
7157                        device,
7158                        input_device: input_device.as_deref(),
7159                        sample_rate_hz,
7160                        bits,
7161                        exclusive,
7162                        period_frames,
7163                        realtime_frames,
7164                        low_watermark_frames,
7165                        nperiods,
7166                        sync_mode,
7167                        hybrid_enabled,
7168                    };
7169                    if self.maybe_open_jack_runtime(request).await.is_some() {
7170                        return;
7171                    }
7172                }
7173                let hw_period = if hybrid_enabled {
7174                    realtime_frames
7175                } else {
7176                    period_frames
7177                };
7178                let hw_opts = Self::build_hw_options(exclusive, hw_period, nperiods, sync_mode);
7179                self.hybrid_playback_frames = period_frames.max(1);
7180                self.hybrid_realtime_frames = realtime_frames.max(1);
7181                self.hybrid_low_watermark_frames = low_watermark_frames.max(1);
7182                self.hybrid_enabled = hybrid_enabled;
7183                let open_result = self
7184                    .open_non_jack_audio_device(
7185                        device,
7186                        input_device.as_deref(),
7187                        sample_rate_hz,
7188                        bits,
7189                        hw_opts,
7190                    )
7191                    .await;
7192                match open_result {
7193                    Ok(()) => {}
7194                    Err(e) => {
7195                        error!("Failed to open audio device: {e}");
7196                        self.notify_clients(Err(e)).await;
7197                        return;
7198                    }
7199                }
7200                {
7201                    let state = self.state.lock();
7202                    for track in state.tracks.values() {
7203                        track.lock().set_hybrid_enabled(hybrid_enabled);
7204                    }
7205                }
7206                self.finalize_open_audio_device().await;
7207                if let Some(hw) = &self.hw_driver {
7208                    let effective_action = {
7209                        let hw = hw.lock();
7210                        Action::OpenAudioDevice {
7211                            device: device.clone(),
7212                            input_device: input_device.clone(),
7213                            sample_rate_hz: hw.sample_rate(),
7214                            bits: hw.sample_bits(),
7215                            exclusive,
7216                            period_frames,
7217                            realtime_frames,
7218                            low_watermark_frames: low_watermark_frames.max(1),
7219                            nperiods,
7220                            sync_mode,
7221                            hybrid_enabled,
7222                            actual_period_frames: hw.cycle_samples(),
7223                            input_channels: hw.input_channels(),
7224                            output_channels: hw.output_channels(),
7225                            bytes_per_frame: hw.frame_size_bytes(),
7226                        }
7227                    };
7228                    action_to_process = effective_action;
7229                }
7230            }
7231            Action::JackAddAudioInputPort => {
7232                #[cfg(unix)]
7233                {
7234                    if let Some(jack) = self.jack_runtime.clone() {
7235                        let (input_channels, output_channels, rate) = {
7236                            let jack = jack.lock();
7237                            if let Err(e) = jack.add_audio_input_port() {
7238                                self.notify_clients(Err(e)).await;
7239                                return;
7240                            }
7241                            (
7242                                jack.input_channels(),
7243                                jack.output_channels(),
7244                                jack.sample_rate,
7245                            )
7246                        };
7247                        self.publish_hw_infos(input_channels, output_channels, rate)
7248                            .await;
7249                        self.notify_clients(Ok(a.clone())).await;
7250                    } else {
7251                        self.notify_clients(Err(
7252                            "JACK runtime is not active; open the JACK backend first".to_string(),
7253                        ))
7254                        .await;
7255                    }
7256                }
7257                #[cfg(not(unix))]
7258                {
7259                    self.notify_clients(Err(
7260                        "JACK backend is not available on this platform build".to_string(),
7261                    ))
7262                    .await;
7263                }
7264            }
7265            Action::JackRemoveAudioInputPort(_removed_port) => {
7266                #[cfg(unix)]
7267                {
7268                    let removed_port = _removed_port;
7269                    if let Some(jack) = self.jack_runtime.clone() {
7270                        let (removed_port, removed_io) = {
7271                            let jack = jack.lock();
7272                            let removed_port = Some(removed_port);
7273                            let removed_io =
7274                                removed_port.and_then(|port| jack.input_audio_port(port));
7275                            match (removed_port, removed_io) {
7276                                (Some(port), Some(io)) => (port, io),
7277                                _ => {
7278                                    self.notify_clients(Err(
7279                                        "JACK audio input port index is out of range".to_string(),
7280                                    ))
7281                                    .await;
7282                                    return;
7283                                }
7284                            }
7285                        };
7286                        let reindex_notifications =
7287                            self.reindex_notifications_for_removed_hw_input(removed_port);
7288                        for disconnect in
7289                            self.disconnect_actions_for_removed_hw_input(removed_port, &removed_io)
7290                        {
7291                            if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await
7292                            {
7293                                self.notify_clients(Err(e)).await;
7294                                return;
7295                            }
7296                        }
7297                        let (input_channels, output_channels, rate) = {
7298                            let jack = jack.lock();
7299                            if let Err(e) = jack.remove_audio_input_port(removed_port) {
7300                                self.notify_clients(Err(e)).await;
7301                                return;
7302                            }
7303                            (
7304                                jack.input_channels(),
7305                                jack.output_channels(),
7306                                jack.sample_rate,
7307                            )
7308                        };
7309                        for action in reindex_notifications {
7310                            self.notify_clients(Ok(action)).await;
7311                        }
7312                        self.publish_hw_infos(input_channels, output_channels, rate)
7313                            .await;
7314                        self.notify_clients(Ok(a.clone())).await;
7315                    } else {
7316                        self.notify_clients(Err(
7317                            "JACK runtime is not active; open the JACK backend first".to_string(),
7318                        ))
7319                        .await;
7320                    }
7321                }
7322                #[cfg(not(unix))]
7323                {
7324                    self.notify_clients(Err(
7325                        "JACK backend is not available on this platform build".to_string(),
7326                    ))
7327                    .await;
7328                }
7329            }
7330            Action::JackAddAudioOutputPort => {
7331                #[cfg(unix)]
7332                {
7333                    if let Some(jack) = self.jack_runtime.clone() {
7334                        let (input_channels, output_channels, rate) = {
7335                            let jack = jack.lock();
7336                            if let Err(e) = jack.add_audio_output_port() {
7337                                self.notify_clients(Err(e)).await;
7338                                return;
7339                            }
7340                            (
7341                                jack.input_channels(),
7342                                jack.output_channels(),
7343                                jack.sample_rate,
7344                            )
7345                        };
7346                        self.publish_hw_infos(input_channels, output_channels, rate)
7347                            .await;
7348                        self.notify_clients(Ok(a.clone())).await;
7349                    } else {
7350                        self.notify_clients(Err(
7351                            "JACK runtime is not active; open the JACK backend first".to_string(),
7352                        ))
7353                        .await;
7354                    }
7355                }
7356                #[cfg(not(unix))]
7357                {
7358                    self.notify_clients(Err(
7359                        "JACK backend is not available on this platform build".to_string(),
7360                    ))
7361                    .await;
7362                }
7363            }
7364            Action::JackRemoveAudioOutputPort(_removed_port) => {
7365                #[cfg(unix)]
7366                {
7367                    let removed_port = _removed_port;
7368                    if let Some(jack) = self.jack_runtime.clone() {
7369                        let (removed_port, removed_io) = {
7370                            let jack = jack.lock();
7371                            let removed_port = Some(removed_port);
7372                            let removed_io =
7373                                removed_port.and_then(|port| jack.output_audio_port(port));
7374                            match (removed_port, removed_io) {
7375                                (Some(port), Some(io)) => (port, io),
7376                                _ => {
7377                                    self.notify_clients(Err(
7378                                        "JACK audio output port index is out of range".to_string(),
7379                                    ))
7380                                    .await;
7381                                    return;
7382                                }
7383                            }
7384                        };
7385                        let reindex_notifications =
7386                            self.reindex_notifications_for_removed_hw_output(removed_port);
7387                        for disconnect in
7388                            self.disconnect_actions_for_removed_hw_output(removed_port, &removed_io)
7389                        {
7390                            if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await
7391                            {
7392                                self.notify_clients(Err(e)).await;
7393                                return;
7394                            }
7395                        }
7396                        let (input_channels, output_channels, rate) = {
7397                            let jack = jack.lock();
7398                            if let Err(e) = jack.remove_audio_output_port(removed_port) {
7399                                self.notify_clients(Err(e)).await;
7400                                return;
7401                            }
7402                            (
7403                                jack.input_channels(),
7404                                jack.output_channels(),
7405                                jack.sample_rate,
7406                            )
7407                        };
7408                        for action in reindex_notifications {
7409                            self.notify_clients(Ok(action)).await;
7410                        }
7411                        self.publish_hw_infos(input_channels, output_channels, rate)
7412                            .await;
7413                        self.notify_clients(Ok(a.clone())).await;
7414                    } else {
7415                        self.notify_clients(Err(
7416                            "JACK runtime is not active; open the JACK backend first".to_string(),
7417                        ))
7418                        .await;
7419                    }
7420                }
7421                #[cfg(not(unix))]
7422                {
7423                    self.notify_clients(Err(
7424                        "JACK backend is not available on this platform build".to_string(),
7425                    ))
7426                    .await;
7427                }
7428            }
7429            Action::OpenMidiInputDevice(ref device) => {
7430                let midi_hub = self.midi_hub.lock();
7431                if let Err(e) = midi_hub.open_input(device) {
7432                    self.notify_clients(Err(e)).await;
7433                    return;
7434                }
7435            }
7436            Action::OpenMidiOutputDevice(ref device) => {
7437                let midi_hub = self.midi_hub.lock();
7438                if let Err(e) = midi_hub.open_output(device) {
7439                    self.notify_clients(Err(e)).await;
7440                    return;
7441                }
7442            }
7443            Action::RequestSessionDiagnostics => {
7444                let (
7445                    track_count,
7446                    frozen_track_count,
7447                    audio_clip_count,
7448                    midi_clip_count,
7449                    lv2_instance_count,
7450                    vst3_instance_count,
7451                    clap_instance_count,
7452                ) = {
7453                    let tracks = &self.state.lock().tracks;
7454                    let mut track_count = 0usize;
7455                    let mut frozen_track_count = 0usize;
7456                    let mut audio_clip_count = 0usize;
7457                    let mut midi_clip_count = 0usize;
7458                    #[cfg(all(unix, not(target_os = "macos")))]
7459                    let mut lv2_instance_count = 0usize;
7460                    #[cfg(not(all(unix, not(target_os = "macos"))))]
7461                    let lv2_instance_count = 0usize;
7462                    let mut vst3_instance_count = 0usize;
7463                    let mut clap_instance_count = 0usize;
7464                    for track in tracks.values() {
7465                        let t = track.lock();
7466                        track_count += 1;
7467                        if t.frozen {
7468                            frozen_track_count += 1;
7469                        }
7470                        audio_clip_count += t.audio.clips.len();
7471                        midi_clip_count += t.midi.clips.len();
7472                        #[cfg(all(unix, not(target_os = "macos")))]
7473                        {
7474                            lv2_instance_count += t.lv2_plugins.len();
7475                        }
7476                        vst3_instance_count += t.vst3_plugins.len();
7477                        clap_instance_count += t.clap_plugins.len();
7478                    }
7479                    (
7480                        track_count,
7481                        frozen_track_count,
7482                        audio_clip_count,
7483                        midi_clip_count,
7484                        lv2_instance_count,
7485                        vst3_instance_count,
7486                        clap_instance_count,
7487                    )
7488                };
7489                #[cfg(not(all(unix, not(target_os = "macos"))))]
7490                let _lv2_instance_count = lv2_instance_count;
7491                let pending_hw_midi_events = self.pending_hw_midi_events.len()
7492                    + self
7493                        .pending_hw_midi_events_by_device
7494                        .values()
7495                        .map(std::vec::Vec::len)
7496                        .sum::<usize>();
7497                let sample_rate_hz = if let Some(hw) = &self.hw_driver {
7498                    hw.lock().sample_rate() as usize
7499                } else {
7500                    #[cfg(unix)]
7501                    {
7502                        self.jack_runtime
7503                            .as_ref()
7504                            .map(|j| j.lock().sample_rate)
7505                            .unwrap_or(0)
7506                    }
7507                    #[cfg(not(unix))]
7508                    0
7509                };
7510                let cycle_samples = self.current_cycle_samples();
7511                tracing::info!(
7512                    "Hybrid diagnostics: refill_budget_per_pass={}, refill_budget_throttle_count={}, realtime_fallback_dispatch_count={}, realtime_ready={}, refill_ready={}",
7513                    self.refill_budget_per_pass,
7514                    self.refill_budget_throttle_count,
7515                    self.realtime_fallback_dispatch_count,
7516                    self.ready_realtime_workers.len(),
7517                    self.ready_refill_workers.len()
7518                );
7519                self.notify_clients(Ok(Action::SessionDiagnosticsReport {
7520                    track_count,
7521                    frozen_track_count,
7522                    audio_clip_count,
7523                    midi_clip_count,
7524                    #[cfg(all(unix, not(target_os = "macos")))]
7525                    lv2_instance_count,
7526                    vst3_instance_count,
7527                    clap_instance_count,
7528                    pending_requests: self.pending_requests.len(),
7529                    workers_total: self.workers.len(),
7530                    workers_ready: self.ready_realtime_workers.len()
7531                        + self.ready_refill_workers.len(),
7532                    pending_hw_midi_events,
7533                    playing: self.playing,
7534                    transport_sample: self.transport_sample,
7535                    tempo_bpm: self.tempo_bpm,
7536                    sample_rate_hz,
7537                    cycle_samples,
7538                }))
7539                .await;
7540            }
7541            Action::RequestMidiLearnMappingsReport => {
7542                let mut lines = Vec::<String>::new();
7543                let fmt_binding = |b: &crate::message::MidiLearnBinding| {
7544                    let device = b.device.as_deref().unwrap_or("*");
7545                    format!("{device} CH{} CC{}", b.channel + 1, b.cc)
7546                };
7547                if let Some(b) = self.global_midi_learn_play_pause.as_ref() {
7548                    lines.push(format!("Global PlayPause: {}", fmt_binding(b)));
7549                }
7550                if let Some(b) = self.global_midi_learn_stop.as_ref() {
7551                    lines.push(format!("Global Stop: {}", fmt_binding(b)));
7552                }
7553                if let Some(b) = self.global_midi_learn_record_toggle.as_ref() {
7554                    lines.push(format!("Global RecordToggle: {}", fmt_binding(b)));
7555                }
7556                for (track_name, track) in self.state.lock().tracks.iter() {
7557                    let t = track.lock();
7558                    if let Some(b) = t.midi_learn_volume.as_ref() {
7559                        lines.push(format!("{} Volume: {}", track_name, fmt_binding(b)));
7560                    }
7561                    if let Some(b) = t.midi_learn_balance.as_ref() {
7562                        lines.push(format!("{} Balance: {}", track_name, fmt_binding(b)));
7563                    }
7564                    if let Some(b) = t.midi_learn_mute.as_ref() {
7565                        lines.push(format!("{} Mute: {}", track_name, fmt_binding(b)));
7566                    }
7567                    if let Some(b) = t.midi_learn_solo.as_ref() {
7568                        lines.push(format!("{} Solo: {}", track_name, fmt_binding(b)));
7569                    }
7570                    if let Some(b) = t.midi_learn_arm.as_ref() {
7571                        lines.push(format!("{} Arm: {}", track_name, fmt_binding(b)));
7572                    }
7573                    if let Some(b) = t.midi_learn_input_monitor.as_ref() {
7574                        lines.push(format!("{} InputMonitor: {}", track_name, fmt_binding(b)));
7575                    }
7576                    if let Some(b) = t.midi_learn_disk_monitor.as_ref() {
7577                        lines.push(format!("{} DiskMonitor: {}", track_name, fmt_binding(b)));
7578                    }
7579                }
7580                if lines.is_empty() {
7581                    lines.push("No MIDI learn mappings configured".to_string());
7582                }
7583                self.notify_clients(Ok(Action::MidiLearnMappingsReport { lines }))
7584                    .await;
7585            }
7586            Action::ClearAllMidiLearnBindings => {
7587                self.pending_midi_learn = None;
7588                self.pending_global_midi_learn = None;
7589                self.global_midi_learn_play_pause = None;
7590                self.global_midi_learn_stop = None;
7591                self.global_midi_learn_record_toggle = None;
7592                self.midi_cc_gate.clear();
7593                for track in self.state.lock().tracks.values() {
7594                    let t = track.lock();
7595                    t.midi_learn_volume = None;
7596                    t.midi_learn_balance = None;
7597                    t.midi_learn_mute = None;
7598                    t.midi_learn_solo = None;
7599                    t.midi_learn_arm = None;
7600                    t.midi_learn_input_monitor = None;
7601                    t.midi_learn_disk_monitor = None;
7602                }
7603            }
7604            #[cfg(all(unix, not(target_os = "macos")))]
7605            Action::TrackLv2PluginControls { .. } => {}
7606            #[cfg(all(unix, not(target_os = "macos")))]
7607            Action::ClipLv2PluginControls { .. } => {}
7608            #[cfg(all(unix, not(target_os = "macos")))]
7609            Action::TrackLv2Midnam { .. } => {}
7610            Action::TrackClapNoteNames { .. } => {}
7611            Action::SessionDiagnosticsReport { .. } => {}
7612            Action::MidiLearnMappingsReport { .. } => {}
7613            Action::HWInfo { .. } => {}
7614            Action::HistoryState { .. } => {}
7615            Action::Undo => {}
7616            Action::Redo => {}
7617            Action::ApplyGroupedActions(_) => {}
7618            _ => {}
7619        }
7620
7621        if let Some(inverse) = inverse_actions {
7622            if let Some(group) = self.history_group.as_mut() {
7623                group.forward_actions.push(action_to_process.clone());
7624                group.inverse_actions.splice(0..0, inverse);
7625            } else {
7626                self.history.record(UndoEntry {
7627                    forward_actions: vec![action_to_process.clone()],
7628                    inverse_actions: inverse,
7629                });
7630            }
7631        }
7632
7633        self.notify_clients(Ok(action_to_process)).await;
7634    }
7635
7636    pub async fn work(&mut self) {
7637        while let Some(message) = self.rx.recv().await {
7638            match message {
7639                Message::Ready(id) => self.push_ready_worker(id),
7640                Message::Finished {
7641                    worker_id,
7642                    track_name,
7643                    output_linear,
7644                    process_epoch,
7645                    parameter_updates,
7646                } => {
7647                    self.push_ready_worker(worker_id);
7648                    self.track_processing_started_at.remove(&track_name);
7649                    if process_epoch != self.track_process_epoch {
7650                        if let Some(track) = self.state.lock().tracks.get(&track_name).cloned() {
7651                            let t = track.lock();
7652                            t.audio.finished = false;
7653                            t.audio.processing = false;
7654                        }
7655                        continue;
7656                    }
7657                    self.track_meter_linear_by_track
7658                        .insert(track_name, output_linear);
7659                    for action in parameter_updates {
7660                        self.notify_clients(Ok(action)).await;
7661                    }
7662                    self.force_stalled_track_completions();
7663                    let all_finished = self.send_tracks().await;
7664                    if all_finished {
7665                        self.on_all_tracks_finished().await;
7666                    }
7667                }
7668                Message::Channel(s) => {
7669                    self.clients.push(s);
7670                }
7671
7672                Message::Request(a) => match a {
7673                    Action::TrackOfflineBounceCancel { track_name } => {
7674                        if let Some(job) = self.offline_bounce_jobs.get(&track_name) {
7675                            job.cancel.store(true, Ordering::Relaxed);
7676                        }
7677                    }
7678                    Action::TrackOfflineBounceCancelAll => {
7679                        for job in self.offline_bounce_jobs.values() {
7680                            job.cancel.store(true, Ordering::Relaxed);
7681                        }
7682                    }
7683                    _ if !self.offline_bounce_jobs.is_empty() => {
7684                        self.pending_requests.push_back(a);
7685                    }
7686                    Action::OpenAudioDevice { .. }
7687                    | Action::OpenMidiInputDevice(_)
7688                    | Action::OpenMidiOutputDevice(_)
7689                    | Action::RequestMeterSnapshot
7690                    | Action::Quit
7691                    | Action::Log { .. }
7692                    | Action::Play
7693                    | Action::Pause
7694                    | Action::Stop
7695                    | Action::TransportPosition(_)
7696                    | Action::JumpToEnd
7697                    | Action::SetLoopEnabled(_)
7698                    | Action::SetLoopRange(_)
7699                    | Action::SetPunchEnabled(_)
7700                    | Action::SetPunchRange(_)
7701                    | Action::SetMetronomeEnabled(_)
7702                    | Action::SetTempo(_)
7703                    | Action::SetTimeSignature { .. }
7704                    | Action::SetOscEnabled(_)
7705                    | Action::SetClipPlaybackEnabled(_)
7706                    | Action::SetRecordEnabled(_)
7707                    | Action::SetSessionPath(_)
7708                    | Action::ClearHistory
7709                    | Action::BeginSessionRestore
7710                    | Action::PianoKey { .. }
7711                    | Action::ModifyMidiNotes { .. }
7712                    | Action::ModifyMidiControllers { .. }
7713                    | Action::DeleteMidiControllers { .. }
7714                    | Action::InsertMidiControllers { .. }
7715                    | Action::DeleteMidiNotes { .. }
7716                    | Action::InsertMidiNotes { .. }
7717                    | Action::SetMidiSysExEvents { .. } => {
7718                        self.handle_request(a).await;
7719                    }
7720                    #[cfg(all(unix, not(target_os = "macos")))]
7721                    Action::ListLv2Plugins => {
7722                        self.handle_request(a).await;
7723                    }
7724                    Action::ListVst3Plugins => {
7725                        self.handle_request(a).await;
7726                    }
7727                    Action::ListClapPlugins => {
7728                        self.handle_request(a).await;
7729                    }
7730                    Action::ListClapPluginsWithCapabilities => {
7731                        self.handle_request(a).await;
7732                    }
7733                    _ => {
7734                        self.pending_requests.push_back(a);
7735                        if self.can_schedule_hw_cycle() {
7736                            self.request_hw_cycle().await;
7737                        } else {
7738                            while let Some(next) = self.pending_requests.pop_front() {
7739                                self.handle_request(next).await;
7740                            }
7741                        }
7742                    }
7743                },
7744                Message::OfflineBounceFinished { result } => {
7745                    if let Ok(Action::TrackOfflineBounce { track_name, .. }) = &result {
7746                        self.offline_bounce_jobs.remove(track_name);
7747                    }
7748                    self.notify_clients(result).await;
7749                    if self.offline_bounce_jobs.is_empty() {
7750                        while let Some(next) = self.pending_requests.pop_front() {
7751                            self.handle_request(next).await;
7752                        }
7753                    }
7754                }
7755                Message::HWFinished => {
7756                    if !self.awaiting_hwfinished {
7757                        continue;
7758                    }
7759                    self.handling_hwfinished = true;
7760                    self.awaiting_hwfinished = false;
7761                    #[cfg(unix)]
7762                    {
7763                        if let Some(jack) = &self.jack_runtime {
7764                            if !self.pending_hw_midi_out_events.is_empty() {
7765                                let out_events =
7766                                    std::mem::take(&mut self.pending_hw_midi_out_events);
7767                                jack.lock().write_events(&out_events);
7768                            }
7769                            let mut in_events = vec![];
7770                            jack.lock().read_events_into(&mut in_events);
7771                            if !in_events.is_empty() {
7772                                self.pending_hw_midi_events.extend(in_events);
7773                            }
7774                        }
7775                    }
7776                    #[cfg(unix)]
7777                    if self.jack_runtime.is_some() {
7778                        self.sync_from_jack_transport().await;
7779                    }
7780                    while let Some(a) = self.pending_requests.pop_front() {
7781                        self.handle_request(a).await;
7782                    }
7783                    self.apply_mute_solo_policy();
7784                    self.append_recorded_cycle();
7785                    self.flush_completed_recordings().await;
7786                    let hw_in_routes = self.midi_hw_in_routes.clone();
7787                    let pending_hw_in_by_device = self.pending_hw_midi_events_by_device.clone();
7788                    let mut reconfigured_tracks = Vec::new();
7789                    for (track_name, track) in self.state.lock().tracks.iter() {
7790                        let track_lock = track.lock();
7791                        if self.jack_runtime_is_some() {
7792                            if !self.pending_hw_midi_events.is_empty() {
7793                                track_lock.push_hw_midi_events(&self.pending_hw_midi_events);
7794                            }
7795                        } else {
7796                            for route in hw_in_routes.iter().filter(|r| &r.to_track == track_name) {
7797                                if let Some(events) = pending_hw_in_by_device.get(&route.device) {
7798                                    track_lock.push_hw_midi_events_to_port(route.to_port, events);
7799                                }
7800                            }
7801                        }
7802                        if track_lock.setup() {
7803                            reconfigured_tracks.push(track_name.clone());
7804                        }
7805                    }
7806                    self.publish_track_meters().await;
7807                    for track_name in reconfigured_tracks {
7808                        let track = self.state.lock().tracks.get(&track_name).cloned();
7809                        if let Some(track) = track {
7810                            let (plugins, connections) = {
7811                                let track_lock = track.lock();
7812                                (
7813                                    track_lock.plugin_graph_plugins(),
7814                                    track_lock.plugin_graph_connections(),
7815                                )
7816                            };
7817                            self.notify_clients(Ok(Action::TrackPluginGraph {
7818                                track_name: track_name.clone(),
7819                                plugins,
7820                                connections,
7821                            }))
7822                            .await;
7823                        }
7824                    }
7825                    self.pending_hw_midi_events.clear();
7826                    self.pending_hw_midi_events_by_device.clear();
7827                    if self.playing {
7828                        if self.transport_panic_flush_pending {
7829                            self.transport_panic_flush_pending = false;
7830                        } else if self.transport_restart_pending {
7831                            self.transport_restart_pending = false;
7832                        } else {
7833                            let next = self
7834                                .transport_sample
7835                                .saturating_add(self.current_cycle_samples());
7836                            let normalized = self.normalize_transport_sample(next);
7837                            let wrapped = normalized != next;
7838                            self.transport_sample = normalized;
7839                            if wrapped {
7840                                if self.notified_loop_wrap_sample == Some(self.transport_sample) {
7841                                    self.notified_loop_wrap_sample = None;
7842                                } else {
7843                                    self.notify_clients(Ok(Action::TransportPosition(
7844                                        self.transport_sample,
7845                                    )))
7846                                    .await;
7847                                }
7848                            }
7849                        }
7850                    }
7851                    if self.send_tracks().await && self.hw_worker.is_some() {
7852                        self.request_hw_cycle().await;
7853                    }
7854                    #[cfg(unix)]
7855                    {
7856                        if self.jack_runtime.is_some() {
7857                            self.awaiting_hwfinished = true;
7858                        }
7859                    }
7860                    self.handling_hwfinished = false;
7861                }
7862                Message::HWMidiEvents(events) => {
7863                    for hw_event in events {
7864                        let thru_targets: Vec<String> = self
7865                            .midi_hw_thru_routes
7866                            .iter()
7867                            .filter(|route| route.from_device == hw_event.device)
7868                            .map(|route| route.to_device.clone())
7869                            .collect();
7870                        for device in thru_targets {
7871                            self.pending_hw_midi_out_events_by_device.push(HwMidiEvent {
7872                                device,
7873                                event: hw_event.event.clone(),
7874                            });
7875                        }
7876                        if hw_event.event.data.len() >= 3 {
7877                            let status = hw_event.event.data[0];
7878                            if status & 0xF0 == 0xB0 {
7879                                let channel = status & 0x0F;
7880                                let cc = hw_event.event.data[1];
7881                                let value = hw_event.event.data[2];
7882                                self.handle_incoming_hw_cc(&hw_event.device, channel, cc, value)
7883                                    .await;
7884                            }
7885                        }
7886                        self.pending_hw_midi_events_by_device
7887                            .entry(hw_event.device)
7888                            .or_default()
7889                            .push(hw_event.event);
7890                    }
7891                }
7892                _ => {}
7893            }
7894        }
7895    }
7896
7897    fn collect_hw_midi_output_events(&self) -> Vec<MidiEvent> {
7898        let mut events = vec![];
7899        for track in self.state.lock().tracks.values() {
7900            events.extend(
7901                track
7902                    .lock()
7903                    .take_hw_midi_out_events()
7904                    .into_iter()
7905                    .map(|evt| evt.event),
7906            );
7907        }
7908        events.sort_by_key(|a| a.frame);
7909        events
7910    }
7911
7912    fn collect_hw_midi_output_events_by_device(&mut self) -> Vec<HwMidiEvent> {
7913        let mut events = Vec::<HwMidiEvent>::new();
7914        let routes = self.midi_hw_out_routes.clone();
7915        let mut events_by_track = HashMap::<String, Vec<crate::track::HwMidiOutEvent>>::new();
7916        {
7917            let state = self.state.lock();
7918            for route in &routes {
7919                if events_by_track.contains_key(&route.from_track) {
7920                    continue;
7921                }
7922                let Some(track) = state.tracks.get(&route.from_track) else {
7923                    continue;
7924                };
7925                events_by_track.insert(
7926                    route.from_track.clone(),
7927                    track.lock().take_hw_midi_out_events(),
7928                );
7929            }
7930        }
7931
7932        for route in routes {
7933            let Some(track_events) = events_by_track.get(&route.from_track) else {
7934                continue;
7935            };
7936            for hw_event in track_events
7937                .iter()
7938                .filter(|evt| evt.port == route.from_port)
7939            {
7940                self.update_active_hw_notes_for_track(
7941                    &route.from_track,
7942                    &route.device,
7943                    &hw_event.event.data,
7944                );
7945                events.push(HwMidiEvent {
7946                    device: route.device.clone(),
7947                    event: hw_event.event.clone(),
7948                });
7949            }
7950        }
7951        events.sort_by(|a, b| {
7952            a.event
7953                .frame
7954                .cmp(&b.event.frame)
7955                .then_with(|| a.device.cmp(&b.device))
7956        });
7957        events
7958    }
7959}
7960
7961#[cfg(test)]
7962mod tests {
7963    use super::*;
7964    use crate::mutex::UnsafeMutex;
7965    use tokio::sync::mpsc::channel;
7966    use tokio::time::{Duration as TokioDuration, timeout};
7967
7968    #[test]
7969    #[cfg(unix)]
7970    fn jack_transport_sync_decision_starts_and_syncs_position_on_external_play() {
7971        let decision = Engine::jack_transport_sync_decision(false, 128, true, 256, 64);
7972
7973        assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Start));
7974        assert_eq!(decision.position_sync, Some(256));
7975    }
7976
7977    #[test]
7978    #[cfg(unix)]
7979    fn jack_transport_sync_decision_stops_and_syncs_position_on_external_stop() {
7980        let decision = Engine::jack_transport_sync_decision(true, 512, false, 96, 64);
7981
7982        assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Stop));
7983        assert_eq!(decision.position_sync, Some(96));
7984    }
7985
7986    #[test]
7987    #[cfg(unix)]
7988    fn jack_transport_sync_decision_ignores_small_rolling_drift() {
7989        let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1040, 64);
7990
7991        assert_eq!(decision.play_sync, None);
7992        assert_eq!(decision.position_sync, None);
7993    }
7994
7995    #[test]
7996    #[cfg(unix)]
7997    fn jack_transport_sync_decision_syncs_large_rolling_jump() {
7998        let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1200, 64);
7999
8000        assert_eq!(decision.play_sync, None);
8001        assert_eq!(decision.position_sync, Some(1200));
8002    }
8003
8004    #[test]
8005    #[cfg(unix)]
8006    fn jack_transport_sync_decision_syncs_locate_while_stopped() {
8007        let decision = Engine::jack_transport_sync_decision(false, 400, false, 900, 64);
8008
8009        assert_eq!(decision.play_sync, None);
8010        assert_eq!(decision.position_sync, Some(900));
8011    }
8012
8013    fn make_engine_with_client() -> (Engine, tokio::sync::mpsc::Receiver<Message>) {
8014        let (engine_tx, engine_rx) = channel(16);
8015        let mut engine = Engine::new(engine_rx, engine_tx);
8016        let (client_tx, client_rx) = channel(16);
8017        engine.clients.push(client_tx);
8018        (engine, client_rx)
8019    }
8020
8021    fn insert_track(engine: &mut Engine, track: Track) {
8022        engine.state.lock().tracks.insert(
8023            track.name.clone(),
8024            Arc::new(UnsafeMutex::new(Box::new(track))),
8025        );
8026    }
8027
8028    fn osc_packet(address: &str) -> Vec<u8> {
8029        fn push_padded_osc_string(packet: &mut Vec<u8>, value: &str) {
8030            packet.extend_from_slice(value.as_bytes());
8031            packet.push(0);
8032            while !packet.len().is_multiple_of(4) {
8033                packet.push(0);
8034            }
8035        }
8036
8037        let mut packet = Vec::new();
8038        push_padded_osc_string(&mut packet, address);
8039        push_padded_osc_string(&mut packet, ",");
8040        packet
8041    }
8042
8043    #[tokio::test]
8044    async fn set_osc_enabled_starts_and_stops_server() {
8045        let (mut engine, _client_rx) = make_engine_with_client();
8046
8047        engine
8048            .set_osc_enabled_with(true, |tx| OscServer::start_on_addr(tx, "127.0.0.1:0"))
8049            .expect("start osc server on ephemeral port");
8050        assert!(engine.osc_server.is_some());
8051
8052        engine
8053            .set_osc_enabled_with(false, OscServer::start)
8054            .expect("stop osc server");
8055        assert!(engine.osc_server.is_none());
8056    }
8057
8058    #[tokio::test]
8059    async fn osc_server_forwards_transport_packets_to_engine_channel() {
8060        let (tx, mut rx) = channel(4);
8061        let mut server =
8062            OscServer::start_on_addr(tx, "127.0.0.1:0").expect("start osc test server");
8063        let socket = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind sender socket");
8064        let packet = osc_packet("/transport/play");
8065        socket
8066            .send_to(&packet, server.listen_addr())
8067            .expect("send osc packet");
8068
8069        let message = timeout(TokioDuration::from_secs(1), rx.recv())
8070            .await
8071            .expect("packet delivery timeout")
8072            .expect("osc message");
8073        match message {
8074            Message::Request(Action::Play) => {}
8075            other => panic!("unexpected osc message: {other:?}"),
8076        }
8077
8078        server.stop();
8079    }
8080
8081    #[tokio::test]
8082    async fn track_offline_bounce_rejects_zero_length_requests() {
8083        let (mut engine, mut client_rx) = make_engine_with_client();
8084        insert_track(
8085            &mut engine,
8086            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
8087        );
8088
8089        engine
8090            .handle_request(Action::TrackOfflineBounce {
8091                track_name: "track".to_string(),
8092                output_path: "/tmp/out.wav".to_string(),
8093                start_sample: 0,
8094                length_samples: 0,
8095                automation_lanes: vec![],
8096                apply_fader: false,
8097            })
8098            .await;
8099
8100        match client_rx.recv().await.expect("response") {
8101            Message::Response(Err(err)) => {
8102                assert!(err.contains("has no renderable content for offline bounce"));
8103            }
8104            other => panic!("unexpected message: {other:?}"),
8105        }
8106    }
8107
8108    #[tokio::test]
8109    async fn track_offline_bounce_rejects_when_same_track_is_active() {
8110        let (mut engine, mut client_rx) = make_engine_with_client();
8111        engine.offline_bounce_jobs.insert(
8112            "other".to_string(),
8113            OfflineBounceJob {
8114                cancel: Arc::new(AtomicBool::new(false)),
8115            },
8116        );
8117
8118        engine
8119            .handle_request(Action::TrackOfflineBounce {
8120                track_name: "other".to_string(),
8121                output_path: "/tmp/out.wav".to_string(),
8122                start_sample: 0,
8123                length_samples: 128,
8124                automation_lanes: vec![],
8125                apply_fader: false,
8126            })
8127            .await;
8128
8129        match client_rx.recv().await.expect("response") {
8130            Message::Response(Err(err)) => {
8131                assert!(err.contains("already in progress"));
8132            }
8133            other => panic!("unexpected message: {other:?}"),
8134        }
8135    }
8136
8137    #[tokio::test]
8138    async fn track_offline_bounce_allows_different_track_concurrently() {
8139        let (mut engine, _client_rx) = make_engine_with_client();
8140        insert_track(
8141            &mut engine,
8142            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
8143        );
8144        engine.offline_bounce_jobs.insert(
8145            "other".to_string(),
8146            OfflineBounceJob {
8147                cancel: Arc::new(AtomicBool::new(false)),
8148            },
8149        );
8150
8151        engine
8152            .handle_request(Action::TrackOfflineBounce {
8153                track_name: "track".to_string(),
8154                output_path: "/tmp/out.wav".to_string(),
8155                start_sample: 0,
8156                length_samples: 128,
8157                automation_lanes: vec![],
8158                apply_fader: false,
8159            })
8160            .await;
8161
8162        assert!(engine.offline_bounce_jobs.contains_key("other"));
8163        assert_eq!(engine.pending_requests.len(), 1);
8164    }
8165
8166    #[tokio::test]
8167    async fn reject_if_track_frozen_sends_error_and_blocks_operation() {
8168        let (mut engine, mut client_rx) = make_engine_with_client();
8169        let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
8170        track.set_frozen(true);
8171        insert_track(&mut engine, track);
8172
8173        let rejected = engine
8174            .reject_if_track_frozen("track", "arming/disarming")
8175            .await;
8176
8177        assert!(rejected);
8178        match client_rx.recv().await.expect("response") {
8179            Message::Response(Err(err)) => {
8180                assert_eq!(err, "Track 'track' is frozen; arming/disarming is blocked");
8181            }
8182            other => panic!("unexpected message: {other:?}"),
8183        }
8184    }
8185
8186    #[tokio::test]
8187    async fn undo_restores_original_clip_bounds_after_stretch_style_group() {
8188        let (mut engine, _client_rx) = make_engine_with_client();
8189        let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
8190        let mut clip = AudioClip::new("audio/original.wav".to_string(), 100, 220);
8191        clip.offset = 12;
8192        clip.fade_in_samples = 20;
8193        clip.fade_out_samples = 30;
8194        track.audio.clips.push(clip);
8195        insert_track(&mut engine, track);
8196
8197        engine.handle_request(Action::BeginHistoryGroup).await;
8198        engine
8199            .handle_request(Action::SetClipBounds {
8200                track_name: "track".to_string(),
8201                clip_index: 0,
8202                kind: Kind::Audio,
8203                start: 120,
8204                length: 180,
8205                offset: 0,
8206            })
8207            .await;
8208        engine
8209            .handle_request(Action::SetClipSourceName {
8210                track_name: "track".to_string(),
8211                clip_index: 0,
8212                kind: Kind::Audio,
8213                name: "audio/stretched.wav".to_string(),
8214            })
8215            .await;
8216        engine
8217            .handle_request(Action::SetClipFade {
8218                track_name: "track".to_string(),
8219                clip_index: 0,
8220                kind: Kind::Audio,
8221                fade_enabled: true,
8222                fade_in_samples: 12,
8223                fade_out_samples: 12,
8224            })
8225            .await;
8226        engine.handle_request(Action::EndHistoryGroup).await;
8227
8228        engine.handle_request(Action::Undo).await;
8229
8230        let state = engine.state.lock();
8231        let track = state.tracks.get("track").expect("track exists").lock();
8232        let clip = track.audio.clips.first().expect("clip exists");
8233        assert_eq!(clip.name, "audio/original.wav");
8234        assert_eq!(clip.start, 100);
8235        assert_eq!(clip.end, 220);
8236        assert_eq!(clip.end.saturating_sub(clip.start), 120);
8237        assert_eq!(clip.offset, 12);
8238    }
8239
8240    #[tokio::test]
8241    async fn track_offline_bounce_queues_when_no_worker_is_ready() {
8242        let (mut engine, _client_rx) = make_engine_with_client();
8243        insert_track(
8244            &mut engine,
8245            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
8246        );
8247
8248        engine
8249            .handle_request(Action::TrackOfflineBounce {
8250                track_name: "track".to_string(),
8251                output_path: "/tmp/out.wav".to_string(),
8252                start_sample: 0,
8253                length_samples: 128,
8254                automation_lanes: vec![],
8255                apply_fader: false,
8256            })
8257            .await;
8258
8259        assert!(engine.offline_bounce_jobs.is_empty());
8260        assert_eq!(engine.pending_requests.len(), 1);
8261        assert!(matches!(
8262            engine.pending_requests.front(),
8263            Some(Action::TrackOfflineBounce { track_name, length_samples, .. })
8264                if track_name == "track" && *length_samples == 128
8265        ));
8266    }
8267
8268    #[tokio::test]
8269    async fn track_offline_bounce_returns_missing_track_error() {
8270        let (mut engine, mut client_rx) = make_engine_with_client();
8271
8272        engine
8273            .handle_request(Action::TrackOfflineBounce {
8274                track_name: "missing".to_string(),
8275                output_path: "/tmp/out.wav".to_string(),
8276                start_sample: 0,
8277                length_samples: 128,
8278                automation_lanes: vec![],
8279                apply_fader: false,
8280            })
8281            .await;
8282
8283        match client_rx.recv().await.expect("response") {
8284            Message::Response(Err(err)) => {
8285                assert_eq!(err, "Track not found: missing");
8286            }
8287            other => panic!("unexpected message: {other:?}"),
8288        }
8289    }
8290
8291    #[tokio::test]
8292    async fn track_offline_bounce_clears_job_when_worker_send_fails() {
8293        let (mut engine, mut client_rx) = make_engine_with_client();
8294        insert_track(
8295            &mut engine,
8296            Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
8297        );
8298        let (worker_tx, worker_rx) = channel(1);
8299        drop(worker_rx);
8300        engine
8301            .workers
8302            .push(WorkerData::new(worker_tx, tokio::spawn(async {})));
8303        engine.ready_refill_workers.push(0);
8304
8305        engine
8306            .handle_request(Action::TrackOfflineBounce {
8307                track_name: "track".to_string(),
8308                output_path: "/tmp/out.wav".to_string(),
8309                start_sample: 0,
8310                length_samples: 128,
8311                automation_lanes: vec![],
8312                apply_fader: false,
8313            })
8314            .await;
8315
8316        assert!(engine.offline_bounce_jobs.is_empty());
8317        match client_rx.recv().await.expect("response") {
8318            Message::Response(Err(err)) => {
8319                assert!(err.contains("Failed to schedule offline bounce"));
8320            }
8321            other => panic!("unexpected message: {other:?}"),
8322        }
8323    }
8324}