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