Skip to main content

maolan_engine/engine/
runtime.rs

1use super::*;
2#[cfg(target_os = "linux")]
3use crate::hw::alsa::MidiHub;
4#[cfg(target_os = "macos")]
5use crate::hw::coremidi::MidiHub;
6#[cfg(target_os = "freebsd")]
7use crate::hw::oss::MidiHub;
8#[cfg(target_os = "openbsd")]
9use crate::hw::sndio::{HwDriver, HwOptions, MidiHub};
10#[cfg(target_os = "windows")]
11use crate::hw::wasapi::MidiHub;
12#[cfg(target_os = "openbsd")]
13use crate::workers::sndio_worker::HwWorker;
14use crate::{
15    history::{History, UndoEntry},
16    message::{Action, HwMidiEvent, Message, ProcessTask, SessionSlotState},
17    midi::io::MidiEvent,
18    osc::{OscArg, OscServer, build_error_packet, build_osc_packet},
19    state::State,
20    workers::worker::Worker,
21};
22use mixosc::parameters::{OscValue, build_set};
23use std::{
24    collections::{HashMap, VecDeque},
25    net::{SocketAddr, UdpSocket},
26    path::{Path, PathBuf},
27    sync::{Arc, atomic::Ordering},
28    time::{Duration, Instant},
29};
30use tokio::sync::Notify;
31use tokio::sync::mpsc::{Receiver, Sender, channel};
32use tracing::error;
33
34impl Engine {
35    pub fn state(&self) -> Arc<State> {
36        self.state.clone()
37    }
38
39    pub(crate) fn timing_at_sample(&self, sample: usize) -> (f64, u16, u16) {
40        let bpm = self
41            .tempo_points
42            .iter()
43            .filter(|p| p.sample <= sample)
44            .max_by_key(|p| p.sample)
45            .map(|p| p.bpm)
46            .unwrap_or(self.tempo_bpm)
47            .max(1.0);
48        let (num, den) = self
49            .time_signature_points
50            .iter()
51            .filter(|p| p.sample <= sample)
52            .max_by_key(|p| p.sample)
53            .map(|p| (p.numerator.max(1), p.denominator.max(1)))
54            .unwrap_or((self.tsig_num.max(1), self.tsig_denom.max(1)));
55        (bpm, num, den)
56    }
57
58    pub(crate) fn update_global_tempo_from_map(&mut self) {
59        let (bpm, num, den) = self.timing_at_sample(0);
60        self.tempo_bpm = bpm;
61        self.tsig_num = num;
62        self.tsig_denom = den;
63    }
64
65    pub(crate) fn meter_linear_to_db(peak: f32) -> f32 {
66        if peak <= 1.0e-6 {
67            -90.0
68        } else {
69            (20.0 * peak.log10()).clamp(-90.0, 20.0)
70        }
71    }
72
73    pub(crate) fn meter_db_to_linear(db: f32) -> f32 {
74        if db <= -90.0 {
75            0.0
76        } else {
77            10.0_f32.powf(db / 20.0)
78        }
79    }
80
81    pub(crate) const METER_PUBLISH_INTERVAL: Duration = Duration::from_millis(50);
82    pub(crate) const METER_DECAY_AFTER_STOP: Duration = Duration::from_secs(1);
83    pub(crate) const SESSION_RUNTIME_REPORT_INTERVAL: Duration = Duration::from_millis(50);
84    pub(crate) const TRACK_PROCESS_TIMEOUT: Duration = Duration::from_millis(250);
85    #[cfg(unix)]
86    pub(crate) const HW_OUT_METER_LINEAR_EPSILON: f32 = 0.0025;
87
88    #[cfg(unix)]
89    pub(crate) fn session_plugins_dir(&self) -> Option<PathBuf> {
90        self.session_dir.as_ref().map(|d| d.join("plugins"))
91    }
92
93    pub(crate) fn session_audio_dir(&self) -> Option<PathBuf> {
94        self.session_dir.as_ref().map(|d| d.join("audio"))
95    }
96
97    pub(crate) fn session_midi_dir(&self) -> Option<PathBuf> {
98        self.session_dir.as_ref().map(|d| d.join("midi"))
99    }
100
101    pub(crate) fn session_peaks_dir(&self) -> Option<PathBuf> {
102        self.session_dir.as_ref().map(|d| d.join("peaks"))
103    }
104
105    pub(crate) fn ensure_session_subdirs(&self) {
106        if let Some(root) = &self.session_dir {
107            let _ = std::fs::create_dir_all(root.join("plugins"));
108            let _ = std::fs::create_dir_all(root.join("audio"));
109            let _ = std::fs::create_dir_all(root.join("midi"));
110            let _ = std::fs::create_dir_all(root.join("peaks"));
111        }
112    }
113
114    pub fn new(rx: Receiver<Message>, tx: Sender<Message>) -> Self {
115        let (meter_snapshot_producer, _) =
116            crate::triple_buffer::triple_buffer(crate::meter::MeterSnapshot::default());
117        let (transport_snapshot_producer, _) =
118            crate::triple_buffer::triple_buffer(crate::meter::TransportSnapshot::default());
119        let (session_runtime_snapshot_producer, _) =
120            crate::triple_buffer::triple_buffer(crate::meter::SessionRuntimeSnapshot::default());
121        Self::new_with_snapshots(
122            rx,
123            tx,
124            meter_snapshot_producer,
125            transport_snapshot_producer,
126            session_runtime_snapshot_producer,
127        )
128    }
129
130    pub fn new_with_snapshots(
131        rx: Receiver<Message>,
132        tx: Sender<Message>,
133        meter_snapshot_producer: crate::triple_buffer::TripleBufferProducer<
134            crate::meter::MeterSnapshot,
135        >,
136        transport_snapshot_producer: crate::triple_buffer::TripleBufferProducer<
137            crate::meter::TransportSnapshot,
138        >,
139        session_runtime_snapshot_producer: crate::triple_buffer::TripleBufferProducer<
140            crate::meter::SessionRuntimeSnapshot,
141        >,
142    ) -> Self {
143        let state = Arc::new(State::default());
144        let initial_state_snapshot = state.lock().snapshot();
145        let state_snapshot = Arc::new(crate::state::StateSlot::from_pointee(
146            initial_state_snapshot.clone(),
147        ));
148        // Phase 2 render-plan machinery: an initial (empty-session) plan is
149        // built synchronously; the builder thread republishes on demand.
150        let collector = basedrop::Collector::new();
151        let hw_ports = Arc::new(arc_swap::ArcSwap::from_pointee(
152            crate::plan_builder::HwPorts {
153                buffer_size: 1024,
154                ..Default::default()
155            },
156        ));
157        let initial_plan =
158            { crate::render_plan::RenderPlan::compile(&initial_state_snapshot, &[], &[], 1024) };
159        let plan_slot = Arc::new(crate::render_plan::PlanSlot::from_pointee(
160            basedrop::Owned::new(&collector.handle(), initial_plan),
161        ));
162        let plan_builder = crate::plan_builder::PlanBuilder::spawn(
163            state_snapshot.clone(),
164            hw_ports.clone(),
165            plan_slot.clone(),
166            collector,
167        );
168        let executor = crate::executor::CycleExecutor::new(plan_slot.clone());
169        Self {
170            rx,
171            tx,
172            clients: vec![],
173            state,
174            state_snapshot,
175            workers: vec![],
176            hw_driver: None,
177            hw_driver_info: None,
178            hw_input_ports: Vec::new(),
179            hw_output_ports: Vec::new(),
180            #[cfg(unix)]
181            jack_runtime: None,
182            midi_hub: Some(MidiHub::default()),
183            hw_worker: None,
184            osc_server: None,
185            osc_reply_socket: None,
186            osc_reply_target: None,
187            mixosc_socket: None,
188            pending_hw_midi_events: vec![],
189            pending_hw_midi_events_by_device: HashMap::new(),
190            pending_hw_midi_out_events: vec![],
191            pending_hw_midi_out_events_by_device: vec![],
192            active_hw_notes_by_track: HashMap::new(),
193            active_hw_notes_cycle_start: HashMap::new(),
194            midi_hw_in_routes: vec![],
195            midi_hw_out_routes: vec![],
196            midi_hw_thru_routes: vec![],
197            ready_workers: vec![],
198            pending_requests: VecDeque::new(),
199            awaiting_hwfinished: false,
200            handling_hwfinished: false,
201            transport_panic_flush_pending: false,
202            transport_restart_pending: false,
203            notified_loop_wrap_sample: None,
204            transport_sample: 0,
205            hw_input_latency_frames: 0,
206            hw_output_latency_frames: 0,
207            loop_enabled: false,
208            loop_range_samples: None,
209            metronome_enabled: false,
210            tempo_bpm: 120.0,
211            tsig_num: 4,
212            tsig_denom: 4,
213            tempo_points: vec![crate::message::TempoPoint {
214                sample: 0,
215                bpm: 120.0,
216            }],
217            time_signature_points: vec![crate::message::TimeSignaturePoint {
218                sample: 0,
219                numerator: 4,
220                denominator: 4,
221            }],
222            punch_enabled: false,
223            punch_range_samples: None,
224            audio_recordings: std::collections::HashMap::new(),
225            midi_recordings: std::collections::HashMap::new(),
226            completed_audio_recordings: Vec::new(),
227            completed_midi_recordings: Vec::new(),
228            playing: false,
229            transport_running: false,
230            clip_playback_enabled: true,
231            session_clip_playback_enabled: false,
232            session_transport_sample: 0,
233            session_scene_queue: None,
234            session_scene_queue_length_samples: 0,
235            session_current_scene: None,
236            session_current_scene_previous_scene: None,
237            session_current_scene_start_sample: 0,
238            session_current_scene_length_samples: 0,
239            session_completed_clip_passes: Vec::new(),
240            session_reported_clip_passes: std::collections::HashSet::new(),
241            record_enabled: false,
242            step_recording_enabled: false,
243            session_dir: None,
244            hw_out_level_db: 0.0,
245            hw_out_balance: 0.0,
246            hw_out_muted: false,
247            last_hw_out_meter_publish: None,
248            #[cfg(unix)]
249            last_hw_out_meter_linear: vec![],
250            hw_out_peak_hold_linear: vec![],
251            #[cfg(unix)]
252            hw_out_meter_publish_phase: false,
253            last_track_meter_publish: None,
254            last_meter_snapshot_publish: None,
255            last_session_report_publish: None,
256            track_meter_linear_by_track: HashMap::new(),
257            meter_decay_after_stop: None,
258            meter_snapshot_producer,
259            transport_snapshot_producer,
260            session_runtime_snapshot_producer,
261            executor,
262            plan_builder,
263            plan_slot,
264            hw_ports,
265            pending_node_jobs: VecDeque::new(),
266            latest_hw_out_meter_db: Arc::new(Vec::new()),
267            latest_track_meter_snapshot: Arc::new(Vec::new()),
268            hw_out_loudness_meter: None,
269            latest_hw_out_lufs: None,
270            history: History::default(),
271            history_group: None,
272            history_suspended: false,
273            offline_bounce_jobs: HashMap::new(),
274            pending_bounce_starts: Vec::new(),
275            bounce_worker_tracks: HashMap::new(),
276            pending_midi_learn: None,
277            pending_global_midi_learn: None,
278            pending_session_midi_learn: None,
279            audio_preview: None,
280            global_midi_learn_play_pause: None,
281            global_midi_learn_stop: None,
282            global_midi_learn_record_toggle: None,
283            session_midi_learn_slots: HashMap::new(),
284            session_midi_learn_scenes: HashMap::new(),
285            session_midi_learn_stop_track: HashMap::new(),
286            session_midi_learn_stop_all: None,
287            midi_cc_gate: HashMap::new(),
288            modulators: Vec::new(),
289            modulator_values: None,
290            mixosc_last_values: HashMap::new(),
291            #[cfg(target_os = "windows")]
292            _windows_timer_guard: crate::enable_windows_high_resolution_timer(),
293            node_result_notify: Arc::new(Notify::new()),
294        }
295    }
296
297    pub(crate) fn publish_state_snapshot(&self) {
298        let snapshot = self.state.lock().snapshot();
299        self.state_snapshot.store(Arc::new(snapshot));
300    }
301
302    pub(crate) fn hw_driver_cycle_samples(&self) -> Option<usize> {
303        self.hw_driver_info.map(|info| info.cycle_samples)
304    }
305
306    #[cfg(unix)]
307    pub(crate) fn jack_cycle_samples(&self) -> Option<usize> {
308        self.jack_runtime.as_ref().map(|j| j.buffer_size)
309    }
310
311    #[cfg(not(unix))]
312    pub(crate) fn jack_cycle_samples(&self) -> Option<usize> {
313        None
314    }
315
316    pub(crate) fn current_cycle_samples(&self) -> usize {
317        self.hw_driver_cycle_samples()
318            .or_else(|| self.jack_cycle_samples())
319            .unwrap_or(0)
320    }
321
322    pub(crate) fn sample_rate(&self) -> f64 {
323        if let Some(info) = self.hw_driver_info {
324            info.sample_rate as f64
325        } else {
326            #[cfg(unix)]
327            {
328                self.jack_runtime
329                    .as_ref()
330                    .map(|j| j.sample_rate as f64)
331                    .unwrap_or(48_000.0)
332            }
333            #[cfg(not(unix))]
334            {
335                48_000.0
336            }
337        }
338    }
339
340    pub(crate) async fn set_hw_playing(&mut self, playing: bool) {
341        if let Some(worker) = &self.hw_worker {
342            // Await instead of try_send: a dropped HWSetPlaying(false) would
343            // leave the device playing after transport stop.
344            let _ = worker.tx.send(Message::HWSetPlaying(playing)).await;
345        } else if let Some(driver) = self.hw_driver.as_mut() {
346            driver.set_playing(playing);
347        }
348    }
349
350    pub(crate) fn active_transport_sample(&self) -> usize {
351        if self.session_clip_playback_enabled && self.playing {
352            self.session_transport_sample
353        } else {
354            self.transport_sample
355        }
356    }
357
358    pub(crate) fn compute_modulator_values(
359        &self,
360        sample: usize,
361    ) -> Arc<std::collections::HashMap<usize, f32>> {
362        let sample_rate = self.sample_rate();
363        let (bpm, tsig_num, tsig_denom) = self.timing_at_sample(sample);
364        let values: std::collections::HashMap<usize, f32> = self
365            .modulators
366            .iter()
367            .filter(|m| m.enabled)
368            .map(|m| {
369                (
370                    m.id,
371                    m.value_at(sample, sample_rate, bpm, tsig_num, tsig_denom),
372                )
373            })
374            .collect();
375        Arc::new(values)
376    }
377
378    pub(crate) fn apply_modulators(&mut self, sample: usize) -> Vec<Action> {
379        use crate::modulator::ModulatorTarget;
380        let values = self.compute_modulator_values(sample);
381        self.modulator_values = Some(values.clone());
382        let mut echoes = Vec::new();
383        let mut per_track: HashMap<String, (Option<f32>, Option<f32>)> = HashMap::new();
384        let mut clap_params: HashMap<(String, usize, u32), f64> = HashMap::new();
385        let mut vst3_params: HashMap<(String, usize, u32), f32> = HashMap::new();
386        #[cfg(unix)]
387        let mut lv2_params: HashMap<(String, usize, u32), f32> = HashMap::new();
388        let mut midi_cc_events: HashMap<String, Vec<MidiEvent>> = HashMap::new();
389
390        let map_f32 = |value: f32, min: f32, max: f32| -> f32 {
391            crate::modulator::map_value(value, min, max)
392        };
393        let map_f64 = |value: f32, min: f64, max: f64| -> f64 {
394            crate::modulator::map_value_f64(value, min, max)
395        };
396
397        for m in &self.modulators {
398            if !m.enabled {
399                continue;
400            }
401            let Some(&value) = values.get(&m.id) else {
402                continue;
403            };
404            for target in &m.targets {
405                match target {
406                    ModulatorTarget::TrackVolume {
407                        track_name,
408                        min,
409                        max,
410                    } => {
411                        let clamped = map_f32(value, *min, *max);
412                        per_track.entry(track_name.clone()).or_default().0 = Some(clamped);
413                    }
414                    ModulatorTarget::TrackBalance {
415                        track_name,
416                        min,
417                        max,
418                    } => {
419                        let clamped = map_f32(value, *min, *max);
420                        per_track.entry(track_name.clone()).or_default().1 = Some(clamped);
421                    }
422                    ModulatorTarget::HwOutVolume { min, max } => {
423                        let clamped = map_f32(value, *min, *max);
424                        if (self.hw_out_level_db - clamped).abs() > f32::EPSILON {
425                            self.hw_out_level_db = clamped;
426                            echoes
427                                .push(Action::TrackAutomationLevel("hw:out".to_string(), clamped));
428                        }
429                    }
430                    ModulatorTarget::HwOutBalance { min, max } => {
431                        let next = map_f32(value, *min, *max).clamp(-1.0, 1.0);
432                        if (self.hw_out_balance - next).abs() > f32::EPSILON {
433                            self.hw_out_balance = next;
434                            echoes.push(Action::TrackAutomationBalance("hw:out".to_string(), next));
435                        }
436                    }
437                    ModulatorTarget::ClapParameter {
438                        track_name,
439                        instance_id,
440                        param_id,
441                        min,
442                        max,
443                    } => {
444                        let param_value = map_f64(value, *min, *max);
445                        clap_params
446                            .insert((track_name.clone(), *instance_id, *param_id), param_value);
447                    }
448                    ModulatorTarget::Vst3Parameter {
449                        track_name,
450                        instance_id,
451                        param_id,
452                        min,
453                        max,
454                    } => {
455                        let param_value = map_f32(value, *min, *max);
456                        vst3_params
457                            .insert((track_name.clone(), *instance_id, *param_id), param_value);
458                    }
459                    #[cfg(unix)]
460                    ModulatorTarget::Lv2Parameter {
461                        track_name,
462                        instance_id,
463                        index,
464                        min,
465                        max,
466                    } => {
467                        let param_value = map_f32(value, *min, *max);
468                        lv2_params.insert((track_name.clone(), *instance_id, *index), param_value);
469                    }
470                    ModulatorTarget::MidiCc {
471                        track_name,
472                        channel,
473                        cc,
474                    } => {
475                        let cc_value = (value * 127.0).round() as u8;
476                        midi_cc_events
477                            .entry(track_name.clone())
478                            .or_default()
479                            .push(MidiEvent::new(
480                                0,
481                                vec![0xB0 | (*channel).min(15), (*cc).min(127), cc_value],
482                            ));
483                    }
484                }
485            }
486        }
487        let state = self.state_snapshot.load_full();
488        for (track_name, (level, balance)) in per_track {
489            if let Some(level) = level
490                && let Some(track) = state.tracks.get(&track_name).cloned()
491            {
492                let t = track.lock();
493                if (t.level() - level).abs() > f32::EPSILON {
494                    t.set_level(level);
495                    echoes.push(Action::TrackAutomationLevel(track_name.clone(), level));
496                }
497            }
498            if let Some(balance) = balance
499                && let Some(track) = state.tracks.get(&track_name).cloned()
500            {
501                let t = track.lock();
502                let next = balance.clamp(-1.0, 1.0);
503                if (t.balance() - next).abs() > f32::EPSILON {
504                    t.set_balance(next);
505                    echoes.push(Action::TrackAutomationBalance(track_name.clone(), next));
506                }
507            }
508        }
509
510        for (track_name, events) in midi_cc_events {
511            if let Some(track) = state.tracks.get(&track_name).cloned() {
512                track.lock().rt.pending_modulator_midi_events.extend(events);
513            }
514        }
515
516        for ((track_name, instance_id, param_id), value) in clap_params {
517            if let Some(track) = state.tracks.get(&track_name).cloned()
518                && track
519                    .lock()
520                    .set_clap_parameter(instance_id, param_id, value)
521                    .is_ok()
522            {
523                echoes.push(Action::TrackSetClapParameter {
524                    track_name,
525                    instance_id,
526                    param_id,
527                    value,
528                });
529            }
530        }
531        for ((track_name, instance_id, param_id), value) in vst3_params {
532            if let Some(track) = state.tracks.get(&track_name).cloned()
533                && track
534                    .lock()
535                    .set_vst3_parameter(instance_id, param_id, value)
536                    .is_ok()
537            {
538                echoes.push(Action::TrackSetVst3Parameter {
539                    track_name,
540                    instance_id,
541                    param_id,
542                    value,
543                });
544            }
545        }
546        #[cfg(unix)]
547        for ((track_name, instance_id, index), value) in lv2_params {
548            if let Some(track) = state.tracks.get(&track_name).cloned()
549                && track
550                    .lock()
551                    .set_lv2_control_value(instance_id, index as usize, f64::from(value))
552                    .is_ok()
553            {
554                echoes.push(Action::TrackSetLv2ControlValue {
555                    track_name,
556                    instance_id,
557                    index,
558                    value,
559                });
560            }
561        }
562
563        echoes
564    }
565
566    /// Evaluates MixOSC automation lanes for tracks bound to a mixer and sends
567    /// the current values over UDP/OSC. Called once per hardware cycle.
568    pub(crate) fn apply_mixosc_automation(&mut self, sample: usize) {
569        if !self.playing {
570            return;
571        }
572
573        let socket = match self.mixosc_socket.as_ref() {
574            Some(socket) => socket,
575            None => match UdpSocket::bind("0.0.0.0:0") {
576                Ok(socket) => {
577                    self.mixosc_socket = Some(socket);
578                    self.mixosc_socket.as_ref().unwrap()
579                }
580                Err(err) => {
581                    tracing::warn!(%err, "Failed to bind MixOSC output socket");
582                    return;
583                }
584            },
585        };
586
587        let state = self.state_snapshot.load_full();
588        for (track_name, track) in state.tracks.iter() {
589            let track_lock = track.lock();
590            let Some(track_addr) = track_lock.mixosc_addr.as_ref() else {
591                continue;
592            };
593            let lanes: Vec<crate::message::OfflineAutomationLane> =
594                crate::engine::parse_automation_lanes(&track_lock.automation_lanes);
595            for lane in lanes {
596                if !lane.visible {
597                    continue;
598                }
599                let crate::message::OfflineAutomationTarget::MixOsc {
600                    addr: ref lane_addr,
601                    path: ref lane_path,
602                } = lane.target
603                else {
604                    continue;
605                };
606                if lane_addr != track_addr {
607                    continue;
608                }
609                let Some(value) = lane.value_at(sample) else {
610                    continue;
611                };
612                let key = (track_addr.clone(), lane_path.clone());
613                if let Some(last) = self.mixosc_last_values.get(&key)
614                    && (last - value).abs() < f32::EPSILON
615                {
616                    continue;
617                }
618                self.mixosc_last_values.insert(key, value);
619                let packet = build_set(lane_path, OscValue::Float(value));
620                if let Err(err) = socket.send_to(&packet, track_addr) {
621                    tracing::debug!(
622                        %err,
623                        %track_name,
624                        %track_addr,
625                        %lane_path,
626                        "Failed to send MixOSC packet"
627                    );
628                }
629            }
630        }
631    }
632
633    pub(crate) fn session_end_sample(&self) -> usize {
634        self.state
635            .lock()
636            .tracks
637            .values()
638            .map(|track| {
639                let track = track.lock();
640                let audio_end = track
641                    .audio
642                    .clips()
643                    .iter()
644                    .map(|clip| clip.end)
645                    .max()
646                    .unwrap_or(0);
647                let midi_end = track
648                    .midi
649                    .clips()
650                    .iter()
651                    .map(|clip| clip.end)
652                    .max()
653                    .unwrap_or(0);
654                audio_end.max(midi_end)
655            })
656            .max()
657            .unwrap_or(0)
658    }
659
660    pub(crate) fn normalize_transport_sample(&self, sample: usize) -> usize {
661        if self.loop_enabled
662            && let Some((loop_start, loop_end)) = self.loop_range_samples
663            && loop_end > loop_start
664            && sample >= loop_end
665        {
666            let loop_len = loop_end - loop_start;
667            return loop_start + (sample - loop_start) % loop_len;
668        }
669        sample
670    }
671
672    pub(crate) fn scheduled_loop_wrap_for_next_cycle(&self) -> Option<(usize, usize, usize)> {
673        if !self.playing || !self.loop_enabled {
674            return None;
675        }
676        let (loop_start, loop_end) = self.loop_range_samples?;
677        if loop_end <= loop_start || self.transport_sample >= loop_end {
678            return None;
679        }
680        let cycle_samples = self.current_cycle_samples();
681        if cycle_samples == 0 {
682            return None;
683        }
684        let next = self.transport_sample.saturating_add(cycle_samples);
685        if next < loop_end {
686            return None;
687        }
688        let after_frames = loop_end.saturating_sub(self.transport_sample);
689        Some((
690            after_frames,
691            loop_start,
692            self.normalize_transport_sample(next),
693        ))
694    }
695
696    pub(crate) fn cycle_segments(&self, frames: usize) -> Vec<(usize, usize, usize)> {
697        if frames == 0 {
698            return vec![];
699        }
700        if !self.loop_enabled {
701            return vec![(
702                self.transport_sample,
703                self.transport_sample.saturating_add(frames),
704                0,
705            )];
706        }
707        let Some((loop_start, loop_end)) = self.loop_range_samples else {
708            return vec![(
709                self.transport_sample,
710                self.transport_sample.saturating_add(frames),
711                0,
712            )];
713        };
714        if loop_end <= loop_start {
715            return vec![(
716                self.transport_sample,
717                self.transport_sample.saturating_add(frames),
718                0,
719            )];
720        }
721        let mut segments = Vec::new();
722        let mut remaining = frames;
723        let mut out_offset = 0usize;
724        let mut current = self.transport_sample;
725        while remaining > 0 {
726            let take = loop_end.saturating_sub(current).min(remaining);
727            if take == 0 {
728                current = loop_start;
729                continue;
730            }
731            segments.push((current, current.saturating_add(take), out_offset));
732            out_offset = out_offset.saturating_add(take);
733            remaining -= take;
734            current = if remaining > 0 {
735                loop_start
736            } else {
737                current.saturating_add(take)
738            };
739        }
740        segments
741    }
742
743    pub(crate) fn recording_segments_for_cycle(&self, frames: usize) -> Vec<(usize, usize, usize)> {
744        let segments = self.cycle_segments(frames);
745        let comp = self.hw_input_latency_frames;
746        let segments: Vec<_> = if comp > 0 {
747            segments
748                .into_iter()
749                .map(|(start, end, offset)| {
750                    (start.saturating_sub(comp), end.saturating_sub(comp), offset)
751                })
752                .collect()
753        } else {
754            segments
755        };
756        if !self.punch_enabled {
757            return segments;
758        }
759        let Some((punch_start, punch_end)) = self.punch_range_samples else {
760            return vec![];
761        };
762        if punch_end <= punch_start {
763            return vec![];
764        }
765        let mut clipped = Vec::new();
766        for (segment_start, segment_end, frame_offset) in segments {
767            let start = segment_start.max(punch_start);
768            let end = segment_end.min(punch_end);
769            if end <= start {
770                continue;
771            }
772            let clipped_offset = frame_offset.saturating_add(start.saturating_sub(segment_start));
773            clipped.push((start, end, clipped_offset));
774        }
775        clipped
776    }
777
778    pub async fn init(&mut self) {
779        let max_threads = num_cpus::get();
780        for id in 0..max_threads {
781            let (tx, rx) = channel::<Message>(32);
782            let tx_thread = self.tx.clone();
783            let handler = tokio::spawn(async move {
784                let wrk = Worker::new(id, rx, tx_thread, 8);
785                wrk.await.work().await;
786            });
787            let (node_job_tx, mut node_job_rx) = rtrb::RingBuffer::new(64);
788            let (mut node_result_tx, node_result_rx) = rtrb::RingBuffer::new(64);
789            let node_quit = Arc::new(AtomicBool::new(false));
790            let node_quit_thread = node_quit.clone();
791            let node_result_notify = self.node_result_notify.clone();
792            let node_thread_handle = std::thread::Builder::new()
793                .name(format!("maolan-node-worker-{id}"))
794                .spawn(move || {
795                    crate::enable_flush_denormals_to_zero();
796                    if let Err(e) = Worker::try_enable_realtime(8) {
797                        tracing::warn!(
798                            "Node worker {} realtime priority {} not enabled: {}",
799                            id,
800                            8,
801                            e
802                        );
803                    }
804                    while !node_quit_thread.load(std::sync::atomic::Ordering::Acquire) {
805                        match node_job_rx.pop() {
806                            Ok(job) => {
807                                let mut result = Worker::process_node_job_result(id, job);
808                                loop {
809                                    match node_result_tx.push(result) {
810                                        Ok(()) => {
811                                            node_result_notify.notify_one();
812                                            break;
813                                        }
814                                        Err(rtrb::PushError::Full(returned)) => {
815                                            if node_quit_thread
816                                                .load(std::sync::atomic::Ordering::Acquire)
817                                            {
818                                                break;
819                                            }
820                                            result = returned;
821                                            std::thread::yield_now();
822                                        }
823                                    }
824                                }
825                            }
826                            Err(rtrb::PopError::Empty) => {
827                                std::thread::park();
828                            }
829                        }
830                    }
831                })
832                .expect("failed to spawn node worker thread");
833            let node_thread = node_thread_handle.thread().clone();
834            std::mem::forget(node_thread_handle);
835            self.workers.push(WorkerData::with_node_mailbox(
836                tx.clone(),
837                handler,
838                node_job_tx,
839                node_result_rx,
840                node_thread,
841                node_quit,
842            ));
843        }
844    }
845
846    pub(crate) async fn notify_clients(&mut self, action: Result<Action, String>) {
847        self.clients.retain(|client| !client.is_closed());
848        for client in self.clients.iter() {
849            if client
850                .send(Message::Response(action.clone()))
851                .await
852                .is_err()
853            {}
854        }
855        if let Some(reply_to) = self.osc_reply_target {
856            match &action {
857                Err(reason) => {
858                    self.send_osc_reply(reply_to, &build_error_packet(reason));
859                }
860                Ok(Action::TrackList(names)) => {
861                    let args: Vec<OscArg> = names
862                        .iter()
863                        .map(|name| OscArg::String(name.clone()))
864                        .collect();
865                    self.send_osc_reply(
866                        reply_to,
867                        &build_osc_packet("/response/tracks", &"s".repeat(names.len()), &args),
868                    );
869                }
870                Ok(Action::TransportState {
871                    sample,
872                    tempo_bpm,
873                    playing,
874                    paused: _,
875                    tsig_num,
876                    tsig_denom,
877                }) => {
878                    self.send_osc_reply(
879                        reply_to,
880                        &build_osc_packet(
881                            "/response/transport",
882                            "idffii",
883                            &[
884                                OscArg::Int(*sample as i32),
885                                OscArg::Int(if *playing { 1 } else { 0 }),
886                                OscArg::Float(*tempo_bpm as f32),
887                                OscArg::Float(0.0), // placeholder for future beat position
888                                OscArg::Int(*tsig_num as i32),
889                                OscArg::Int(*tsig_denom as i32),
890                            ],
891                        ),
892                    );
893                }
894                Ok(Action::MeterSnapshot {
895                    hw_out_db,
896                    track_meters,
897                    ..
898                }) => {
899                    let mut args: Vec<OscArg> = Vec::new();
900                    args.push(OscArg::Int(hw_out_db.len() as i32));
901                    for db in hw_out_db.iter() {
902                        args.push(OscArg::Float(*db));
903                    }
904                    args.push(OscArg::Int(track_meters.len() as i32));
905                    for (name, channels) in track_meters.iter() {
906                        args.push(OscArg::String(name.clone()));
907                        args.push(OscArg::Int(channels.len() as i32));
908                        for db in channels.iter() {
909                            args.push(OscArg::Float(*db));
910                        }
911                    }
912                    let types = args
913                        .iter()
914                        .map(|a| match a {
915                            OscArg::String(_) => 's',
916                            OscArg::Int(_) => 'i',
917                            OscArg::Float(_) => 'f',
918                        })
919                        .collect::<String>();
920                    self.send_osc_reply(
921                        reply_to,
922                        &build_osc_packet("/response/meters", &types, &args),
923                    );
924                }
925                Ok(Action::TrackPluginGraph {
926                    track_name,
927                    plugins,
928                    connections: _,
929                    connectable_connections: _,
930                }) => {
931                    let mut args: Vec<OscArg> = vec![OscArg::String(track_name.clone())];
932                    args.push(OscArg::Int(plugins.len() as i32));
933                    for plugin in plugins.iter() {
934                        args.push(OscArg::Int(plugin.instance_id as i32));
935                        args.push(OscArg::String(plugin.format.clone()));
936                        args.push(OscArg::String(plugin.uri.clone()));
937                        args.push(OscArg::String(plugin.name.clone()));
938                        args.push(OscArg::Int(plugin.bypassed as i32));
939                    }
940                    let types = args
941                        .iter()
942                        .map(|a| match a {
943                            OscArg::String(_) => 's',
944                            OscArg::Int(_) => 'i',
945                            OscArg::Float(_) => 'f',
946                        })
947                        .collect::<String>();
948                    self.send_osc_reply(
949                        reply_to,
950                        &build_osc_packet("/response/plugins", &types, &args),
951                    );
952                }
953                Ok(Action::ClapPlugins(plugins)) => {
954                    let args: Vec<OscArg> = plugins
955                        .iter()
956                        .map(|p| OscArg::String(format!("{}|{}", p.path, p.name)))
957                        .collect();
958                    let types = "s".repeat(args.len());
959                    self.send_osc_reply(
960                        reply_to,
961                        &build_osc_packet("/response/clap_plugins", &types, &args),
962                    );
963                }
964                Ok(Action::Vst3Plugins(plugins)) => {
965                    let args: Vec<OscArg> = plugins
966                        .iter()
967                        .map(|p| OscArg::String(format!("{}|{}", p.id, p.name)))
968                        .collect();
969                    let types = "s".repeat(args.len());
970                    self.send_osc_reply(
971                        reply_to,
972                        &build_osc_packet("/response/vst3_plugins", &types, &args),
973                    );
974                }
975                #[cfg(unix)]
976                Ok(Action::Lv2Plugins(plugins)) => {
977                    let args: Vec<OscArg> = plugins
978                        .iter()
979                        .map(|p| OscArg::String(format!("{}|{}", p.uri, p.name)))
980                        .collect();
981                    let types = "s".repeat(args.len());
982                    self.send_osc_reply(
983                        reply_to,
984                        &build_osc_packet("/response/lv2_plugins", &types, &args),
985                    );
986                }
987                Ok(Action::ClapPluginsUnavailable { error })
988                | Ok(Action::Vst3PluginsUnavailable { error }) => {
989                    self.send_osc_reply(reply_to, &build_error_packet(error));
990                }
991                #[cfg(unix)]
992                Ok(Action::Lv2PluginsUnavailable { error }) => {
993                    self.send_osc_reply(reply_to, &build_error_packet(error));
994                }
995                Ok(Action::TrackClapParameters {
996                    track_name,
997                    instance_id,
998                    parameters,
999                }) => {
1000                    let json = serde_json::json!(
1001                        parameters
1002                            .iter()
1003                            .map(|p| serde_json::json!({
1004                                "id": p.id,
1005                                "name": p.name,
1006                                "module": p.module,
1007                                "min_value": p.min_value,
1008                                "max_value": p.max_value,
1009                                "default_value": p.default_value,
1010                            }))
1011                            .collect::<Vec<_>>()
1012                    )
1013                    .to_string();
1014                    self.send_osc_reply(
1015                        reply_to,
1016                        &build_osc_packet(
1017                            "/response/plugin_parameters",
1018                            "siss",
1019                            &[
1020                                OscArg::String(track_name.clone()),
1021                                OscArg::Int(*instance_id as i32),
1022                                OscArg::String("clap".to_string()),
1023                                OscArg::String(json),
1024                            ],
1025                        ),
1026                    );
1027                }
1028                Ok(Action::TrackVst3Parameters {
1029                    track_name,
1030                    instance_id,
1031                    parameters,
1032                }) => {
1033                    let json = serde_json::to_string(parameters).unwrap_or_default();
1034                    self.send_osc_reply(
1035                        reply_to,
1036                        &build_osc_packet(
1037                            "/response/plugin_parameters",
1038                            "siss",
1039                            &[
1040                                OscArg::String(track_name.clone()),
1041                                OscArg::Int(*instance_id as i32),
1042                                OscArg::String("vst3".to_string()),
1043                                OscArg::String(json),
1044                            ],
1045                        ),
1046                    );
1047                }
1048                #[cfg(unix)]
1049                Ok(Action::TrackLv2PluginControls {
1050                    track_name,
1051                    instance_id,
1052                    controls,
1053                    instance_access_handle: _,
1054                }) => {
1055                    let json = serde_json::json!(
1056                        controls
1057                            .iter()
1058                            .map(|c| serde_json::json!({
1059                                "index": c.index,
1060                                "name": c.name,
1061                                "min": c.min,
1062                                "max": c.max,
1063                                "value": c.value,
1064                            }))
1065                            .collect::<Vec<_>>()
1066                    )
1067                    .to_string();
1068                    self.send_osc_reply(
1069                        reply_to,
1070                        &build_osc_packet(
1071                            "/response/plugin_parameters",
1072                            "siss",
1073                            &[
1074                                OscArg::String(track_name.clone()),
1075                                OscArg::Int(*instance_id as i32),
1076                                OscArg::String("lv2".to_string()),
1077                                OscArg::String(json),
1078                            ],
1079                        ),
1080                    );
1081                }
1082                Ok(Action::TrackClapNoteNames {
1083                    track_name,
1084                    note_names,
1085                }) => {
1086                    let json = serde_json::to_string(note_names).unwrap_or_default();
1087                    self.send_osc_reply(
1088                        reply_to,
1089                        &build_osc_packet(
1090                            "/response/clap_note_names",
1091                            "ss",
1092                            &[OscArg::String(track_name.clone()), OscArg::String(json)],
1093                        ),
1094                    );
1095                }
1096                #[cfg(unix)]
1097                Ok(Action::TrackLv2Midnam {
1098                    track_name,
1099                    note_names,
1100                }) => {
1101                    let json = serde_json::to_string(note_names).unwrap_or_default();
1102                    self.send_osc_reply(
1103                        reply_to,
1104                        &build_osc_packet(
1105                            "/response/lv2_midnam",
1106                            "ss",
1107                            &[OscArg::String(track_name.clone()), OscArg::String(json)],
1108                        ),
1109                    );
1110                }
1111                _ => {}
1112            }
1113        }
1114    }
1115
1116    pub(crate) fn send_osc_reply(&mut self, reply_to: SocketAddr, packet: &[u8]) {
1117        if self.osc_reply_socket.is_none() {
1118            self.osc_reply_socket = UdpSocket::bind("0.0.0.0:0").ok();
1119        }
1120        if let Some(socket) = self.osc_reply_socket.as_ref() {
1121            let _ = socket.send_to(packet, reply_to);
1122        }
1123    }
1124
1125    pub(crate) async fn dispatch_request(&mut self, a: Action) {
1126        match a {
1127            Action::TrackOfflineBounceCancel { track_name } => {
1128                if let Some(job) = self.offline_bounce_jobs.get(&track_name) {
1129                    job.cancel.store(true, Ordering::Relaxed);
1130                }
1131            }
1132            Action::TrackOfflineBounceCancelAll => {
1133                for job in self.offline_bounce_jobs.values() {
1134                    job.cancel.store(true, Ordering::Relaxed);
1135                }
1136            }
1137            _ if !self.offline_bounce_jobs.is_empty() => {
1138                self.pending_requests.push_back(a);
1139            }
1140            Action::OpenAudioDevice { .. }
1141            | Action::OpenMidiInputDevice(_)
1142            | Action::OpenMidiOutputDevice(_)
1143            | Action::RequestMeterSnapshot
1144            | Action::RequestTrackList
1145            | Action::RequestTransportState
1146            | Action::Quit
1147            | Action::Log { .. }
1148            | Action::Play
1149            | Action::Pause
1150            | Action::Stop
1151            | Action::TransportPosition(_)
1152            | Action::JumpToEnd
1153            | Action::SetLoopEnabled(_)
1154            | Action::SetLoopRange(_)
1155            | Action::SetPunchEnabled(_)
1156            | Action::SetPunchRange(_)
1157            | Action::SetMetronomeEnabled(_)
1158            | Action::SetTempo(_)
1159            | Action::SetTimeSignature { .. }
1160            | Action::SetTempoMap { .. }
1161            | Action::SetOscEnabled(_)
1162            | Action::SetClipPlaybackEnabled(_)
1163            | Action::SetRecordEnabled(_)
1164            | Action::SetStepRecording(_)
1165            | Action::StepRecordMidiNote { .. }
1166            | Action::SetClipIdentity { .. }
1167            | Action::SetClipGainDb { .. }
1168            | Action::SetSessionPath(_)
1169            | Action::ClearHistory
1170            | Action::BeginSessionRestore
1171            | Action::PianoKey { .. }
1172            | Action::ModifyMidiNotes { .. }
1173            | Action::ModifyMidiControllers { .. }
1174            | Action::DeleteMidiControllers { .. }
1175            | Action::InsertMidiControllers { .. }
1176            | Action::DeleteMidiNotes { .. }
1177            | Action::InsertMidiNotes { .. }
1178            | Action::SetMidiSysExEvents { .. }
1179            | Action::Session(_) => {
1180                self.handle_request(a).await;
1181            }
1182            #[cfg(unix)]
1183            Action::ListLv2Plugins => {
1184                self.handle_request(a).await;
1185            }
1186            Action::ListVst3Plugins => {
1187                self.handle_request(a).await;
1188            }
1189            Action::ListClapPlugins => {
1190                self.handle_request(a).await;
1191            }
1192            Action::ListClapPluginsWithCapabilities => {
1193                self.handle_request(a).await;
1194            }
1195            _ => {
1196                self.pending_requests.push_back(a);
1197                if self.can_schedule_hw_cycle() {
1198                    self.request_hw_cycle().await;
1199                } else {
1200                    while let Some(next) = self.pending_requests.pop_front() {
1201                        self.handle_request(next).await;
1202                    }
1203                }
1204            }
1205        };
1206        self.publish_clap_state_dirty().await;
1207    }
1208
1209    pub(crate) fn spawn_plugin_host_stderr_reader(
1210        &self,
1211        stderr: std::process::ChildStderr,
1212        source: String,
1213    ) {
1214        let tx = self.tx.clone();
1215        std::thread::spawn(move || {
1216            use std::io::{BufRead, BufReader};
1217            let reader = BufReader::new(stderr);
1218            for line in reader.lines() {
1219                if let Ok(line) = line
1220                    && !line.is_empty()
1221                {
1222                    let _ = tx.blocking_send(Message::Request(Action::Log {
1223                        source: source.clone(),
1224                        message: line,
1225                    }));
1226                }
1227            }
1228        });
1229    }
1230
1231    pub(crate) fn set_osc_enabled_with<F>(
1232        &mut self,
1233        enabled: bool,
1234        start_server: F,
1235    ) -> Result<(), String>
1236    where
1237        F: FnOnce(Sender<Message>) -> Result<OscServer, String>,
1238    {
1239        if enabled {
1240            if self.osc_server.is_none() {
1241                self.osc_server = Some(start_server(self.tx.clone())?);
1242            }
1243        } else if let Some(mut server) = self.osc_server.take() {
1244            server.stop();
1245        }
1246        Ok(())
1247    }
1248
1249    pub(crate) async fn request_hw_cycle(&mut self) {
1250        if self.awaiting_hwfinished {
1251            tracing::debug!(
1252                playing = self.playing,
1253                transport_running = self.transport_running,
1254                transport_sample = self.transport_sample,
1255                session_transport_sample = self.session_transport_sample,
1256                cycle_samples = self.current_cycle_samples(),
1257                "request_hw_cycle skipped because HWFinished is still pending"
1258            );
1259            return;
1260        }
1261        self.mix_audio_preview_into_hw_outputs();
1262        tracing::debug!(
1263            playing = self.playing,
1264            transport_running = self.transport_running,
1265            transport_sample = self.transport_sample,
1266            session_transport_sample = self.session_transport_sample,
1267            cycle_samples = self.current_cycle_samples(),
1268            "request_hw_cycle sending TracksFinished"
1269        );
1270        self.apply_hw_out_gain_and_meter().await;
1271        self.publish_meter_snapshot_if_due();
1272        if let Some((after_frames, loop_start, cycle_end_sample)) =
1273            self.scheduled_loop_wrap_for_next_cycle()
1274        {
1275            self.notified_loop_wrap_sample = Some(cycle_end_sample);
1276            self.notify_clients(Ok(Action::TransportPositionAt {
1277                sample: loop_start,
1278                after_frames,
1279            }))
1280            .await;
1281        } else {
1282            self.notified_loop_wrap_sample = None;
1283        }
1284        if let Some(worker) = &self.hw_worker {
1285            if !self.pending_hw_midi_out_events_by_device.is_empty() {
1286                let out_events = std::mem::take(&mut self.pending_hw_midi_out_events_by_device);
1287                if let Err(e) = worker.tx.send(Message::HWMidiOutEvents(out_events)).await {
1288                    error!("Error sending HWMidiOutEvents {e}");
1289                }
1290            }
1291            match worker.tx.send(Message::TracksFinished).await {
1292                Ok(_) => {
1293                    self.awaiting_hwfinished = true;
1294                }
1295                Err(e) => {
1296                    error!("Error sending TracksFinished {e}");
1297                }
1298            }
1299        }
1300    }
1301
1302    fn mix_audio_preview_into_hw_outputs(&mut self) {
1303        let cycle_samples = self.current_cycle_samples();
1304        if cycle_samples == 0 {
1305            return;
1306        }
1307        let plan = self.executor.plan().clone();
1308        let Some(preview) = self.audio_preview.as_mut() else {
1309            return;
1310        };
1311        let channels = preview.channels.max(1);
1312        let total_frames = preview.samples.len() / channels;
1313        if preview.cursor >= total_frames {
1314            self.audio_preview = None;
1315            return;
1316        }
1317
1318        for &(buffer, channel) in &plan.hw_out_map {
1319            // Safety: request_hw_cycle runs after all render-plan producers
1320            // completed for this hardware cycle and before the hardware
1321            // backend reads the output arena.
1322            let dst = unsafe { &mut *plan.buffer_ptr(buffer) };
1323            let frames = cycle_samples.min(dst.len());
1324            dst[..frames].fill(0.0);
1325            let source_channel = channel.min(channels - 1);
1326            for (frame, out) in dst.iter_mut().take(frames).enumerate() {
1327                let source_frame = preview.cursor + frame;
1328                if source_frame >= total_frames {
1329                    break;
1330                }
1331                let sample_index = source_frame * channels + source_channel;
1332                *out = preview.samples.get(sample_index).copied().unwrap_or(0.0);
1333            }
1334        }
1335
1336        preview.cursor = preview.cursor.saturating_add(cycle_samples);
1337        if preview.cursor >= total_frames {
1338            self.audio_preview = None;
1339        }
1340    }
1341
1342    pub(crate) fn should_publish_hw_out_meters(&mut self) -> bool {
1343        let now = Instant::now();
1344        match self.last_hw_out_meter_publish {
1345            Some(last) if now.duration_since(last) < Self::METER_PUBLISH_INTERVAL => false,
1346            _ => {
1347                self.last_hw_out_meter_publish = Some(now);
1348                true
1349            }
1350        }
1351    }
1352
1353    pub(crate) fn should_publish_track_meters(&mut self) -> bool {
1354        let now = Instant::now();
1355        match self.last_track_meter_publish {
1356            Some(last) if now.duration_since(last) < Self::METER_PUBLISH_INTERVAL => false,
1357            _ => {
1358                self.last_track_meter_publish = Some(now);
1359                true
1360            }
1361        }
1362    }
1363
1364    pub(crate) fn should_publish_hw_out_linear(&mut self, peaks_linear: &[f32]) -> bool {
1365        #[cfg(unix)]
1366        {
1367            self.hw_out_meter_publish_phase = !self.hw_out_meter_publish_phase;
1368            if !self.hw_out_meter_publish_phase {
1369                return false;
1370            }
1371            let changed = if self.last_hw_out_meter_linear.len() != peaks_linear.len() {
1372                true
1373            } else {
1374                self.last_hw_out_meter_linear
1375                    .iter()
1376                    .zip(peaks_linear.iter())
1377                    .any(|(prev, next)| (prev - next).abs() >= Self::HW_OUT_METER_LINEAR_EPSILON)
1378            };
1379            if !changed {
1380                return false;
1381            }
1382            self.last_hw_out_meter_linear.clear();
1383            self.last_hw_out_meter_linear
1384                .extend_from_slice(peaks_linear);
1385            true
1386        }
1387        #[cfg(not(unix))]
1388        {
1389            let _ = peaks_linear;
1390            false
1391        }
1392    }
1393
1394    pub(crate) async fn maybe_notify_hw_out_meter(&mut self, _meter_db: Vec<f32>) {
1395        {}
1396    }
1397
1398    pub(crate) async fn apply_hw_out_gain_and_meter(&mut self) {
1399        let gain = if self.hw_out_muted {
1400            0.0
1401        } else {
1402            10.0_f32.powf(self.hw_out_level_db / 20.0)
1403        };
1404
1405        // Send master gain/balance to the driver. If there is no active audio
1406        // backend there is nothing further to meter.
1407        if let Some(worker) = &self.hw_worker {
1408            let _ = worker
1409                .tx
1410                .send(Message::HWSetOutputGainBalance {
1411                    gain,
1412                    balance: self.hw_out_balance,
1413                })
1414                .await;
1415        } else {
1416            #[cfg(unix)]
1417            {
1418                if let Some(jack) = self.jack_runtime.as_ref() {
1419                    jack.set_output_gain_linear(gain);
1420                    jack.set_output_balance(self.hw_out_balance);
1421                } else {
1422                    return;
1423                }
1424            }
1425            #[cfg(not(unix))]
1426            {
1427                return;
1428            }
1429        }
1430
1431        if self.meter_decay_after_stop.is_some() {
1432            return;
1433        }
1434
1435        // Loudness must be fed every cycle so integrated LUFS is accurate.
1436        self.feed_hw_out_loudness_meter();
1437
1438        let should_notify_interval = self.should_publish_hw_out_meters();
1439        if !should_notify_interval {
1440            return;
1441        }
1442
1443        let plan = self.executor.plan().clone();
1444        let peaks_linear =
1445            crate::hw::common::output_meter_linear_from_plan(&plan, gain, self.hw_out_balance);
1446        if self.hw_out_peak_hold_linear.len() != peaks_linear.len() {
1447            self.hw_out_peak_hold_linear.resize(peaks_linear.len(), 0.0);
1448        }
1449        let mut held_peaks = Vec::with_capacity(peaks_linear.len());
1450        for (idx, peak_now) in peaks_linear.iter().copied().enumerate() {
1451            let held = self.hw_out_peak_hold_linear[idx] * 0.92;
1452            let next = peak_now.max(held);
1453            self.hw_out_peak_hold_linear[idx] = next;
1454            held_peaks.push(next);
1455        }
1456        let should_notify = self.should_publish_hw_out_linear(&held_peaks);
1457        let meter_db: Vec<f32> = held_peaks
1458            .into_iter()
1459            .map(Self::meter_linear_to_db)
1460            .collect();
1461        self.latest_hw_out_meter_db = Arc::new(meter_db.clone());
1462        if should_notify {
1463            self.maybe_notify_hw_out_meter(meter_db).await;
1464        }
1465    }
1466
1467    fn feed_hw_out_loudness_meter(&mut self) {
1468        let Some(info) = self.hw_driver_info else {
1469            return;
1470        };
1471        let plan = self.executor.plan();
1472        let channels = plan.hw_out_map.len();
1473        if channels == 0 {
1474            return;
1475        }
1476        let sample_rate = info.sample_rate as u32;
1477
1478        let needs_recreate = self
1479            .hw_out_loudness_meter
1480            .as_ref()
1481            .is_none_or(|m| m.channels() != channels || m.sample_rate() != sample_rate);
1482        if needs_recreate {
1483            self.hw_out_loudness_meter =
1484                crate::loudness::LoudnessMeter::new(channels, sample_rate).ok();
1485        }
1486
1487        if let Some(meter) = self.hw_out_loudness_meter.as_mut() {
1488            let interleaved = crate::hw::common::interleaved_hw_out_samples(plan);
1489            meter.feed_interleaved(&interleaved);
1490        }
1491    }
1492
1493    fn update_hw_out_lufs_readout(&mut self) {
1494        self.latest_hw_out_lufs = self
1495            .hw_out_loudness_meter
1496            .as_ref()
1497            .map(|meter| meter.values());
1498    }
1499
1500    pub(crate) fn preload_track_clips_spawn(&self) {
1501        let tracks: Vec<_> = self
1502            .state_snapshot
1503            .load_full()
1504            .tracks
1505            .values()
1506            .cloned()
1507            .collect();
1508        for track in tracks {
1509            tokio::task::spawn_blocking(move || {
1510                track.lock().preload_clips();
1511            });
1512        }
1513    }
1514
1515    /// Preload all track clips and wait for completion. Used on transport
1516    /// play paths: the audio cycle must not start until clip caches are
1517    /// populated, otherwise the preload tasks' `&mut Track` access races the
1518    /// RT worker and cold-cache reads do disk I/O on the audio path.
1519    ///
1520    /// Returns an owned future rather than borrowing `self`: `Engine` is not
1521    /// `Sync` (it owns JackRuntime/MidiHub directly since Phase 6), so an
1522    /// `async fn(&self)` future would be non-`Send`.
1523    pub(crate) fn preload_track_clips(
1524        &self,
1525    ) -> impl std::future::Future<Output = ()> + Send + 'static {
1526        let tracks: Vec<_> = self
1527            .state_snapshot
1528            .load_full()
1529            .tracks
1530            .values()
1531            .cloned()
1532            .collect();
1533        Self::preload_track_handles(tracks)
1534    }
1535
1536    async fn preload_track_handles(tracks: Vec<crate::state::TrackHandle>) {
1537        if tracks.is_empty() {
1538            return;
1539        }
1540        let mut handles = Vec::with_capacity(tracks.len());
1541        for track in tracks {
1542            handles.push(tokio::task::spawn_blocking(move || {
1543                track.lock().preload_clips();
1544            }));
1545        }
1546        for handle in handles {
1547            if let Err(e) = handle.await {
1548                tracing::warn!("Clip preload task panicked: {e}");
1549            }
1550        }
1551    }
1552
1553    pub(crate) fn task_track_name(task: &ProcessTask) -> String {
1554        match task {
1555            ProcessTask::Track(t) | ProcessTask::FolderInput(t) | ProcessTask::FolderOutput(t) => {
1556                t.lock().name.clone()
1557            }
1558            ProcessTask::Plugin { track, .. } => track.lock().name.clone(),
1559        }
1560    }
1561
1562    pub(crate) fn prepare_task_track(&self, task: &ProcessTask) {
1563        let track = match task {
1564            ProcessTask::Track(t) | ProcessTask::FolderInput(t) | ProcessTask::FolderOutput(t) => t,
1565            ProcessTask::Plugin { track, .. } => track,
1566        };
1567        let mut t = track.lock();
1568        let transport_sample = if self.session_clip_playback_enabled && self.playing {
1569            self.session_transport_sample
1570        } else {
1571            self.transport_sample
1572        };
1573        t.set_transport_sample(transport_sample);
1574        t.set_loop_config(self.loop_enabled, self.loop_range_samples);
1575        t.set_transport_timing(self.tempo_bpm, self.tsig_num, self.tsig_denom);
1576        t.set_clip_playback_enabled(self.clip_playback_enabled && self.playing);
1577        t.set_session_clip_playback_enabled(self.session_clip_playback_enabled && self.playing);
1578        t.set_record_tap_enabled(self.playing && self.record_enabled);
1579        t.audio.set_processing(true);
1580    }
1581
1582    /// Dispatch queued node jobs to ready workers, buffering the rest until
1583    /// a worker reports `Ready`. Returns true if the cycle completed while
1584    /// dispatching (only possible via abandoned nodes).
1585    pub(crate) async fn dispatch_node_jobs(&mut self, jobs: Vec<crate::executor::NodeJob>) -> bool {
1586        self.pending_node_jobs.extend(jobs);
1587        let mut cycle_complete = false;
1588        while !self.pending_node_jobs.is_empty() {
1589            let Some(worker_index) = self.take_ready_worker_index() else {
1590                break;
1591            };
1592            let Some(job) = self.pending_node_jobs.pop_front() else {
1593                break;
1594            };
1595            if let Some(crate::render_plan::Op::Task { task, .. }) =
1596                job.plan.nodes.get(job.node as usize)
1597            {
1598                self.prepare_task_track(task);
1599            }
1600            let worker = &mut self.workers[worker_index];
1601            if let Some(node_job_tx) = worker.node_job_tx.as_mut() {
1602                match node_job_tx.push(job) {
1603                    Ok(()) => {
1604                        if let Some(thread) = &worker.node_thread {
1605                            thread.unpark();
1606                        }
1607                    }
1608                    Err(rtrb::PushError::Full(job)) => {
1609                        self.pending_node_jobs.push_front(job);
1610                        self.push_ready_worker(worker_index);
1611                        break;
1612                    }
1613                }
1614            } else {
1615                let node = job.node;
1616                error!("Worker {worker_index} has no node-job mailbox");
1617                let outcome = self.executor.abandon_node(node, Instant::now());
1618                self.log_silenced_nodes(&outcome.silenced);
1619                cycle_complete |= outcome.cycle_complete;
1620                self.pending_node_jobs.extend(outcome.jobs);
1621            }
1622        }
1623        cycle_complete
1624    }
1625
1626    /// Start the next audio cycle: pull any published plan, copy hardware
1627    /// inputs, and dispatch the seed jobs. Returns true when the cycle
1628    /// completed instantly (empty plan) and `on_all_tracks_finished` ran.
1629    /// Ensure the metronome track's source port and output wiring exist
1630    /// before tasks are dispatched. Runs on the dispatcher at cycle top so
1631    /// `AudioIO.connections` is never mutated from a worker thread; marks
1632    /// the plan dirty when the wiring changed.
1633    fn ensure_metronome_wiring(&mut self) {
1634        let Some(track) = self
1635            .state_snapshot
1636            .load_full()
1637            .tracks
1638            .get(Self::METRONOME_TRACK)
1639            .cloned()
1640        else {
1641            return;
1642        };
1643        let frames = self.current_cycle_samples();
1644        let (_, changed) = track.lock().ensure_metronome_source(frames);
1645        if changed {
1646            self.plan_builder.mark_dirty();
1647        }
1648    }
1649
1650    pub(crate) async fn start_plan_cycle(&mut self) -> bool {
1651        // While a bounce job exists, plan cycles are suspended: the bounce
1652        // worker renders through live track bodies and must be their only
1653        // mutator (LOCKLESS.md Phase 5, 5b-iii).
1654        if !self.playing || !self.executor.cycle_complete() || !self.offline_bounce_jobs.is_empty()
1655        {
1656            return false;
1657        }
1658        self.refresh_realtime_infection();
1659        self.ensure_metronome_wiring();
1660        let jobs = self.executor.start_cycle(Instant::now());
1661        if self.dispatch_node_jobs(jobs).await {
1662            self.on_all_tracks_finished().await;
1663            return true;
1664        }
1665        false
1666    }
1667
1668    /// Handle a worker completion: cascade the executor, publish meters and
1669    /// parameter echoes, force timed-out nodes, and finish the cycle when
1670    /// all nodes are done.
1671    pub(crate) async fn on_node_done(
1672        &mut self,
1673        worker_id: usize,
1674        epoch: u64,
1675        node: u32,
1676        output_linear: Vec<f32>,
1677        parameter_updates: Vec<Action>,
1678        latency_changed: bool,
1679    ) {
1680        self.push_ready_worker(worker_id);
1681        let mut complete = self.dispatch_node_jobs(Vec::new()).await;
1682        if epoch != self.executor.epoch() {
1683            tracing::debug!(
1684                "dropping stale NodeDone (epoch {} vs {}) for node {}",
1685                epoch,
1686                self.executor.epoch(),
1687                node
1688            );
1689            return;
1690        }
1691        if latency_changed {
1692            self.publish_state_snapshot();
1693            self.plan_builder.mark_dirty();
1694        }
1695        let plan = self.executor.plan().clone();
1696        if let Some(crate::render_plan::Op::Task { task, .. }) = plan.nodes.get(node as usize) {
1697            let track_name = Self::task_track_name(task);
1698            self.track_meter_linear_by_track
1699                .insert(track_name, output_linear);
1700        }
1701        for action in parameter_updates {
1702            self.notify_clients(Ok(action)).await;
1703        }
1704        let now = Instant::now();
1705        let (jobs, done) = self.executor.on_node_done(epoch, node, now);
1706        complete |= done;
1707        complete |= self.dispatch_node_jobs(jobs).await;
1708        let outcome = self
1709            .executor
1710            .force_timeouts(now, Self::TRACK_PROCESS_TIMEOUT);
1711        self.log_silenced_nodes(&outcome.silenced);
1712        complete |= self.dispatch_node_jobs(outcome.jobs).await;
1713        complete |= outcome.cycle_complete;
1714        if complete {
1715            self.on_all_tracks_finished().await;
1716        }
1717    }
1718
1719    pub(crate) async fn poll_stopped_plugin_parameter_echoes(&mut self) {
1720        if self.playing || self.transport_running {
1721            return;
1722        }
1723
1724        let state = self.state_snapshot.load_full();
1725        let mut updates = Vec::new();
1726        for track in state.tracks.values() {
1727            updates.extend(track.lock().drain_plugin_parameter_echoes());
1728        }
1729        drop(state);
1730
1731        for action in updates {
1732            self.notify_clients(Ok(action)).await;
1733        }
1734    }
1735
1736    pub(crate) async fn poll_node_worker_results(&mut self) {
1737        let mut results = Vec::new();
1738        for worker in &mut self.workers {
1739            if let Some(rx) = worker.node_result_rx.as_mut() {
1740                while let Ok(result) = rx.pop() {
1741                    results.push(result);
1742                }
1743            }
1744        }
1745        for result in results {
1746            self.on_node_done(
1747                result.worker_id,
1748                result.epoch,
1749                result.node,
1750                result.output_linear,
1751                result.parameter_updates,
1752                result.latency_changed,
1753            )
1754            .await;
1755        }
1756    }
1757
1758    pub(crate) async fn poll_jack_hw_finished(&mut self) {
1759        #[cfg(unix)]
1760        {
1761            let finished = self
1762                .jack_runtime
1763                .as_ref()
1764                .map(|jack| jack.take_hw_finished_count())
1765                .unwrap_or(0);
1766            if finished > 0 {
1767                self.handle_hw_finished().await;
1768            }
1769        }
1770    }
1771
1772    #[cfg(unix)]
1773    pub(crate) async fn handle_hw_finished(&mut self) {
1774        if !self.awaiting_hwfinished {
1775            return;
1776        }
1777        tracing::debug!(
1778            playing = self.playing,
1779            transport_running = self.transport_running,
1780            transport_sample = self.transport_sample,
1781            session_transport_sample = self.session_transport_sample,
1782            cycle_samples = self.current_cycle_samples(),
1783            "HWFinished handling"
1784        );
1785        self.handling_hwfinished = true;
1786        self.awaiting_hwfinished = false;
1787        #[cfg(unix)]
1788        {
1789            if let Some(jack) = self.jack_runtime.as_mut() {
1790                if !self.pending_hw_midi_out_events.is_empty() {
1791                    let out_events = std::mem::take(&mut self.pending_hw_midi_out_events);
1792                    jack.write_events(&out_events);
1793                }
1794                let mut in_events = vec![];
1795                jack.read_events_into(&mut in_events);
1796                if !in_events.is_empty() {
1797                    self.pending_hw_midi_events.extend(in_events);
1798                }
1799                let dropped = jack.take_midi_events_dropped();
1800                if dropped > 0 {
1801                    tracing::warn!(
1802                        "JACK MIDI ring full; {dropped} events dropped since last cycle"
1803                    );
1804                }
1805            }
1806        }
1807        #[cfg(unix)]
1808        if self.jack_runtime.is_some() {
1809            self.sync_from_jack_transport().await;
1810        }
1811        while let Some(a) = self.pending_requests.pop_front() {
1812            self.handle_request(a).await;
1813        }
1814        self.apply_mute_solo_policy();
1815        self.append_recorded_cycle();
1816        self.flush_completed_recordings().await;
1817        let hw_in_routes = self.midi_hw_in_routes.clone();
1818        let pending_hw_in_by_device = self.pending_hw_midi_events_by_device.clone();
1819        let mut reconfigured_tracks = Vec::new();
1820        let state = self.state_snapshot.load_full();
1821        for (track_name, track) in state.tracks.iter() {
1822            let mut track_lock = track.lock();
1823            if self.jack_runtime_is_some() {
1824                if !self.pending_hw_midi_events.is_empty() {
1825                    track_lock.push_hw_midi_events(&self.pending_hw_midi_events);
1826                }
1827            } else {
1828                for route in hw_in_routes.iter().filter(|r| &r.to_track == track_name) {
1829                    if let Some(events) = pending_hw_in_by_device.get(&route.device) {
1830                        track_lock.push_hw_midi_events_to_port(route.to_port, events);
1831                    }
1832                }
1833            }
1834            if track_lock.setup() {
1835                reconfigured_tracks.push(track_name.clone());
1836            }
1837        }
1838        self.publish_track_meters();
1839        self.publish_session_runtime_reports().await;
1840        self.publish_clap_state_dirty().await;
1841        for track_name in reconfigured_tracks {
1842            let track = state.tracks.get(&track_name).cloned();
1843            if let Some(track) = track {
1844                let (plugins, connections, connectable_connections) = {
1845                    let track_lock = track.lock();
1846                    (
1847                        track_lock.plugin_graph_plugins(false),
1848                        track_lock.plugin_graph_connections(),
1849                        track_lock.connectable_connections(),
1850                    )
1851                };
1852                self.notify_clients(Ok(Action::TrackPluginGraph {
1853                    track_name: track_name.clone(),
1854                    plugins,
1855                    connections,
1856                    connectable_connections,
1857                }))
1858                .await;
1859            }
1860        }
1861        self.pending_hw_midi_events.clear();
1862        self.pending_hw_midi_events_by_device.clear();
1863        let cycle_samples = self.current_cycle_samples();
1864        if self.transport_running {
1865            if self.transport_panic_flush_pending {
1866                self.transport_panic_flush_pending = false;
1867            } else if self.transport_restart_pending {
1868                self.transport_restart_pending = false;
1869            } else {
1870                let before = self.transport_sample;
1871                let next = self.transport_sample.saturating_add(cycle_samples);
1872                let normalized = self.normalize_transport_sample(next);
1873                let wrapped = normalized != next;
1874                self.transport_sample = normalized;
1875                tracing::debug!(
1876                    before,
1877                    delta = cycle_samples,
1878                    next,
1879                    normalized,
1880                    wrapped,
1881                    "transport advanced after HWFinished"
1882                );
1883                self.publish_transport_snapshot();
1884                if wrapped {
1885                    if self.notified_loop_wrap_sample == Some(self.transport_sample) {
1886                        self.notified_loop_wrap_sample = None;
1887                    } else {
1888                        self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
1889                            .await;
1890                    }
1891                }
1892            }
1893        } else {
1894            tracing::debug!(
1895                playing = self.playing,
1896                cycle_samples,
1897                "transport not advanced because transport_running is false"
1898            );
1899        }
1900        if self.session_clip_playback_enabled && self.playing {
1901            let before = self.session_transport_sample;
1902            self.session_transport_sample =
1903                self.session_transport_sample.saturating_add(cycle_samples);
1904            tracing::debug!(
1905                before,
1906                delta = cycle_samples,
1907                after = self.session_transport_sample,
1908                "session transport advanced after HWFinished"
1909            );
1910        }
1911        {
1912            let echoes = self.apply_modulators(self.active_transport_sample());
1913            for action in echoes {
1914                self.notify_clients(Ok(action)).await;
1915            }
1916        }
1917        self.apply_mixosc_automation(self.active_transport_sample());
1918        let cycle_started = self.start_plan_cycle().await;
1919        // If a plan cycle is still running, its completion will request the
1920        // hardware cycle. Requesting here would replay stale arena buffers.
1921        if self.hw_worker.is_some()
1922            && !cycle_started
1923            && (self.playing || self.audio_preview.is_some())
1924            && self.executor.cycle_complete()
1925        {
1926            self.request_hw_cycle().await;
1927        }
1928        tracing::debug!(
1929            cycle_started,
1930            hw_worker = self.hw_worker.is_some(),
1931            awaiting_hwfinished = self.awaiting_hwfinished,
1932            executor_complete = self.executor.cycle_complete(),
1933            "HWFinished rearm decision"
1934        );
1935        #[cfg(unix)]
1936        {
1937            if self.jack_runtime.is_some() {
1938                self.awaiting_hwfinished = true;
1939            }
1940        }
1941        self.handling_hwfinished = false;
1942    }
1943
1944    /// Periodic tick (called from the engine loop's interval): force
1945    /// timed-out nodes even when no worker message arrives.
1946    pub(crate) async fn on_executor_tick(&mut self) {
1947        if self.executor.cycle_complete() {
1948            return;
1949        }
1950        let outcome = self
1951            .executor
1952            .force_timeouts(Instant::now(), Self::TRACK_PROCESS_TIMEOUT);
1953        self.log_silenced_nodes(&outcome.silenced);
1954        let mut complete = self.dispatch_node_jobs(outcome.jobs).await;
1955        complete |= outcome.cycle_complete;
1956        if complete {
1957            self.on_all_tracks_finished().await;
1958        }
1959    }
1960
1961    pub(crate) fn log_silenced_nodes(&self, silenced: &[u32]) {
1962        for &node in silenced {
1963            let plan = self.executor.plan();
1964            let name = match plan.nodes.get(node as usize) {
1965                Some(crate::render_plan::Op::Task { task, .. }) => Self::task_track_name(task),
1966                _ => format!("node {node}"),
1967            };
1968            tracing::warn!(
1969                "Node {} ('{}') exceeded process timeout ({} ms); forced silent completion for cycle",
1970                node,
1971                name,
1972                Self::TRACK_PROCESS_TIMEOUT.as_millis()
1973            );
1974        }
1975    }
1976
1977    /// Hand a bounce job to its reserved worker, recording the worker→track
1978    /// mapping so the terminal `Ready(id)` cleans the job up on any path.
1979    pub(crate) async fn send_bounce_job(
1980        &mut self,
1981        worker_index: usize,
1982        job: crate::message::OfflineBounceWork,
1983    ) {
1984        let track_name = job.track_name.clone();
1985        self.bounce_worker_tracks
1986            .insert(worker_index, track_name.clone());
1987        let worker = &self.workers[worker_index];
1988        if let Err(e) = worker.tx.send(Message::ProcessOfflineBounce(job)).await {
1989            self.bounce_worker_tracks.remove(&worker_index);
1990            self.offline_bounce_jobs.remove(&track_name);
1991            self.push_ready_worker(worker_index);
1992            self.notify_clients(Err(format!("Failed to schedule offline bounce: {e}")))
1993                .await;
1994        }
1995    }
1996
1997    /// Requests queued while a bounce was running are replayed once the last
1998    /// bounce job is gone (and plan cycles resume).
1999    async fn drain_pending_requests_if_idle(&mut self) {
2000        if self.offline_bounce_jobs.is_empty() {
2001            while let Some(next) = self.pending_requests.pop_front() {
2002                self.handle_request(next).await;
2003            }
2004        }
2005    }
2006
2007    pub(crate) async fn on_all_tracks_finished(&mut self) {
2008        // Hand deferred bounce jobs to their workers now that no plan cycle
2009        // is in flight (see handle_track_offline_bounce).
2010        let pending = std::mem::take(&mut self.pending_bounce_starts);
2011        for (worker_index, job) in pending {
2012            self.send_bounce_job(worker_index, job).await;
2013        }
2014        if self.transport_restart_pending {
2015            let state = self.state_snapshot.load_full();
2016            for track in state.tracks.values() {
2017                track.lock().take_hw_midi_out_events();
2018            }
2019        } else if self.hw_worker.is_some() {
2020            self.active_hw_notes_cycle_start = self.active_hw_notes_by_track.clone();
2021            let mut out_events = self.collect_hw_midi_output_events_by_device();
2022            if self.loop_enabled
2023                && let Some((_, loop_end)) = self.loop_range_samples
2024            {
2025                let cycle_end = self
2026                    .transport_sample
2027                    .saturating_add(self.current_cycle_samples());
2028                if self.transport_sample < loop_end && cycle_end >= loop_end {
2029                    let wrap_frame = loop_end
2030                        .saturating_sub(self.transport_sample)
2031                        .min(self.current_cycle_samples())
2032                        as u32;
2033                    out_events.extend(self.note_off_events_for_active_snapshot(
2034                        &self.active_hw_notes_cycle_start,
2035                        wrap_frame,
2036                    ));
2037                    out_events.sort_by(|a, b| {
2038                        a.event
2039                            .frame
2040                            .cmp(&b.event.frame)
2041                            .then_with(|| a.device.cmp(&b.device))
2042                    });
2043                }
2044            }
2045            self.pending_hw_midi_out_events_by_device.extend(out_events);
2046        } else {
2047            self.pending_hw_midi_out_events = self.collect_hw_midi_output_events();
2048        }
2049        self.request_hw_cycle().await;
2050    }
2051
2052    pub(crate) fn take_ready_worker_index(&mut self) -> Option<usize> {
2053        while !self.ready_workers.is_empty() {
2054            let worker_index = self.ready_workers.remove(0);
2055            if worker_index < self.workers.len() {
2056                return Some(worker_index);
2057            }
2058        }
2059        None
2060    }
2061
2062    pub(crate) fn push_ready_worker(&mut self, worker_index: usize) {
2063        self.ready_workers.push(worker_index);
2064    }
2065
2066    pub(crate) fn publish_track_meters(&mut self) {
2067        if !self.should_publish_track_meters() {
2068            return;
2069        }
2070        if self.meter_decay_after_stop.is_some() {
2071            self.update_meter_decay_after_stop();
2072            return;
2073        }
2074        let tracks: Vec<(String, crate::state::TrackHandle)> = self
2075            .state_snapshot
2076            .load_full()
2077            .tracks
2078            .iter()
2079            .map(|(name, track)| (name.clone(), track.clone()))
2080            .collect();
2081        let mut snapshot = Vec::with_capacity(tracks.len());
2082        for (name, track) in &tracks {
2083            let linear = self
2084                .track_meter_linear_by_track
2085                .get(name)
2086                .cloned()
2087                .unwrap_or_else(|| track.lock().output_meter_linear());
2088            let output_db = linear
2089                .iter()
2090                .copied()
2091                .map(Self::meter_linear_to_db)
2092                .collect::<Vec<_>>();
2093            snapshot.push((name.clone(), output_db));
2094        }
2095        self.latest_track_meter_snapshot = Arc::new(snapshot);
2096    }
2097
2098    fn record_session_completed_clip_pass(
2099        &mut self,
2100        track_name: String,
2101        scene_index: usize,
2102        clip_id: String,
2103        pass_index: usize,
2104        start_sample: usize,
2105        length_samples: usize,
2106    ) {
2107        let key = (
2108            track_name.clone(),
2109            scene_index,
2110            clip_id.clone(),
2111            pass_index,
2112            start_sample,
2113        );
2114        if self.session_reported_clip_passes.insert(key) {
2115            self.session_completed_clip_passes
2116                .push(crate::meter::SessionCompletedClipPass {
2117                    track_name,
2118                    scene_index,
2119                    clip_id,
2120                    pass_index,
2121                    start_sample,
2122                    length_samples,
2123                });
2124        }
2125    }
2126
2127    fn record_completed_session_scene_span(
2128        &mut self,
2129        tracks: &HashMap<String, crate::state::TrackHandle>,
2130        scene_index: usize,
2131        previous_scene: Option<usize>,
2132        scene_start_sample: usize,
2133        elapsed_samples: usize,
2134    ) {
2135        for (track_name, track) in tracks {
2136            let track = track.lock();
2137            let slot = track.rt.session_slots.get(&scene_index);
2138            let prev_slot = previous_scene.and_then(|scene| track.rt.session_slots.get(&scene));
2139            let playing_clip_id = track
2140                .rt
2141                .playing_session_clips
2142                .last()
2143                .map(|clip| clip.clip_id.as_str());
2144            let clip_id = match (slot, prev_slot) {
2145                (Some(slot), _) if slot.play_enabled => Some(slot.clip_id.as_str()),
2146                (Some(slot), _) if slot.stop_enabled => None,
2147                (_, Some(prev_slot)) if prev_slot.play_enabled => Some(prev_slot.clip_id.as_str()),
2148                (_, Some(prev_slot)) if prev_slot.stop_enabled => None,
2149                _ => playing_clip_id,
2150            }
2151            .filter(|clip_id| !clip_id.is_empty());
2152            let Some(clip_id) = clip_id else { continue };
2153            let clip_length = track
2154                .session_clip_length(clip_id, crate::kind::Kind::Audio)
2155                .or_else(|| track.session_clip_length(clip_id, crate::kind::Kind::MIDI))
2156                .unwrap_or(0);
2157            if clip_length == 0 {
2158                continue;
2159            }
2160            let completed_passes = elapsed_samples / clip_length;
2161            for pass_index in 0..completed_passes {
2162                self.record_session_completed_clip_pass(
2163                    track_name.clone(),
2164                    scene_index,
2165                    clip_id.to_string(),
2166                    pass_index,
2167                    scene_start_sample.saturating_add(pass_index * clip_length),
2168                    clip_length,
2169                );
2170            }
2171        }
2172    }
2173
2174    pub(crate) async fn publish_session_runtime_reports(&mut self) {
2175        let mut current = HashMap::<(String, usize), (SessionSlotState, usize, usize)>::new();
2176        {
2177            let state = self.state_snapshot.load_full();
2178            if let Some((queued_scene, queued_launch_at)) = self.session_scene_queue {
2179                // Drop the queue marker once it has fired: no matching
2180                // pending launch or scheduled stop remains and the session
2181                // transport has passed the launch time. The transport check
2182                // matters when the queued scene scheduled nothing (all
2183                // tracks continue): the marker must still hold until the
2184                // launch time arrives. The fired scene becomes the current
2185                // one.
2186                let still_pending = state.tracks.values().any(|track| {
2187                    let track = track.lock();
2188                    track.rt.pending_session_launches.iter().any(|launch| {
2189                        launch.scene_index == queued_scene
2190                            && launch.launch_at_sample == queued_launch_at
2191                    }) || track
2192                        .rt
2193                        .playing_session_clips
2194                        .iter()
2195                        .any(|clip| clip.stop_at_sample == Some(queued_launch_at))
2196                });
2197                if !still_pending && self.session_transport_sample >= queued_launch_at {
2198                    if let Some(current_scene) = self.session_current_scene {
2199                        let scene_start = self.session_current_scene_start_sample;
2200                        let elapsed = queued_launch_at.saturating_sub(scene_start);
2201                        self.record_completed_session_scene_span(
2202                            &state.tracks,
2203                            current_scene,
2204                            self.session_current_scene_previous_scene,
2205                            scene_start,
2206                            elapsed,
2207                        );
2208                    }
2209                    self.session_scene_queue = None;
2210                    self.session_current_scene_previous_scene = self.session_current_scene;
2211                    self.session_current_scene = Some(queued_scene);
2212                    self.session_current_scene_start_sample = queued_launch_at;
2213                    self.session_current_scene_length_samples =
2214                        self.session_scene_queue_length_samples;
2215                    self.session_scene_queue_length_samples = 0;
2216                }
2217            }
2218            if let Some(current_scene) = self.session_current_scene
2219                && self.session_current_scene_length_samples > 0
2220            {
2221                self.record_completed_session_scene_span(
2222                    &state.tracks,
2223                    current_scene,
2224                    self.session_current_scene_previous_scene,
2225                    self.session_current_scene_start_sample,
2226                    self.session_transport_sample
2227                        .saturating_sub(self.session_current_scene_start_sample),
2228                );
2229            }
2230            for (track_name, track) in &state.tracks {
2231                let track = track.lock();
2232                for launch in &track.rt.pending_session_launches {
2233                    current.insert(
2234                        (track_name.clone(), launch.scene_index),
2235                        (SessionSlotState::Queued, 0, 0),
2236                    );
2237                }
2238                for clip in &track.rt.playing_session_clips {
2239                    if self.session_current_scene_length_samples == 0
2240                        && let Some(clip_length) =
2241                            track.session_clip_length(&clip.clip_id, clip.kind)
2242                    {
2243                        let scene_report = self
2244                            .session_current_scene
2245                            .map(|_| self.session_current_scene_length_samples)
2246                            .filter(|length| *length > 0)
2247                            .map(|length| {
2248                                (
2249                                    self.session_current_scene.unwrap_or(clip.scene_index),
2250                                    self.session_current_scene_start_sample,
2251                                    self.session_transport_sample
2252                                        .saturating_sub(self.session_current_scene_start_sample),
2253                                    length,
2254                                )
2255                            });
2256                        let launch_sample = self
2257                            .session_transport_sample
2258                            .saturating_sub(clip.elapsed_samples);
2259                        let (
2260                            report_scene_index,
2261                            report_start_sample,
2262                            report_elapsed,
2263                            report_length,
2264                        ) = scene_report.unwrap_or((
2265                            clip.scene_index,
2266                            launch_sample,
2267                            clip.elapsed_samples,
2268                            clip_length,
2269                        ));
2270                        if report_length == 0 {
2271                            continue;
2272                        }
2273                        let completed_passes = report_elapsed / report_length;
2274                        for pass_index in 0..completed_passes {
2275                            self.record_session_completed_clip_pass(
2276                                track_name.clone(),
2277                                report_scene_index,
2278                                clip.clip_id.clone(),
2279                                pass_index,
2280                                report_start_sample.saturating_add(pass_index * report_length),
2281                                report_length,
2282                            );
2283                        }
2284                    }
2285                    current.insert(
2286                        (track_name.clone(), clip.scene_index),
2287                        (
2288                            SessionSlotState::Playing,
2289                            clip.play_position_samples,
2290                            clip.elapsed_samples,
2291                        ),
2292                    );
2293                }
2294            }
2295        }
2296
2297        if self
2298            .last_session_report_publish
2299            .is_some_and(|t| t.elapsed() < Self::SESSION_RUNTIME_REPORT_INTERVAL)
2300        {
2301            return;
2302        }
2303
2304        let snapshot = self.session_runtime_snapshot_producer.write_buffer();
2305        snapshot.session_sample = self.session_transport_sample;
2306        snapshot.slots.clear();
2307        snapshot.slots.extend(current.iter().map(
2308            |((track_name, scene_index), (state, play_position_samples, elapsed_samples))| {
2309                crate::meter::SessionRuntimeSlotSnapshot {
2310                    track_name: track_name.clone(),
2311                    scene_index: *scene_index,
2312                    state: *state,
2313                    play_position_samples: *play_position_samples,
2314                    elapsed_samples: *elapsed_samples,
2315                }
2316            },
2317        ));
2318        snapshot.completed_clip_passes = self.session_completed_clip_passes.clone();
2319        snapshot.current_scene = self.session_current_scene;
2320        self.session_runtime_snapshot_producer.publish();
2321        self.last_session_report_publish = Some(Instant::now());
2322    }
2323
2324    pub(crate) async fn publish_clap_state_dirty(&mut self) {
2325        let tracks: Vec<(String, crate::state::TrackHandle)> = self
2326            .state_snapshot
2327            .load_full()
2328            .tracks
2329            .iter()
2330            .map(|(name, track)| (name.clone(), track.clone()))
2331            .collect();
2332        for (track_name, track) in &tracks {
2333            let dirty = track.lock().take_dirty_clap_instances();
2334            for instance_id in dirty {
2335                self.notify_clients(Ok(Action::TrackClapStateDirty {
2336                    track_name: track_name.clone(),
2337                    instance_id,
2338                }))
2339                .await;
2340            }
2341        }
2342    }
2343
2344    pub(crate) fn reset_meters_after_stop(&mut self) {
2345        self.last_hw_out_meter_publish = None;
2346        self.last_track_meter_publish = None;
2347        self.last_meter_snapshot_publish = None;
2348        #[cfg(unix)]
2349        {
2350            self.last_hw_out_meter_linear.clear();
2351        }
2352
2353        let tracks: Vec<(String, crate::state::TrackHandle)> = self
2354            .state_snapshot
2355            .load_full()
2356            .tracks
2357            .iter()
2358            .map(|(name, track)| (name.clone(), track.clone()))
2359            .collect();
2360        let mut track_linear = Vec::with_capacity(tracks.len());
2361        for (name, track) in tracks {
2362            let linear = self
2363                .track_meter_linear_by_track
2364                .get(&name)
2365                .cloned()
2366                .unwrap_or_else(|| track.lock().output_meter_linear());
2367            track_linear.push((name, linear));
2368        }
2369        let hw_out_linear = if self.hw_out_peak_hold_linear.is_empty() {
2370            self.latest_hw_out_meter_db
2371                .iter()
2372                .copied()
2373                .map(Self::meter_db_to_linear)
2374                .collect()
2375        } else {
2376            self.hw_out_peak_hold_linear.clone()
2377        };
2378        self.meter_decay_after_stop = Some(MeterDecay {
2379            started_at: Instant::now(),
2380            hw_out_linear,
2381            track_linear,
2382        });
2383        self.update_meter_decay_after_stop();
2384        self.publish_meter_snapshot();
2385    }
2386
2387    pub(crate) fn update_meter_decay_after_stop(&mut self) {
2388        let Some(decay) = self.meter_decay_after_stop.as_ref() else {
2389            return;
2390        };
2391        let elapsed = decay.started_at.elapsed();
2392        if elapsed >= Self::METER_DECAY_AFTER_STOP {
2393            self.finish_meter_decay_after_stop();
2394            return;
2395        }
2396
2397        let remaining = 1.0 - (elapsed.as_secs_f32() / Self::METER_DECAY_AFTER_STOP.as_secs_f32());
2398        let hw_out_linear = decay
2399            .hw_out_linear
2400            .iter()
2401            .copied()
2402            .map(|value| value * remaining)
2403            .collect::<Vec<_>>();
2404        self.latest_hw_out_meter_db = Arc::new(
2405            hw_out_linear
2406                .iter()
2407                .copied()
2408                .map(Self::meter_linear_to_db)
2409                .collect(),
2410        );
2411        self.hw_out_peak_hold_linear = hw_out_linear;
2412
2413        let mut track_linear_by_track = HashMap::with_capacity(decay.track_linear.len());
2414        let mut snapshot = Vec::with_capacity(decay.track_linear.len());
2415        for (name, start_linear) in &decay.track_linear {
2416            let linear = start_linear
2417                .iter()
2418                .copied()
2419                .map(|value| value * remaining)
2420                .collect::<Vec<_>>();
2421            let output_db = linear
2422                .iter()
2423                .copied()
2424                .map(Self::meter_linear_to_db)
2425                .collect::<Vec<_>>();
2426            track_linear_by_track.insert(name.clone(), linear);
2427            snapshot.push((name.clone(), output_db));
2428        }
2429        self.track_meter_linear_by_track = track_linear_by_track;
2430        self.latest_track_meter_snapshot = Arc::new(snapshot);
2431    }
2432
2433    pub(crate) fn finish_meter_decay_after_stop(&mut self) {
2434        self.meter_decay_after_stop = None;
2435        self.hw_out_peak_hold_linear.fill(0.0);
2436        let hw_channels = self.latest_hw_out_meter_db.len();
2437        self.latest_hw_out_meter_db = Arc::new(vec![-90.0; hw_channels]);
2438
2439        let tracks: Vec<(String, crate::state::TrackHandle)> = self
2440            .state_snapshot
2441            .load_full()
2442            .tracks
2443            .iter()
2444            .map(|(name, track)| (name.clone(), track.clone()))
2445            .collect();
2446        self.track_meter_linear_by_track.clear();
2447        let mut snapshot = Vec::with_capacity(tracks.len());
2448        for (name, track) in tracks {
2449            let mut t = track.lock();
2450            t.clear_output_meters();
2451            let width = t.output_meter_linear().len();
2452            let zero_linear = vec![0.0; width];
2453            self.track_meter_linear_by_track
2454                .insert(name.clone(), zero_linear);
2455            snapshot.push((name, vec![-90.0; width]));
2456        }
2457        self.latest_track_meter_snapshot = Arc::new(snapshot);
2458        self.publish_meter_snapshot();
2459    }
2460
2461    pub(crate) fn publish_meter_snapshot_if_due(&mut self) {
2462        let now = Instant::now();
2463        if self
2464            .last_meter_snapshot_publish
2465            .is_some_and(|last| now.duration_since(last) < Self::METER_PUBLISH_INTERVAL)
2466        {
2467            return;
2468        }
2469        self.last_meter_snapshot_publish = Some(now);
2470        self.update_meter_decay_after_stop();
2471        self.publish_meter_snapshot();
2472    }
2473
2474    pub(crate) fn publish_meter_snapshot(&mut self) {
2475        // Query LUFS at the same cadence as the VU meter snapshot rather than
2476        // every audio cycle, which keeps the CPU cost low.
2477        self.update_hw_out_lufs_readout();
2478
2479        let snapshot = self.meter_snapshot_producer.write_buffer();
2480        snapshot.hw_out_db.clear();
2481        snapshot
2482            .hw_out_db
2483            .extend(self.latest_hw_out_meter_db.iter().copied());
2484        snapshot.hw_out_lufs = self.latest_hw_out_lufs;
2485        snapshot.track_meters.clear();
2486        snapshot
2487            .track_meters
2488            .extend(self.latest_track_meter_snapshot.iter().cloned());
2489        self.meter_snapshot_producer.publish();
2490    }
2491
2492    pub(crate) fn publish_transport_snapshot(&mut self) {
2493        let snapshot = self.transport_snapshot_producer.write_buffer();
2494        snapshot.sample = self.transport_sample;
2495        snapshot.tempo_bpm = self.tempo_bpm;
2496        snapshot.playing = self.playing;
2497        snapshot.transport_running = self.transport_running;
2498        snapshot.tsig_num = self.tsig_num;
2499        snapshot.tsig_denom = self.tsig_denom;
2500        self.transport_snapshot_producer.publish();
2501    }
2502
2503    pub(crate) async fn handle_request(&mut self, a: Action) {
2504        match a {
2505            Action::Log { source, message } => {
2506                self.notify_clients(Ok(Action::Log { source, message }))
2507                    .await;
2508            }
2509            Action::Undo => {
2510                let actions = match self.history.undo() {
2511                    Some(actions) => actions,
2512                    None => {
2513                        self.notify_clients(Ok(Action::Undo)).await;
2514                        self.notify_clients(Ok(Action::HistoryState {
2515                            dirty: self.history.is_dirty(),
2516                        }))
2517                        .await;
2518                        return;
2519                    }
2520                };
2521
2522                let was_suspended = self.history_suspended;
2523                self.history_suspended = true;
2524                for action in actions {
2525                    self.handle_request_inner(action, false).await;
2526                }
2527                self.history_suspended = was_suspended;
2528                self.notify_clients(Ok(Action::Undo)).await;
2529                self.notify_clients(Ok(Action::HistoryState {
2530                    dirty: self.history.is_dirty(),
2531                }))
2532                .await;
2533            }
2534            Action::Redo => {
2535                let actions = match self.history.redo() {
2536                    Some(actions) => actions,
2537                    None => {
2538                        self.notify_clients(Ok(Action::Redo)).await;
2539                        self.notify_clients(Ok(Action::HistoryState {
2540                            dirty: self.history.is_dirty(),
2541                        }))
2542                        .await;
2543                        return;
2544                    }
2545                };
2546
2547                let was_suspended = self.history_suspended;
2548                self.history_suspended = true;
2549                for action in actions {
2550                    self.handle_request_inner(action, false).await;
2551                }
2552                self.history_suspended = was_suspended;
2553                self.notify_clients(Ok(Action::Redo)).await;
2554                self.notify_clients(Ok(Action::HistoryState {
2555                    dirty: self.history.is_dirty(),
2556                }))
2557                .await;
2558            }
2559            Action::ApplyGroupedActions(actions) => {
2560                self.handle_request_inner(Action::BeginHistoryGroup, true)
2561                    .await;
2562                for action in actions {
2563                    self.handle_request_inner(action, true).await;
2564                }
2565                self.handle_request_inner(Action::EndHistoryGroup, true)
2566                    .await;
2567            }
2568            Action::Session(_) => {
2569                self.handle_request_inner(a, false).await;
2570            }
2571            other => {
2572                self.handle_request_inner(other, true).await;
2573            }
2574        }
2575        self.publish_state_snapshot();
2576    }
2577
2578    pub(crate) async fn handle_quit(&mut self, a: Action) {
2579        self.flush_recordings().await;
2580        // Stop the HW worker before notifying the GUI so the
2581        // OSS audio channels are halted and closed from the
2582        // worker's own thread. The GUI calls exit(0) upon
2583        // receiving the Quit response, which skips Rust
2584        // destructors. Without this, the kernel's dsp_close
2585        // drains pending audio buffers for up to CHN_TIMEOUT
2586        // (5s) during process teardown.
2587        if let Some(mut worker) = self.hw_worker.take() {
2588            // Send MIDI panic (All Sound Off) for any active
2589            // notes before stopping the worker.
2590            let panic_events = self.panic_events_for_all_hw_midi_outputs();
2591            if !panic_events.is_empty() {
2592                let _ = worker.tx.send(Message::HWMidiOutEvents(panic_events)).await;
2593            }
2594            // Send Quit to the worker so it stops its audio
2595            // cycle loop and releases the driver.
2596            if let Err(e) = worker.tx.send(Message::Request(a.clone())).await {
2597                error!("Error sending quit message to HW worker: {e}");
2598            }
2599            if let Some(handle) = worker.handle.take() {
2600                handle
2601                    .await
2602                    .unwrap_or_else(|e| error!("Error waiting for HW worker to quit: {e}"));
2603            }
2604        }
2605        // Explicitly close audio and MIDI fds before sending
2606        // the Quit response. The GUI calls exit(0) upon
2607        // receiving it, which skips destructors — any
2608        // still-open device fd would trigger the kernel's
2609        // 5-second drain during process teardown.
2610        if let Some(hw) = self.hw_driver.as_mut() {
2611            hw.close_fds();
2612        }
2613        if let Some(midi_hub) = self.midi_hub.as_mut() {
2614            midi_hub.close_all();
2615        }
2616        self.hw_driver = None;
2617        self.hw_driver_info = None;
2618        self.hw_input_ports.clear();
2619        self.hw_output_ports.clear();
2620        self.notify_clients(Ok(Action::Quit)).await;
2621        self.ready_workers.clear();
2622        while !self.workers.is_empty() {
2623            let mut worker = self.workers.remove(0);
2624            if let Err(e) = worker.tx.send(Message::Request(a.clone())).await {
2625                error!("Error sending quit message to worker: {e}");
2626            }
2627            if let Some(handle) = worker.handle.take() {
2628                handle
2629                    .await
2630                    .unwrap_or_else(|e| error!("Error waiting for worker to quit: {e}"));
2631            }
2632        }
2633        #[cfg(unix)]
2634        {
2635            self.jack_runtime = None;
2636        }
2637        self.osc_server = None;
2638    }
2639
2640    #[inline]
2641    pub(crate) fn box_bool<'a>(
2642        fut: impl std::future::Future<Output = bool> + Send + 'a,
2643    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = bool> + Send + 'a>> {
2644        Box::pin(fut)
2645    }
2646
2647    pub(crate) async fn handle_request_inner(
2648        &mut self,
2649        mut action_to_process: Action,
2650        record_history: bool,
2651    ) {
2652        let a = action_to_process.clone();
2653        let suppress_timing_history = self.playing
2654            && matches!(
2655                &action_to_process,
2656                Action::SetTempo(_) | Action::SetTimeSignature { .. } | Action::SetTempoMap { .. }
2657            );
2658        let mut inverse_actions = self.prepare_inverse_actions(
2659            &action_to_process,
2660            record_history,
2661            suppress_timing_history,
2662        );
2663
2664        match action_to_process {
2665            Action::Play => {
2666                if Self::box_bool(self.handle_play(a.clone())).await {
2667                    return;
2668                }
2669            }
2670            Action::Pause => {
2671                if Self::box_bool(self.handle_pause(a.clone())).await {
2672                    return;
2673                }
2674            }
2675            Action::Stop => {
2676                if Self::box_bool(self.handle_stop(a.clone())).await {
2677                    return;
2678                }
2679            }
2680            Action::SessionPlay => {
2681                if Self::box_bool(self.handle_session_play(a.clone())).await {
2682                    return;
2683                }
2684            }
2685            Action::JumpToEnd => {
2686                self.transport_sample = self.normalize_transport_sample(self.session_end_sample());
2687                self.publish_transport_snapshot();
2688                self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
2689                    .await;
2690            }
2691            Action::Panic => {
2692                if Self::box_bool(self.handle_panic(a.clone())).await {
2693                    return;
2694                }
2695            }
2696            Action::Session(ref session_action) => {
2697                self.handle_session_action(session_action.clone()).await;
2698            }
2699            Action::SessionRuntimeReport { .. } => {}
2700            Action::SessionMidiLearnTriggered { .. } => {}
2701            Action::SetClipPlaybackEnabled(enabled) => {
2702                self.clip_playback_enabled = enabled;
2703                for track in self.state_snapshot.load_full().tracks.values() {
2704                    track.lock().set_clip_playback_enabled(enabled);
2705                }
2706            }
2707            Action::SetSessionClipPlaybackEnabled(enabled) => {
2708                self.session_clip_playback_enabled = enabled;
2709                for track in self.state_snapshot.load_full().tracks.values() {
2710                    track.lock().set_session_clip_playback_enabled(enabled);
2711                }
2712            }
2713            Action::TransportPosition(..) => {
2714                if Self::box_bool(self.handle_transport_position(a.clone())).await {
2715                    return;
2716                }
2717            }
2718            Action::SetLoopEnabled(enabled) => {
2719                self.loop_enabled = enabled && self.loop_range_samples.is_some();
2720                self.notified_loop_wrap_sample = None;
2721            }
2722            Action::SetLoopRange(..) => {
2723                if Self::box_bool(self.handle_set_loop_range(a.clone())).await {
2724                    return;
2725                }
2726            }
2727            Action::SetPunchEnabled(enabled) => {
2728                self.punch_enabled = enabled && self.punch_range_samples.is_some();
2729            }
2730            Action::SetPunchRange(range) => {
2731                self.punch_range_samples = range.and_then(|(start, end)| {
2732                    if end > start {
2733                        Some((start, end))
2734                    } else {
2735                        None
2736                    }
2737                });
2738                self.punch_enabled = self.punch_range_samples.is_some();
2739            }
2740            Action::SetMetronomeEnabled(enabled) => {
2741                self.metronome_enabled = enabled;
2742                if enabled {
2743                    self.ensure_metronome_track().await;
2744                }
2745                if let Some(track) = self
2746                    .state_snapshot
2747                    .load_full()
2748                    .tracks
2749                    .get(Self::METRONOME_TRACK)
2750                    .cloned()
2751                {
2752                    track.lock().set_metronome_enabled(enabled);
2753                }
2754            }
2755            Action::SetTempo(bpm) => {
2756                self.tempo_bpm = bpm.max(1.0);
2757                self.publish_transport_snapshot();
2758            }
2759            Action::SetTimeSignature {
2760                numerator,
2761                denominator,
2762            } => {
2763                self.tsig_num = numerator.max(1);
2764                self.tsig_denom = denominator.max(1);
2765                self.publish_transport_snapshot();
2766            }
2767            Action::SetTempoMap {
2768                ref tempo_points,
2769                ref time_signature_points,
2770            } => {
2771                self.tempo_points = tempo_points.clone();
2772                self.time_signature_points = time_signature_points.clone();
2773                self.update_global_tempo_from_map();
2774                self.publish_transport_snapshot();
2775            }
2776            Action::SetOscEnabled(enabled) => {
2777                if let Err(err) = self.set_osc_enabled_with(enabled, OscServer::start) {
2778                    self.notify_clients(Err(err)).await;
2779                }
2780            }
2781            Action::SetRecordEnabled(..) => {
2782                if Self::box_bool(self.handle_set_record_enabled(a.clone())).await {
2783                    return;
2784                }
2785            }
2786            Action::SetModulators(ref modulators) => {
2787                self.modulators = modulators.clone();
2788                let echoes = self.apply_modulators(self.active_transport_sample());
2789                for action in echoes {
2790                    self.notify_clients(Ok(action)).await;
2791                }
2792            }
2793            Action::SetTrackAutomationLanes {
2794                ref track_name,
2795                ref lanes,
2796                mode,
2797            } => {
2798                if let Some(track) = self.state_snapshot.load_full().tracks.get(track_name) {
2799                    let mut track = track.lock();
2800                    track.automation_lanes = lanes.clone();
2801                    track.set_automation_mode(mode);
2802                }
2803            }
2804            Action::TrackAutomationToggleLane { .. } => {
2805                if Self::box_bool(self.handle_track_automation_toggle_lane(a.clone())).await {
2806                    return;
2807                }
2808            }
2809            Action::TrackAutomationInsertPoint { .. } => {
2810                if Self::box_bool(self.handle_track_automation_insert_point(a.clone())).await {
2811                    return;
2812                }
2813            }
2814            Action::TrackAutomationDeletePoint { .. } => {
2815                if Self::box_bool(self.handle_track_automation_delete_point(a.clone())).await {
2816                    return;
2817                }
2818            }
2819            Action::TrackAutomationSetMode {
2820                ref track_name,
2821                mode,
2822            } => {
2823                if let Some(track) = self
2824                    .state_snapshot
2825                    .load_full()
2826                    .tracks
2827                    .get(track_name)
2828                    .cloned()
2829                {
2830                    track.lock().set_automation_mode(mode);
2831                }
2832            }
2833            Action::RequestTrackList => {
2834                let names: Vec<String> = self
2835                    .state_snapshot
2836                    .load_full()
2837                    .tracks
2838                    .keys()
2839                    .cloned()
2840                    .collect();
2841                self.notify_clients(Ok(Action::TrackList(names))).await;
2842            }
2843            Action::TrackList(_) => {}
2844            Action::RequestTransportState => {
2845                self.notify_clients(Ok(Action::TransportState {
2846                    sample: self.transport_sample,
2847                    tempo_bpm: self.tempo_bpm,
2848                    playing: self.playing,
2849                    paused: !self.transport_running && self.playing,
2850                    tsig_num: self.tsig_num,
2851                    tsig_denom: self.tsig_denom,
2852                }))
2853                .await;
2854            }
2855            Action::TransportState { .. } => {}
2856            Action::SetStepRecording(enabled) => {
2857                self.step_recording_enabled = enabled;
2858            }
2859            Action::BeginHistoryGroup if self.history_group.is_none() => {
2860                self.history_group = Some(UndoEntry {
2861                    forward_actions: vec![],
2862                    inverse_actions: vec![],
2863                });
2864            }
2865            Action::EndHistoryGroup => {
2866                if let Some(mut group) = self.history_group.take()
2867                    && !group.forward_actions.is_empty()
2868                    && !group.inverse_actions.is_empty()
2869                {
2870                    let mut add_tracks = Vec::new();
2871                    let mut connections = Vec::new();
2872                    let mut rest = Vec::new();
2873                    for action in group.inverse_actions {
2874                        if matches!(action, Action::AddTrack { .. }) {
2875                            add_tracks.push(action);
2876                        } else if matches!(action, Action::Connect { .. }) {
2877                            connections.push(action);
2878                        } else {
2879                            rest.push(action);
2880                        }
2881                    }
2882                    group.inverse_actions = add_tracks;
2883                    group.inverse_actions.extend(rest);
2884                    group.inverse_actions.extend(connections);
2885                    self.history.record(group);
2886                }
2887            }
2888            Action::SetSessionPath(ref path) => {
2889                self.session_dir = Some(Path::new(path).to_path_buf());
2890                self.ensure_session_subdirs();
2891                #[cfg(unix)]
2892                let _lv2_dir = self.session_plugins_dir();
2893                for track in self.state_snapshot.load_full().tracks.values() {
2894                    track.lock().set_session_base_dir(self.session_dir.clone());
2895                }
2896            }
2897            Action::MarkHistorySavePoint => {
2898                self.history.mark_save_point();
2899                self.notify_clients(Ok(Action::HistoryState {
2900                    dirty: self.history.is_dirty(),
2901                }))
2902                .await;
2903            }
2904            Action::ClearHistory => {
2905                self.history.clear();
2906                self.history.mark_save_point();
2907            }
2908            Action::BeginSessionRestore => {
2909                self.history_suspended = true;
2910                self.history.clear();
2911            }
2912            Action::EndSessionRestore => {
2913                self.history.clear();
2914                self.history_suspended = false;
2915                self.preload_track_clips_spawn();
2916            }
2917            Action::Quit => {
2918                self.handle_quit(a.clone()).await;
2919                return;
2920            }
2921            Action::AddTrack { .. } => {
2922                self.handle_add_track(a.clone()).await;
2923            }
2924            Action::TrackAddAudioInput(..) => {
2925                if Self::box_bool(self.handle_track_add_audio_input(a.clone())).await {
2926                    return;
2927                }
2928            }
2929            Action::TrackAddAudioOutput(..) => {
2930                if Self::box_bool(self.handle_track_add_audio_output(a.clone())).await {
2931                    return;
2932                }
2933            }
2934            Action::TrackRemoveAudioInput(..) => {
2935                if Self::box_bool(self.handle_track_remove_audio_input(a.clone())).await {
2936                    return;
2937                }
2938            }
2939            Action::TrackRemoveAudioOutput(..) => {
2940                if Self::box_bool(self.handle_track_remove_audio_output(a.clone())).await {
2941                    return;
2942                }
2943            }
2944            Action::RenameTrack { .. } => {
2945                if Self::box_bool(self.handle_rename_track(a.clone())).await {
2946                    return;
2947                }
2948            }
2949            Action::RemoveTrack(ref name) => {
2950                self.handle_remove_track(name.clone(), record_history).await;
2951                inverse_actions = None;
2952            }
2953            Action::TrackLevel(ref name, level) => {
2954                if name == "hw:out" {
2955                    self.hw_out_level_db = level;
2956                } else if let Some(track) = self.state_snapshot.load_full().tracks.get(name) {
2957                    track.lock().set_level(level);
2958                }
2959            }
2960            Action::TrackBalance(ref name, balance) => {
2961                if name == "hw:out" {
2962                    self.hw_out_balance = balance.clamp(-1.0, 1.0);
2963                } else if let Some(track) = self.state_snapshot.load_full().tracks.get(name) {
2964                    track.lock().set_balance(balance);
2965                }
2966            }
2967            Action::TrackAutomationLevel(ref name, level) => {
2968                tracing::debug!(%name, level, "engine received TrackAutomationLevel");
2969                if name == "hw:out" {
2970                    self.hw_out_level_db = level;
2971                } else if let Some(track) = self.state_snapshot.load_full().tracks.get(name) {
2972                    track.lock().set_level(level);
2973                }
2974            }
2975            Action::TrackAutomationBalance(ref name, balance) => {
2976                if name == "hw:out" {
2977                    self.hw_out_balance = balance.clamp(-1.0, 1.0);
2978                } else if let Some(track) = self.state_snapshot.load_full().tracks.get(name) {
2979                    track.lock().set_balance(balance);
2980                }
2981            }
2982            Action::TrackMidiCc { .. } => {
2983                if Self::box_bool(self.handle_track_midi_cc(a.clone())).await {
2984                    return;
2985                }
2986            }
2987            Action::RequestMeterSnapshot => {
2988                self.update_meter_decay_after_stop();
2989                self.notify_clients(Ok(Action::MeterSnapshot {
2990                    hw_out_db: self.latest_hw_out_meter_db.clone(),
2991                    hw_out_lufs: self.latest_hw_out_lufs,
2992                    track_meters: self.latest_track_meter_snapshot.clone(),
2993                }))
2994                .await;
2995                return;
2996            }
2997            Action::TrackMeters { .. } => {}
2998            Action::MeterSnapshot { .. } => {}
2999            Action::TrackToggleArm(..) => {
3000                if Self::box_bool(self.handle_track_toggle_arm(a.clone())).await {
3001                    return;
3002                }
3003            }
3004            Action::TrackToggleMute(ref name) => {
3005                if name == "hw:out" {
3006                    self.hw_out_muted = !self.hw_out_muted;
3007                } else if let Some(track) = self.state_snapshot.load_full().tracks.get(name) {
3008                    track.lock().mute();
3009                }
3010            }
3011            Action::TrackTogglePhase(ref name) => {
3012                if let Some(track) = self.state_snapshot.load_full().tracks.get(name) {
3013                    track.lock().invert_phase();
3014                }
3015            }
3016            Action::TrackToggleSolo(ref name) => {
3017                if name == "hw:out" {
3018                    return;
3019                }
3020                if let Some(track) = self.state_snapshot.load_full().tracks.get(name) {
3021                    track.lock().solo();
3022                }
3023            }
3024            Action::TrackToggleMaster(ref name) => {
3025                if let Some(track) = self.state_snapshot.load_full().tracks.get(name) {
3026                    track.lock().toggle_master();
3027                }
3028            }
3029            Action::TrackToggleInputMonitor {
3030                ref track_name,
3031                lane,
3032            } => {
3033                if let Some(track) = self.state_snapshot.load_full().tracks.get(track_name) {
3034                    track.lock().toggle_input_monitor(lane);
3035                }
3036            }
3037            Action::TrackToggleDiskMonitor {
3038                ref track_name,
3039                lane,
3040            } => {
3041                if let Some(track) = self.state_snapshot.load_full().tracks.get(track_name) {
3042                    track.lock().toggle_disk_monitor(lane);
3043                }
3044            }
3045            Action::TrackToggleMidiInputMonitor {
3046                ref track_name,
3047                lane,
3048            } => {
3049                if let Some(track) = self.state_snapshot.load_full().tracks.get(track_name) {
3050                    track.lock().toggle_midi_input_monitor(lane);
3051                }
3052            }
3053            Action::TrackToggleMidiDiskMonitor {
3054                ref track_name,
3055                lane,
3056            } => {
3057                if let Some(track) = self.state_snapshot.load_full().tracks.get(track_name) {
3058                    track.lock().toggle_midi_disk_monitor(lane);
3059                }
3060            }
3061            Action::TrackSetColor {
3062                ref track_name,
3063                color,
3064            } => {
3065                if let Some(track) = self.state_snapshot.load_full().tracks.get(track_name) {
3066                    track.lock().color = color;
3067                }
3068            }
3069            Action::TrackArmMidiLearn {
3070                ref track_name,
3071                target,
3072            } => {
3073                if let Err(e) = self.track_handle_or_err(track_name) {
3074                    self.notify_clients(Err(e)).await;
3075                    return;
3076                }
3077                self.pending_midi_learn = Some((track_name.clone(), target, None));
3078            }
3079            Action::GlobalArmMidiLearn { target } => {
3080                self.pending_global_midi_learn = Some(target);
3081            }
3082            Action::SessionArmMidiLearn { ref target } => {
3083                self.pending_session_midi_learn = Some(target.clone());
3084            }
3085            Action::TrackSetMidiLearnBinding { .. } => {
3086                if Self::box_bool(self.handle_track_set_midi_learn_binding(a.clone())).await {
3087                    return;
3088                }
3089            }
3090            Action::SetGlobalMidiLearnBinding { .. } => {
3091                if Self::box_bool(self.handle_set_global_midi_learn_binding(a.clone())).await {
3092                    return;
3093                }
3094            }
3095            Action::SetSessionMidiLearnBinding { .. } => {
3096                if Self::box_bool(self.handle_set_session_midi_learn_binding(a.clone())).await {
3097                    return;
3098                }
3099            }
3100            Action::TrackSetFolder { .. } => {
3101                if Self::box_bool(self.handle_track_set_folder(a.clone())).await {
3102                    return;
3103                }
3104            }
3105            Action::TrackSetParent {
3106                ref track_name,
3107                ref parent_name,
3108            } => {
3109                self.handle_track_set_parent(track_name.as_str(), parent_name.as_deref())
3110                    .await;
3111            }
3112            Action::TrackToggleFolder { .. } => {
3113                if Self::box_bool(self.handle_track_toggle_folder(a.clone())).await {
3114                    return;
3115                }
3116            }
3117            Action::TrackSetMidiLaneChannel { .. } => {
3118                if Self::box_bool(self.handle_track_set_midi_lane_channel(a.clone())).await {
3119                    return;
3120                }
3121            }
3122            Action::TrackSetMpeZone { .. } => {
3123                if Self::box_bool(self.handle_track_set_mpe_zone(a.clone())).await {
3124                    return;
3125                }
3126            }
3127            Action::TrackSetMpePitchBendSensitivity { .. } => {
3128                if Self::box_bool(self.handle_track_set_mpe_pitch_bend_sensitivity(a.clone())).await
3129                {
3130                    return;
3131                }
3132            }
3133            Action::TrackSetFrozen { .. } => {
3134                if Self::box_bool(self.handle_track_set_frozen(a.clone())).await {
3135                    return;
3136                }
3137            }
3138            Action::TrackSetSessionSlot { .. } => {
3139                if Self::box_bool(self.handle_track_set_session_slot(a.clone())).await {
3140                    return;
3141                }
3142            }
3143            Action::TrackSetSessionSlotPlayEnabled { .. } => {
3144                if self
3145                    .handle_track_set_session_slot_play_enabled(a.clone())
3146                    .await
3147                {
3148                    return;
3149                }
3150            }
3151            Action::TrackSetSessionSlotStopEnabled { .. } => {
3152                if self
3153                    .handle_track_set_session_slot_stop_enabled(a.clone())
3154                    .await
3155                {
3156                    return;
3157                }
3158            }
3159            Action::TrackOfflineBounce { .. } => {
3160                self.handle_track_offline_bounce(action_to_process).await;
3161                return;
3162            }
3163            Action::TrackOfflineBounceCancel { .. } => {}
3164            Action::TrackOfflineBounceCancelAll => {}
3165            Action::TrackOfflineBounceCanceled { .. } => {}
3166            Action::TrackOfflineBounceProgress { .. } => {}
3167            Action::PianoKey {
3168                ref track_name,
3169                note,
3170                velocity,
3171                on,
3172            } => {
3173                if let Some(track) = self.state_snapshot.load_full().tracks.get(track_name) {
3174                    let status = if on { 0x90 } else { 0x80 };
3175                    let event = MidiEvent::new(0, vec![status, note.min(127), velocity.min(127)]);
3176                    track.lock().push_hw_midi_events(&[event]);
3177                }
3178            }
3179            Action::ModifyMidiNotes { .. }
3180            | Action::ModifyMidiControllers { .. }
3181            | Action::DeleteMidiControllers { .. }
3182            | Action::InsertMidiControllers { .. }
3183            | Action::DeleteMidiNotes { .. }
3184            | Action::InsertMidiNotes { .. } => {
3185                if let Err(e) = self.apply_midi_edit_action(&action_to_process) {
3186                    self.notify_clients(Err(e)).await;
3187                    return;
3188                }
3189            }
3190            Action::SetMidiSysExEvents { .. } => {
3191                if let Err(e) = self.apply_midi_edit_action(&action_to_process) {
3192                    self.notify_clients(Err(e)).await;
3193                    return;
3194                }
3195            }
3196            Action::TrackClearDefaultPassthrough { .. } => {
3197                if Self::box_bool(self.handle_track_clear_default_passthrough(a.clone())).await {
3198                    return;
3199                }
3200            }
3201            Action::TrackClearPlugins { .. } => {
3202                if Self::box_bool(self.handle_track_clear_plugins(a.clone())).await {
3203                    return;
3204                }
3205            }
3206            #[cfg(unix)]
3207            Action::TrackSetLv2PluginState { .. } => {
3208                if Self::box_bool(self.handle_track_set_lv2_plugin_state(a.clone())).await {
3209                    return;
3210                }
3211            }
3212            #[cfg(unix)]
3213            Action::ClipSetLv2PluginState { ref track_name, .. } => {
3214                self.notify_clients(Err(format!(
3215                    "Track '{}': clip LV2 plugin state changes are not supported",
3216                    track_name
3217                )))
3218                .await;
3219            }
3220            Action::TrackGetClapNoteNames { .. } => {
3221                if Self::box_bool(self.handle_track_get_clap_note_names(a.clone())).await {
3222                    return;
3223                }
3224            }
3225            #[cfg(unix)]
3226            Action::TrackGetLv2Midnam { .. } => {
3227                if Self::box_bool(self.handle_track_get_lv2_midnam(a.clone())).await {
3228                    return;
3229                }
3230            }
3231            Action::TrackGetPluginGraph { .. } => {
3232                if Self::box_bool(self.handle_track_get_plugin_graph(a.clone())).await {
3233                    return;
3234                }
3235            }
3236            Action::TrackPluginGraph { .. } => {}
3237            Action::TrackConnectPluginAudio { .. } => {
3238                if Self::box_bool(self.handle_track_connect_plugin_audio(a.clone())).await {
3239                    return;
3240                }
3241            }
3242            Action::TrackConnectPluginMidi { .. } => {
3243                if Self::box_bool(self.handle_track_connect_plugin_midi(a.clone())).await {
3244                    return;
3245                }
3246            }
3247            Action::TrackDisconnectPluginAudio { .. } => {
3248                if Self::box_bool(self.handle_track_disconnect_plugin_audio(a.clone())).await {
3249                    return;
3250                }
3251            }
3252            Action::TrackDisconnectPluginMidi { .. } => {
3253                if Self::box_bool(self.handle_track_disconnect_plugin_midi(a.clone())).await {
3254                    return;
3255                }
3256            }
3257            Action::TrackConnectAudio { .. } => {
3258                if Self::box_bool(self.handle_track_connect_audio(a.clone())).await {
3259                    return;
3260                }
3261            }
3262            Action::TrackDisconnectAudio { .. } => {
3263                if Self::box_bool(self.handle_track_disconnect_audio(a.clone())).await {
3264                    return;
3265                }
3266            }
3267            Action::TrackConnectMidi { .. } => {
3268                if Self::box_bool(self.handle_track_connect_midi(a.clone())).await {
3269                    return;
3270                }
3271            }
3272            Action::TrackDisconnectMidi { .. } => {
3273                if Self::box_bool(self.handle_track_disconnect_midi(a.clone())).await {
3274                    return;
3275                }
3276            }
3277            #[cfg(unix)]
3278            Action::ListLv2Plugins => {
3279                if Self::box_bool(self.handle_list_lv2_plugins(a.clone())).await {
3280                    return;
3281                }
3282            }
3283            #[cfg(unix)]
3284            Action::Lv2Plugins(_) => {}
3285            #[cfg(unix)]
3286            Action::Lv2PluginsUnavailable { .. } => {}
3287            Action::ListVst3Plugins => {
3288                if Self::box_bool(self.handle_list_vst3_plugins(a.clone())).await {
3289                    return;
3290                }
3291            }
3292            Action::Vst3Plugins(_) => {}
3293            Action::Vst3PluginsUnavailable { .. } => {}
3294            Action::ListClapPlugins => {
3295                if Self::box_bool(self.handle_list_clap_plugins(a.clone())).await {
3296                    return;
3297                }
3298            }
3299            Action::ListClapPluginsWithCapabilities => {
3300                if self
3301                    .handle_list_clap_plugins_with_capabilities(a.clone())
3302                    .await
3303                {
3304                    return;
3305                }
3306            }
3307            Action::ClapPlugins(_) => {}
3308            Action::ClapPluginsUnavailable { .. } => {}
3309            Action::TrackLoadClapPlugin {
3310                ref track_name,
3311                ref plugin_id,
3312                instance_id,
3313            } => {
3314                if self
3315                    .handle_track_load_clap_plugin(
3316                        track_name.as_str(),
3317                        plugin_id.as_str(),
3318                        instance_id,
3319                    )
3320                    .await
3321                {
3322                    return;
3323                }
3324            }
3325            Action::TrackUnloadClapPlugin {
3326                ref track_name,
3327                ref plugin_id,
3328            } => {
3329                if self
3330                    .handle_track_unload_clap_plugin(track_name.as_str(), plugin_id.as_str())
3331                    .await
3332                {
3333                    return;
3334                }
3335            }
3336            Action::TrackUnloadClapPluginInstance {
3337                ref track_name,
3338                instance_id,
3339            } => {
3340                if self
3341                    .handle_track_unload_clap_plugin_instance(track_name.as_str(), instance_id)
3342                    .await
3343                {
3344                    return;
3345                }
3346            }
3347            Action::TrackShowClapGui { .. } => {
3348                if Self::box_bool(self.handle_track_show_clap_gui(a.clone())).await {
3349                    return;
3350                }
3351            }
3352            Action::ClipShowClapGui { .. } => {
3353                if Self::box_bool(self.handle_clip_show_clap_gui(a.clone())).await {
3354                    return;
3355                }
3356            }
3357            Action::TrackLoadVst3Plugin {
3358                ref track_name,
3359                ref plugin_id,
3360                instance_id,
3361            } => {
3362                if self
3363                    .handle_track_load_vst3_plugin(
3364                        track_name.as_str(),
3365                        plugin_id.as_str(),
3366                        instance_id,
3367                    )
3368                    .await
3369                {
3370                    return;
3371                }
3372            }
3373            Action::TrackUnloadVst3Plugin {
3374                ref track_name,
3375                ref plugin_id,
3376            } => {
3377                if self
3378                    .handle_track_unload_vst3_plugin(track_name.as_str(), plugin_id.as_str())
3379                    .await
3380                {
3381                    return;
3382                }
3383            }
3384            Action::TrackUnloadVst3PluginInstance {
3385                ref track_name,
3386                instance_id,
3387            } => {
3388                if self
3389                    .handle_track_unload_vst3_plugin_instance(track_name.as_str(), instance_id)
3390                    .await
3391                {
3392                    return;
3393                }
3394            }
3395            Action::TrackShowVst3Gui { .. } => {
3396                if Self::box_bool(self.handle_track_show_vst3_gui(a.clone())).await {
3397                    return;
3398                }
3399            }
3400            Action::ClipShowVst3Gui { .. } => {
3401                if Self::box_bool(self.handle_clip_show_vst3_gui(a.clone())).await {
3402                    return;
3403                }
3404            }
3405            #[cfg(unix)]
3406            Action::TrackLoadLv2Plugin {
3407                ref track_name,
3408                ref plugin_uri,
3409                instance_id,
3410            } => {
3411                if self
3412                    .handle_track_load_lv2_plugin(
3413                        track_name.as_str(),
3414                        plugin_uri.as_str(),
3415                        instance_id,
3416                    )
3417                    .await
3418                {
3419                    return;
3420                }
3421            }
3422            #[cfg(unix)]
3423            Action::TrackUnloadLv2Plugin {
3424                ref track_name,
3425                ref plugin_uri,
3426            } => {
3427                if self
3428                    .handle_track_unload_lv2_plugin(track_name.as_str(), plugin_uri.as_str())
3429                    .await
3430                {
3431                    return;
3432                }
3433            }
3434            #[cfg(unix)]
3435            Action::TrackUnloadLv2PluginInstance {
3436                ref track_name,
3437                instance_id,
3438            } => {
3439                if self
3440                    .handle_track_unload_lv2_plugin_instance(track_name.as_str(), instance_id)
3441                    .await
3442                {
3443                    return;
3444                }
3445            }
3446            #[cfg(unix)]
3447            Action::TrackShowLv2Gui { .. } => {
3448                if Self::box_bool(self.handle_track_show_lv2_gui(a.clone())).await {
3449                    return;
3450                }
3451            }
3452            #[cfg(unix)]
3453            Action::ClipShowLv2Gui { .. } => {
3454                if Self::box_bool(self.handle_clip_show_lv2_gui(a.clone())).await {
3455                    return;
3456                }
3457            }
3458            Action::TrackSetPluginResourceDir { .. } => {
3459                if Self::box_bool(self.handle_track_set_plugin_resource_dir(a.clone())).await {
3460                    return;
3461                }
3462            }
3463            Action::TrackClapCollectResources { .. } => {
3464                if Self::box_bool(self.handle_track_clap_collect_resources(a.clone())).await {
3465                    return;
3466                }
3467            }
3468            Action::ClipSetPluginResourceDir { .. } => {
3469                if Self::box_bool(self.handle_clip_set_plugin_resource_dir(a.clone())).await {
3470                    return;
3471                }
3472            }
3473            Action::ClipClapCollectResources { .. } => {
3474                if Self::box_bool(self.handle_clip_clap_collect_resources(a.clone())).await {
3475                    return;
3476                }
3477            }
3478            Action::TrackSetClapParameter { .. } => {
3479                if Self::box_bool(self.handle_track_set_clap_parameter(a.clone())).await {
3480                    return;
3481                }
3482            }
3483            Action::ClipSetClapParameter { .. } => {
3484                if Self::box_bool(self.handle_clip_set_clap_parameter(a.clone())).await {
3485                    return;
3486                }
3487            }
3488            Action::ClipGetClapParameters { .. } => {
3489                if Self::box_bool(self.handle_clip_get_clap_parameters(a.clone())).await {
3490                    return;
3491                }
3492            }
3493            Action::ClipClapParameters { .. } => {}
3494            Action::TrackSetClapParameterAt { .. } => {
3495                if Self::box_bool(self.handle_track_set_clap_parameter_at(a.clone())).await {
3496                    return;
3497                }
3498            }
3499            Action::TrackBeginClapParameterEdit { .. } => {
3500                if Self::box_bool(self.handle_track_begin_clap_parameter_edit(a.clone())).await {
3501                    return;
3502                }
3503            }
3504            Action::TrackEndClapParameterEdit { .. } => {
3505                if Self::box_bool(self.handle_track_end_clap_parameter_edit(a.clone())).await {
3506                    return;
3507                }
3508            }
3509            Action::TrackGetClapParameters { .. } => {
3510                if Self::box_bool(self.handle_track_get_clap_parameters(a.clone())).await {
3511                    return;
3512                }
3513            }
3514            Action::TrackClapParameters { .. } => {}
3515            Action::TrackClapSnapshotState { .. } => {
3516                if Self::box_bool(self.handle_track_clap_snapshot_state(a.clone())).await {
3517                    return;
3518                }
3519            }
3520            Action::ClipClapSnapshotState { .. } => {
3521                if Self::box_bool(self.handle_clip_clap_snapshot_state(a.clone())).await {
3522                    return;
3523                }
3524            }
3525            Action::TrackClapStateSnapshot { .. } => {}
3526            Action::ClipClapStateSnapshot { .. } => {}
3527            Action::TrackClapStateDirty { .. } => {}
3528            Action::ClipClapStateDirty { .. } => {}
3529            Action::TrackClapRestoreState { .. } => {
3530                if Self::box_bool(self.handle_track_clap_restore_state(a.clone())).await {
3531                    return;
3532                }
3533            }
3534            Action::ClipClapRestoreState { .. } => {
3535                if Self::box_bool(self.handle_clip_clap_restore_state(a.clone())).await {
3536                    return;
3537                }
3538            }
3539            Action::TrackSnapshotAllClapStates { .. } => {
3540                if Self::box_bool(self.handle_track_snapshot_all_clap_states(a.clone())).await {
3541                    return;
3542                }
3543            }
3544            Action::TrackSnapshotAllClapStatesDone { .. } => {}
3545            Action::TrackGetVst3Graph { .. } => {
3546                if Self::box_bool(self.handle_track_get_vst3_graph(a.clone())).await {
3547                    return;
3548                }
3549            }
3550            Action::TrackVst3Graph { .. } => {}
3551            Action::TrackSetVst3Parameter { .. } => {
3552                if Self::box_bool(self.handle_track_set_vst3_parameter(a.clone())).await {
3553                    return;
3554                }
3555            }
3556            Action::ClipSetVst3Parameter { .. } => {
3557                if Self::box_bool(self.handle_clip_set_vst3_parameter(a.clone())).await {
3558                    return;
3559                }
3560            }
3561            Action::TrackSetPluginBypassed { .. } => {
3562                if Self::box_bool(self.handle_track_set_plugin_bypassed(a.clone())).await {
3563                    return;
3564                }
3565            }
3566            Action::TrackGetVst3Parameters { .. } => {
3567                if Self::box_bool(self.handle_track_get_vst3_parameters(a.clone())).await {
3568                    return;
3569                }
3570            }
3571            Action::TrackVst3Parameters { .. } => {}
3572            Action::ClipGetVst3Parameters { .. } => {
3573                if Self::box_bool(self.handle_clip_get_vst3_parameters(a.clone())).await {
3574                    return;
3575                }
3576            }
3577            Action::ClipVst3Parameters { .. } => {}
3578            #[cfg(unix)]
3579            Action::TrackSetLv2ControlValue { .. } => {
3580                if Self::box_bool(self.handle_track_set_lv2_control_value(a.clone())).await {
3581                    return;
3582                }
3583            }
3584            #[cfg(unix)]
3585            Action::ClipSetLv2ControlValue { .. } => {
3586                if Self::box_bool(self.handle_clip_set_lv2_control_value(a.clone())).await {
3587                    return;
3588                }
3589            }
3590            #[cfg(unix)]
3591            Action::TrackGetLv2PluginControls { .. } => {
3592                if Self::box_bool(self.handle_track_get_lv2_plugin_controls(a.clone())).await {
3593                    return;
3594                }
3595            }
3596            #[cfg(unix)]
3597            Action::ClipGetLv2PluginControls { .. } => {
3598                if Self::box_bool(self.handle_clip_get_lv2_plugin_controls(a.clone())).await {
3599                    return;
3600                }
3601            }
3602            #[cfg(unix)]
3603            Action::TrackLv2SnapshotState { .. } => {
3604                if Self::box_bool(self.handle_track_lv2_snapshot_state(a.clone())).await {
3605                    return;
3606                }
3607            }
3608            #[cfg(unix)]
3609            Action::ClipLv2SnapshotState { .. } => {
3610                if Self::box_bool(self.handle_clip_lv2_snapshot_state(a.clone())).await {
3611                    return;
3612                }
3613            }
3614            Action::TrackVst3SnapshotState { .. } => {
3615                if Self::box_bool(self.handle_track_vst3_snapshot_state(a.clone())).await {
3616                    return;
3617                }
3618            }
3619            Action::ClipVst3SnapshotState { .. } => {
3620                if Self::box_bool(self.handle_clip_vst3_snapshot_state(a.clone())).await {
3621                    return;
3622                }
3623            }
3624            Action::TrackVst3StateSnapshot { .. } => {}
3625            Action::ClipVst3StateSnapshot { .. } => {}
3626            Action::TrackVst3RestoreState { .. } => {
3627                if Self::box_bool(self.handle_track_vst3_restore_state(a.clone())).await {
3628                    return;
3629                }
3630            }
3631            Action::TrackConnectVst3Audio { .. } => {
3632                if Self::box_bool(self.handle_track_connect_vst3_audio(a.clone())).await {
3633                    return;
3634                }
3635            }
3636            Action::TrackDisconnectVst3Audio { .. } => {
3637                if Self::box_bool(self.handle_track_disconnect_vst3_audio(a.clone())).await {
3638                    return;
3639                }
3640            }
3641            Action::ClipMove { .. } => {
3642                self.handle_clip_move(a.clone()).await;
3643            }
3644            Action::AddClip { .. } => {
3645                if Self::box_bool(self.handle_add_clip(a.clone())).await {
3646                    return;
3647                }
3648            }
3649            Action::AddGroupedClip { .. } => {
3650                if Self::box_bool(self.handle_add_grouped_clip(a.clone())).await {
3651                    return;
3652                }
3653            }
3654            Action::RemoveClip {
3655                ref track_name,
3656                kind,
3657                ref clip_indices,
3658            } => {
3659                self.remove_clips_from_track(track_name, kind, clip_indices);
3660            }
3661            Action::MoveClipToUnused {
3662                ref track_name,
3663                kind,
3664                ref clip_indices,
3665            } => {
3666                self.move_clips_to_unused(track_name, kind, clip_indices);
3667            }
3668            Action::DeleteUnusedClips { ref clip_ids } => {
3669                self.delete_unused_clips(clip_ids);
3670            }
3671            Action::SetUnusedClips {
3672                ref audio,
3673                ref midi,
3674            } => {
3675                self.set_unused_clips(audio.clone(), midi.clone());
3676            }
3677            Action::RenameClip {
3678                ref track_name,
3679                kind,
3680                clip_index,
3681                ref new_name,
3682            } => {
3683                self.rename_clip_references(track_name, kind, clip_index, new_name);
3684            }
3685            Action::SetClipIdentity {
3686                ref track_name,
3687                kind,
3688                clip_index,
3689                ref new_id,
3690                ref new_name,
3691            } => {
3692                self.set_clip_identity(track_name, kind, clip_index, new_id, new_name);
3693            }
3694            Action::SetClipSourceName {
3695                ref track_name,
3696                kind,
3697                clip_index,
3698                ref name,
3699            } => {
3700                self.set_clip_source_name(track_name, clip_index, kind, name.clone());
3701            }
3702            Action::SetClipFade { .. } => {
3703                if Self::box_bool(self.handle_set_clip_fade(a.clone())).await {
3704                    return;
3705                }
3706            }
3707            Action::SetClipBounds {
3708                ref track_name,
3709                clip_index,
3710                kind,
3711                start,
3712                length,
3713                offset,
3714            } => {
3715                self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
3716            }
3717            Action::SyncClipBounds {
3718                ref track_name,
3719                clip_index,
3720                kind,
3721                start,
3722                length,
3723                offset,
3724            } => {
3725                self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
3726            }
3727            Action::SetClipMuted {
3728                ref track_name,
3729                clip_index,
3730                kind,
3731                muted,
3732            } => {
3733                self.set_clip_muted(track_name, clip_index, kind, muted);
3734            }
3735            Action::SetClipReversed {
3736                ref track_name,
3737                clip_index,
3738                kind,
3739                reversed,
3740            } => {
3741                self.set_clip_reversed(track_name, clip_index, kind, reversed);
3742            }
3743            Action::SetClipGainDb {
3744                ref track_name,
3745                clip_index,
3746                kind,
3747                gain_db,
3748            } => {
3749                self.set_clip_gain_db(track_name, clip_index, kind, gain_db);
3750            }
3751            Action::SetClipPluginGraphJson {
3752                ref track_name,
3753                clip_index,
3754                ref plugin_graph_json,
3755            } => {
3756                self.set_clip_plugin_graph_json(track_name, clip_index, plugin_graph_json.clone());
3757            }
3758            Action::SetClipPitchCorrection { .. } => {
3759                if Self::box_bool(self.handle_set_clip_pitch_correction(a.clone())).await {
3760                    return;
3761                }
3762            }
3763            Action::Connect {
3764                ref from_track,
3765                from_port,
3766                ref to_track,
3767                to_port,
3768                kind,
3769            } => {
3770                self.handle_connect(
3771                    from_track.as_str(),
3772                    from_port,
3773                    to_track.as_str(),
3774                    to_port,
3775                    kind,
3776                )
3777                .await;
3778            }
3779            Action::Disconnect { .. } => {
3780                self.handle_disconnect(a.clone()).await;
3781            }
3782            Action::OpenAudioDevice { .. } => {
3783                let (done, updated) = self.handle_open_audio_device(a.clone()).await;
3784                if done {
3785                    return;
3786                }
3787                if let Some(action) = updated {
3788                    action_to_process = action;
3789                }
3790            }
3791            Action::JackAddAudioInputPort => {
3792                if Self::box_bool(self.handle_jack_add_audio_input_port(a.clone())).await {
3793                    return;
3794                }
3795            }
3796            Action::JackRemoveAudioInputPort(_removed_port) => {
3797                if self
3798                    .handle_jack_remove_audio_input_port(_removed_port, a.clone())
3799                    .await
3800                {
3801                    return;
3802                }
3803            }
3804            Action::JackAddAudioOutputPort => {
3805                if Self::box_bool(self.handle_jack_add_audio_output_port(a.clone())).await {
3806                    return;
3807                }
3808            }
3809            Action::JackRemoveAudioOutputPort(_removed_port) => {
3810                if self
3811                    .handle_jack_remove_audio_output_port(_removed_port, a.clone())
3812                    .await
3813                {
3814                    return;
3815                }
3816            }
3817            Action::JackGetGraph => {
3818                #[cfg(unix)]
3819                {
3820                    match self
3821                        .jack_runtime
3822                        .as_ref()
3823                        .ok_or(
3824                            "JACK runtime is not active; open the JACK backend first".to_string(),
3825                        )
3826                        .and_then(|jack| jack.graph_info())
3827                    {
3828                        Ok(graph) => self.notify_clients(Ok(Action::JackGraph(graph))).await,
3829                        Err(e) => self.notify_clients(Err(e)).await,
3830                    }
3831                }
3832                #[cfg(not(unix))]
3833                {
3834                    self.notify_clients(Err(
3835                        "JACK backend is not available on this platform build".to_string(),
3836                    ))
3837                    .await;
3838                }
3839                return;
3840            }
3841            Action::JackConnect {
3842                ref source,
3843                ref destination,
3844            } => {
3845                #[cfg(unix)]
3846                {
3847                    match self
3848                        .jack_runtime
3849                        .as_ref()
3850                        .ok_or(
3851                            "JACK runtime is not active; open the JACK backend first".to_string(),
3852                        )
3853                        .and_then(|jack| jack.connect_ports_by_name(source, destination))
3854                        .and_then(|_| {
3855                            self.jack_runtime
3856                                .as_ref()
3857                                .expect("JACK runtime was checked")
3858                                .graph_info()
3859                        }) {
3860                        Ok(graph) => {
3861                            self.notify_clients(Ok(a.clone())).await;
3862                            self.notify_clients(Ok(Action::JackGraph(graph))).await;
3863                        }
3864                        Err(e) => self.notify_clients(Err(e)).await,
3865                    }
3866                }
3867                #[cfg(not(unix))]
3868                {
3869                    let _ = (source, destination);
3870                    self.notify_clients(Err(
3871                        "JACK backend is not available on this platform build".to_string(),
3872                    ))
3873                    .await;
3874                }
3875                return;
3876            }
3877            Action::JackDisconnect {
3878                ref source,
3879                ref destination,
3880            } => {
3881                #[cfg(unix)]
3882                {
3883                    match self
3884                        .jack_runtime
3885                        .as_ref()
3886                        .ok_or(
3887                            "JACK runtime is not active; open the JACK backend first".to_string(),
3888                        )
3889                        .and_then(|jack| jack.disconnect_ports_by_name(source, destination))
3890                        .and_then(|_| {
3891                            self.jack_runtime
3892                                .as_ref()
3893                                .expect("JACK runtime was checked")
3894                                .graph_info()
3895                        }) {
3896                        Ok(graph) => {
3897                            self.notify_clients(Ok(a.clone())).await;
3898                            self.notify_clients(Ok(Action::JackGraph(graph))).await;
3899                        }
3900                        Err(e) => self.notify_clients(Err(e)).await,
3901                    }
3902                }
3903                #[cfg(not(unix))]
3904                {
3905                    let _ = (source, destination);
3906                    self.notify_clients(Err(
3907                        "JACK backend is not available on this platform build".to_string(),
3908                    ))
3909                    .await;
3910                }
3911                return;
3912            }
3913            Action::OpenMidiInputDevice(ref device) => {
3914                if let Some(worker) = &self.hw_worker {
3915                    if let Err(e) = worker
3916                        .tx
3917                        .send(Message::HWOpenMidiInputDevice(device.clone()))
3918                        .await
3919                    {
3920                        self.notify_clients(Err(format!("Failed to send MIDI input open: {e}")))
3921                            .await;
3922                    }
3923                    return;
3924                }
3925                let Some(midi_hub) = self.midi_hub.as_mut() else {
3926                    self.notify_clients(Err("Hardware MIDI hub is not available".to_string()))
3927                        .await;
3928                    return;
3929                };
3930                if let Err(e) = midi_hub.open_input(device) {
3931                    self.notify_clients(Err(e)).await;
3932                    return;
3933                }
3934            }
3935            Action::OpenMidiOutputDevice(ref device) => {
3936                if let Some(worker) = &self.hw_worker {
3937                    if let Err(e) = worker
3938                        .tx
3939                        .send(Message::HWOpenMidiOutputDevice(device.clone()))
3940                        .await
3941                    {
3942                        self.notify_clients(Err(format!("Failed to send MIDI output open: {e}")))
3943                            .await;
3944                    }
3945                    return;
3946                }
3947                let Some(midi_hub) = self.midi_hub.as_mut() else {
3948                    self.notify_clients(Err("Hardware MIDI hub is not available".to_string()))
3949                        .await;
3950                    return;
3951                };
3952                if let Err(e) = midi_hub.open_output(device) {
3953                    self.notify_clients(Err(e)).await;
3954                    return;
3955                }
3956            }
3957            Action::RequestSessionDiagnostics => {
3958                self.handle_request_session_diagnostics().await;
3959            }
3960            Action::RequestMidiLearnMappingsReport => {
3961                self.handle_request_midi_learn_mappings_report().await;
3962            }
3963            Action::ClearAllMidiLearnBindings => {
3964                if Self::box_bool(self.handle_clear_all_midi_learn_bindings(a.clone())).await {
3965                    return;
3966                }
3967            }
3968            #[cfg(unix)]
3969            Action::TrackLv2PluginControls { .. } => {}
3970            #[cfg(unix)]
3971            Action::ClipLv2PluginControls { .. } => {}
3972            #[cfg(unix)]
3973            Action::TrackLv2StateSnapshot { .. } => {}
3974            #[cfg(unix)]
3975            Action::ClipLv2StateSnapshot { .. } => {}
3976            #[cfg(unix)]
3977            Action::TrackLv2Midnam { .. } => {}
3978            Action::TrackClapNoteNames { .. } => {}
3979            Action::SessionDiagnosticsReport { .. } => {}
3980            Action::MidiLearnMappingsReport { .. } => {}
3981            Action::HWInfo { .. } => {}
3982            Action::HistoryState { .. } => {}
3983            Action::Undo => {}
3984            Action::Redo => {}
3985            Action::ApplyGroupedActions(_) => {}
3986            _ => {}
3987        }
3988
3989        if let Some(inverse) = inverse_actions {
3990            if let Some(group) = self.history_group.as_mut() {
3991                group.forward_actions.push(action_to_process.clone());
3992                group.inverse_actions.splice(0..0, inverse);
3993            } else {
3994                self.history.record(UndoEntry {
3995                    forward_actions: vec![action_to_process.clone()],
3996                    inverse_actions: inverse,
3997                });
3998            }
3999        }
4000
4001        self.notify_clients(Ok(action_to_process)).await;
4002    }
4003    pub async fn work(&mut self) {
4004        // Wake immediately when a fixed-thread node worker pushes a result;
4005        // a slower periodic tick still force-completes timed-out nodes.
4006        let mut timeout_tick = tokio::time::interval(Duration::from_millis(10));
4007        timeout_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
4008        loop {
4009            let message = tokio::select! {
4010                message = self.rx.recv() => {
4011                    let Some(message) = message else {
4012                        break;
4013                    };
4014                    tracing::debug!(?message, "engine work loop received message");
4015                    Some(message)
4016                }
4017                _ = self.node_result_notify.notified() => {
4018                    tracing::trace!("engine work loop woken by node result");
4019                    None
4020                }
4021                _ = timeout_tick.tick() => {
4022                    tracing::trace!("engine work loop timeout tick");
4023                    None
4024                }
4025            };
4026            self.poll_node_worker_results().await;
4027            self.poll_jack_hw_finished().await;
4028            self.poll_stopped_plugin_parameter_echoes().await;
4029            if !self.playing && !self.transport_running {
4030                self.publish_clap_state_dirty().await;
4031            }
4032            self.on_executor_tick().await;
4033            let Some(message) = message else {
4034                continue;
4035            };
4036            match message {
4037                Message::Ready(id) => {
4038                    // A bounce worker's terminal Ready cleans up its job even
4039                    // when the OfflineBounceFinished payload was an Err
4040                    // without a track name.
4041                    if let Some(track_name) = self.bounce_worker_tracks.remove(&id) {
4042                        self.offline_bounce_jobs.remove(&track_name);
4043                    }
4044                    self.push_ready_worker(id);
4045                    if self.dispatch_node_jobs(Vec::new()).await {
4046                        self.on_all_tracks_finished().await;
4047                    }
4048                    self.drain_pending_requests_if_idle().await;
4049                }
4050                Message::NodeDone {
4051                    worker_id,
4052                    epoch,
4053                    node,
4054                    output_linear,
4055                    parameter_updates,
4056                    latency_changed,
4057                } => {
4058                    self.on_node_done(
4059                        worker_id,
4060                        epoch,
4061                        node,
4062                        output_linear,
4063                        parameter_updates,
4064                        latency_changed,
4065                    )
4066                    .await;
4067                }
4068                Message::Channel(s) => {
4069                    self.clients.push(s);
4070                }
4071                Message::Response(result) => {
4072                    self.notify_clients(result).await;
4073                }
4074
4075                Message::Request(a) => {
4076                    self.dispatch_request(a).await;
4077                    // Any request may have changed the topology; the builder
4078                    // coalesces bursts into a single plan rebuild.
4079                    self.plan_builder.mark_dirty();
4080                }
4081                Message::OscRequest { action, reply_to } => {
4082                    tracing::debug!(%reply_to, ?action, "engine received OscRequest");
4083                    self.osc_reply_target = Some(reply_to);
4084                    self.dispatch_request(action).await;
4085                    self.osc_reply_target = None;
4086                    self.plan_builder.mark_dirty();
4087                }
4088                Message::OfflineBounceFinished { result } => {
4089                    if let Ok(Action::TrackOfflineBounce { track_name, .. })
4090                    | Ok(Action::TrackOfflineBounceCanceled { track_name, .. }) = &result
4091                    {
4092                        self.offline_bounce_jobs.remove(track_name);
4093                    }
4094                    self.notify_clients(result).await;
4095                    self.drain_pending_requests_if_idle().await;
4096                }
4097                Message::HWFinished => {
4098                    if !self.awaiting_hwfinished {
4099                        tracing::debug!(
4100                            playing = self.playing,
4101                            transport_running = self.transport_running,
4102                            transport_sample = self.transport_sample,
4103                            session_transport_sample = self.session_transport_sample,
4104                            cycle_samples = self.current_cycle_samples(),
4105                            "HWFinished ignored because engine was not awaiting it"
4106                        );
4107                        continue;
4108                    }
4109                    tracing::debug!(
4110                        playing = self.playing,
4111                        transport_running = self.transport_running,
4112                        transport_sample = self.transport_sample,
4113                        session_transport_sample = self.session_transport_sample,
4114                        cycle_samples = self.current_cycle_samples(),
4115                        "HWFinished handling"
4116                    );
4117                    self.handling_hwfinished = true;
4118                    self.awaiting_hwfinished = false;
4119                    #[cfg(unix)]
4120                    {
4121                        if let Some(jack) = self.jack_runtime.as_mut() {
4122                            if !self.pending_hw_midi_out_events.is_empty() {
4123                                let out_events =
4124                                    std::mem::take(&mut self.pending_hw_midi_out_events);
4125                                jack.write_events(&out_events);
4126                            }
4127                            let mut in_events = vec![];
4128                            jack.read_events_into(&mut in_events);
4129                            if !in_events.is_empty() {
4130                                self.pending_hw_midi_events.extend(in_events);
4131                            }
4132                            let dropped = jack.take_midi_events_dropped();
4133                            if dropped > 0 {
4134                                tracing::warn!(
4135                                    "JACK MIDI ring full; {dropped} events dropped since last cycle"
4136                                );
4137                            }
4138                        }
4139                    }
4140                    #[cfg(unix)]
4141                    if self.jack_runtime.is_some() {
4142                        self.sync_from_jack_transport().await;
4143                    }
4144                    while let Some(a) = self.pending_requests.pop_front() {
4145                        self.handle_request(a).await;
4146                    }
4147                    self.apply_mute_solo_policy();
4148                    self.append_recorded_cycle();
4149                    self.flush_completed_recordings().await;
4150                    let hw_in_routes = self.midi_hw_in_routes.clone();
4151                    let pending_hw_in_by_device = self.pending_hw_midi_events_by_device.clone();
4152                    let mut reconfigured_tracks = Vec::new();
4153                    let state = self.state_snapshot.load_full();
4154                    for (track_name, track) in state.tracks.iter() {
4155                        let mut track_lock = track.lock();
4156                        if self.jack_runtime_is_some() {
4157                            if !self.pending_hw_midi_events.is_empty() {
4158                                track_lock.push_hw_midi_events(&self.pending_hw_midi_events);
4159                            }
4160                        } else {
4161                            for route in hw_in_routes.iter().filter(|r| &r.to_track == track_name) {
4162                                if let Some(events) = pending_hw_in_by_device.get(&route.device) {
4163                                    track_lock.push_hw_midi_events_to_port(route.to_port, events);
4164                                }
4165                            }
4166                        }
4167                        if track_lock.setup() {
4168                            reconfigured_tracks.push(track_name.clone());
4169                        }
4170                    }
4171                    self.publish_track_meters();
4172                    self.publish_session_runtime_reports().await;
4173                    self.publish_clap_state_dirty().await;
4174                    for track_name in reconfigured_tracks {
4175                        let track = state.tracks.get(&track_name).cloned();
4176                        if let Some(track) = track {
4177                            let (plugins, connections, connectable_connections) = {
4178                                let track_lock = track.lock();
4179                                (
4180                                    track_lock.plugin_graph_plugins(false),
4181                                    track_lock.plugin_graph_connections(),
4182                                    track_lock.connectable_connections(),
4183                                )
4184                            };
4185                            self.notify_clients(Ok(Action::TrackPluginGraph {
4186                                track_name: track_name.clone(),
4187                                plugins,
4188                                connections,
4189                                connectable_connections,
4190                            }))
4191                            .await;
4192                        }
4193                    }
4194                    self.pending_hw_midi_events.clear();
4195                    self.pending_hw_midi_events_by_device.clear();
4196                    let cycle_samples = self.current_cycle_samples();
4197                    if self.transport_running {
4198                        if self.transport_panic_flush_pending {
4199                            self.transport_panic_flush_pending = false;
4200                        } else if self.transport_restart_pending {
4201                            self.transport_restart_pending = false;
4202                        } else {
4203                            let before = self.transport_sample;
4204                            let next = self.transport_sample.saturating_add(cycle_samples);
4205                            let normalized = self.normalize_transport_sample(next);
4206                            let wrapped = normalized != next;
4207                            self.transport_sample = normalized;
4208                            tracing::debug!(
4209                                before,
4210                                delta = cycle_samples,
4211                                next,
4212                                normalized,
4213                                wrapped,
4214                                "transport advanced after HWFinished"
4215                            );
4216                            self.publish_transport_snapshot();
4217                            if wrapped {
4218                                if self.notified_loop_wrap_sample == Some(self.transport_sample) {
4219                                    self.notified_loop_wrap_sample = None;
4220                                } else {
4221                                    self.notify_clients(Ok(Action::TransportPosition(
4222                                        self.transport_sample,
4223                                    )))
4224                                    .await;
4225                                }
4226                            }
4227                        }
4228                    } else {
4229                        tracing::debug!(
4230                            playing = self.playing,
4231                            cycle_samples,
4232                            "transport not advanced because transport_running is false"
4233                        );
4234                    }
4235                    if self.session_clip_playback_enabled && self.playing {
4236                        let before = self.session_transport_sample;
4237                        self.session_transport_sample =
4238                            self.session_transport_sample.saturating_add(cycle_samples);
4239                        tracing::debug!(
4240                            before,
4241                            delta = cycle_samples,
4242                            after = self.session_transport_sample,
4243                            "session transport advanced after HWFinished"
4244                        );
4245                    }
4246                    {
4247                        let echoes = self.apply_modulators(self.active_transport_sample());
4248                        for action in echoes {
4249                            self.notify_clients(Ok(action)).await;
4250                        }
4251                    }
4252                    self.apply_mixosc_automation(self.active_transport_sample());
4253                    let cycle_started = self.start_plan_cycle().await;
4254                    // If a plan cycle is still running, its completion will request the
4255                    // hardware cycle. Requesting here would replay stale arena buffers.
4256                    if self.hw_worker.is_some()
4257                        && !cycle_started
4258                        && (self.playing || self.audio_preview.is_some())
4259                        && self.executor.cycle_complete()
4260                    {
4261                        self.request_hw_cycle().await;
4262                    }
4263                    tracing::debug!(
4264                        cycle_started,
4265                        hw_worker = self.hw_worker.is_some(),
4266                        awaiting_hwfinished = self.awaiting_hwfinished,
4267                        executor_complete = self.executor.cycle_complete(),
4268                        "HWFinished rearm decision"
4269                    );
4270                    #[cfg(unix)]
4271                    {
4272                        if self.jack_runtime.is_some() {
4273                            self.awaiting_hwfinished = true;
4274                        }
4275                    }
4276                    self.handling_hwfinished = false;
4277                }
4278                Message::HWMidiEvents(events) => {
4279                    for hw_event in events {
4280                        let thru_targets: Vec<String> = self
4281                            .midi_hw_thru_routes
4282                            .iter()
4283                            .filter(|route| route.from_device == hw_event.device)
4284                            .map(|route| route.to_device.clone())
4285                            .collect();
4286                        for device in thru_targets {
4287                            self.pending_hw_midi_out_events_by_device.push(HwMidiEvent {
4288                                device,
4289                                event: hw_event.event.clone(),
4290                            });
4291                        }
4292                        if hw_event.event.data.len() >= 3 {
4293                            let status = hw_event.event.data[0];
4294                            if status & 0xF0 == 0xB0 {
4295                                let channel = status & 0x0F;
4296                                let cc = hw_event.event.data[1];
4297                                let value = hw_event.event.data[2];
4298                                self.handle_incoming_hw_cc(&hw_event.device, channel, cc, value)
4299                                    .await;
4300                            }
4301                            if self.step_recording_enabled && status & 0xF0 == 0x90 {
4302                                let channel = status & 0x0F;
4303                                let pitch = hw_event.event.data[1];
4304                                let velocity = hw_event.event.data[2];
4305                                if velocity > 0 {
4306                                    self.notify_clients(Ok(Action::StepRecordMidiNote {
4307                                        device: hw_event.device.clone(),
4308                                        channel,
4309                                        pitch,
4310                                        velocity,
4311                                    }))
4312                                    .await;
4313                                }
4314                            }
4315                        }
4316                        self.pending_hw_midi_events_by_device
4317                            .entry(hw_event.device)
4318                            .or_default()
4319                            .push(hw_event.event);
4320                    }
4321                }
4322                Message::StartAudioPreview {
4323                    samples,
4324                    channels,
4325                    start_sample,
4326                } => {
4327                    self.audio_preview = Some(AudioPreviewPlayback {
4328                        samples,
4329                        channels: channels.max(1),
4330                        cursor: start_sample,
4331                    });
4332                    self.meter_decay_after_stop = None;
4333                    self.set_hw_playing(true).await;
4334                    if !self.awaiting_hwfinished && self.executor.cycle_complete() {
4335                        self.request_hw_cycle().await;
4336                    }
4337                }
4338                Message::StopAudioPreview => {
4339                    self.audio_preview = None;
4340                }
4341                _ => {}
4342            }
4343        }
4344    }
4345
4346    pub(crate) fn collect_hw_midi_output_events(&self) -> Vec<MidiEvent> {
4347        let mut events = vec![];
4348        for track in self.state_snapshot.load_full().tracks.values() {
4349            events.extend(
4350                track
4351                    .lock()
4352                    .take_hw_midi_out_events()
4353                    .into_iter()
4354                    .map(|evt| evt.event),
4355            );
4356        }
4357        events.sort_by_key(|a| a.frame);
4358        events
4359    }
4360
4361    pub(crate) fn collect_hw_midi_output_events_by_device(&mut self) -> Vec<HwMidiEvent> {
4362        let mut events = Vec::<HwMidiEvent>::new();
4363        let routes = self.midi_hw_out_routes.clone();
4364        let mut events_by_track = HashMap::<String, Vec<crate::track::HwMidiOutEvent>>::new();
4365        {
4366            let state = self.state_snapshot.load_full();
4367            for route in &routes {
4368                if events_by_track.contains_key(&route.from_track) {
4369                    continue;
4370                }
4371                let Some(track) = state.tracks.get(&route.from_track) else {
4372                    continue;
4373                };
4374                events_by_track.insert(
4375                    route.from_track.clone(),
4376                    track.lock().take_hw_midi_out_events(),
4377                );
4378            }
4379        }
4380
4381        for route in routes {
4382            let Some(track_events) = events_by_track.get(&route.from_track) else {
4383                continue;
4384            };
4385            for hw_event in track_events
4386                .iter()
4387                .filter(|evt| evt.port == route.from_port)
4388            {
4389                self.update_active_hw_notes_for_track(
4390                    &route.from_track,
4391                    &route.device,
4392                    &hw_event.event.data,
4393                );
4394                events.push(HwMidiEvent {
4395                    device: route.device.clone(),
4396                    event: hw_event.event.clone(),
4397                });
4398            }
4399        }
4400        events.sort_by(|a, b| {
4401            a.event
4402                .frame
4403                .cmp(&b.event.frame)
4404                .then_with(|| a.device.cmp(&b.device))
4405        });
4406        events
4407    }
4408}