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