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