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