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