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 write_result =
2670 crate::audio_codec::write_wav_f32(&file_path, &rec.samples, rec.channels, rate as u32);
2671 if let Err(e) = write_result {
2672 self.notify_clients(Err(format!(
2673 "Failed to write recording {}: {}",
2674 file_path.display(),
2675 e
2676 )))
2677 .await;
2678 return;
2679 }
2680 let length = rec.samples.len() / rec.channels;
2681 let clip_rel_name = format!("audio/{}", rec.file_name);
2682 let clip = AudioClip::new(
2683 clip_rel_name.clone(),
2684 rec.start_sample,
2685 rec.start_sample.saturating_add(length.max(1)),
2686 );
2687 let (audio_ins, audio_outs) = if let Some(track) = self.state.lock().tracks.get(&track_name)
2688 {
2689 let track = track.lock();
2690 let audio_ins = track.audio.ins.len();
2691 let audio_outs = track.audio.outs.len();
2692 track.audio.clips.push(clip.clone());
2693 (audio_ins, audio_outs)
2694 } else {
2695 (0, 0)
2696 };
2697 self.notify_clients(Ok(Action::AddClip {
2698 name: clip_rel_name,
2699 track_name: track_name.clone(),
2700 start: rec.start_sample,
2701 length,
2702 offset: 0,
2703 input_channel: 0,
2704 muted: false,
2705 peaks_file: None,
2706 kind: Kind::Audio,
2707 fade_enabled: clip.fade_enabled,
2708 fade_in_samples: clip.fade_in_samples,
2709 fade_out_samples: clip.fade_out_samples,
2710 source_name: None,
2711 source_offset: None,
2712 source_length: None,
2713 preview_name: None,
2714 pitch_correction_points: vec![],
2715 pitch_correction_frame_likeness: None,
2716 pitch_correction_inertia_ms: None,
2717 pitch_correction_formant_compensation: None,
2718 plugin_graph_json: Some(Self::default_clip_plugin_graph_json(audio_ins, audio_outs)),
2719 }))
2720 .await;
2721 if let Some(track) = self.state.lock().tracks.get(&track_name).cloned() {
2722 tokio::task::spawn_blocking(move || {
2723 track.lock().preload_clips();
2724 tracing::debug!("Preloaded clips for track '{}' after recording", track_name);
2725 });
2726 }
2727 }
2728
2729 async fn flush_track_recording(&mut self, track_name: &str) {
2730 let Some(audio_dir) = self.session_audio_dir() else {
2731 self.audio_recordings.remove(track_name);
2732 self.midi_recordings.remove(track_name);
2733 self.completed_audio_recordings
2734 .retain(|(name, _)| name != track_name);
2735 self.completed_midi_recordings
2736 .retain(|(name, _)| name != track_name);
2737 return;
2738 };
2739 let Some(midi_dir) = self.session_midi_dir() else {
2740 self.audio_recordings.remove(track_name);
2741 self.midi_recordings.remove(track_name);
2742 self.completed_audio_recordings
2743 .retain(|(name, _)| name != track_name);
2744 self.completed_midi_recordings
2745 .retain(|(name, _)| name != track_name);
2746 return;
2747 };
2748 if std::fs::create_dir_all(&audio_dir).is_err()
2749 || std::fs::create_dir_all(&midi_dir).is_err()
2750 {
2751 return;
2752 }
2753 let rate = self
2754 .hw_driver
2755 .as_ref()
2756 .map(|o| o.lock().sample_rate())
2757 .unwrap_or(48_000);
2758 let mut i = 0;
2759 while i < self.completed_audio_recordings.len() {
2760 if self.completed_audio_recordings[i].0 == track_name {
2761 let (name, rec) = self.completed_audio_recordings.remove(i);
2762 self.flush_recording_entry(&audio_dir, rate, name, rec)
2763 .await;
2764 } else {
2765 i += 1;
2766 }
2767 }
2768 let mut j = 0;
2769 while j < self.completed_midi_recordings.len() {
2770 if self.completed_midi_recordings[j].0 == track_name {
2771 let (name, rec) = self.completed_midi_recordings.remove(j);
2772 self.flush_midi_recording_entry(&midi_dir, rate as u32, name, rec)
2773 .await;
2774 } else {
2775 j += 1;
2776 }
2777 }
2778
2779 let Some(rec) = self.audio_recordings.remove(track_name) else {
2780 if let Some(mrec) = self.midi_recordings.remove(track_name) {
2781 self.flush_midi_recording_entry(
2782 &midi_dir,
2783 rate as u32,
2784 track_name.to_string(),
2785 mrec,
2786 )
2787 .await;
2788 }
2789 return;
2790 };
2791 self.flush_recording_entry(&audio_dir, rate, track_name.to_string(), rec)
2792 .await;
2793 if let Some(mrec) = self.midi_recordings.remove(track_name) {
2794 self.flush_midi_recording_entry(&midi_dir, rate as u32, track_name.to_string(), mrec)
2795 .await;
2796 }
2797 }
2798
2799 async fn flush_midi_recording_entry(
2800 &mut self,
2801 midi_dir: &Path,
2802 sample_rate: u32,
2803 track_name: String,
2804 mut rec: MidiRecordingSession,
2805 ) {
2806 if rec.events.is_empty() {
2807 return;
2808 }
2809 rec.events.sort_by_key(|(sample, _)| *sample);
2810 let clip_rel_name = format!("midi/{}", rec.file_name);
2811 let clip_len_samples = rec
2812 .events
2813 .last()
2814 .map(|(s, _)| s.saturating_sub(rec.start_sample as u64) as usize + 1)
2815 .unwrap_or(1);
2816
2817 for (sample, _) in &mut rec.events {
2818 *sample = sample.saturating_sub(rec.start_sample as u64);
2819 }
2820 let path = midi_dir.join(&rec.file_name);
2821 if let Err(e) = Self::write_midi_file(&path, sample_rate, &rec.events) {
2822 self.notify_clients(Err(format!(
2823 "Failed to write MIDI recording {}: {}",
2824 path.display(),
2825 e
2826 )))
2827 .await;
2828 return;
2829 }
2830 let mut clip = MIDIClip::new(
2831 clip_rel_name.clone(),
2832 rec.start_sample,
2833 rec.start_sample.saturating_add(clip_len_samples.max(1)),
2834 );
2835 clip.offset = 0;
2836 if let Some(track) = self.state.lock().tracks.get(&track_name) {
2837 track.lock().midi.clips.push(clip);
2838 }
2839 self.notify_clients(Ok(Action::AddClip {
2840 name: clip_rel_name,
2841 track_name: track_name.clone(),
2842 start: rec.start_sample,
2843 length: clip_len_samples,
2844 offset: 0,
2845 input_channel: 0,
2846 muted: false,
2847 peaks_file: None,
2848 kind: Kind::MIDI,
2849 fade_enabled: true,
2850 fade_in_samples: 240,
2851 fade_out_samples: 240,
2852 source_name: None,
2853 source_offset: None,
2854 source_length: None,
2855 preview_name: None,
2856 pitch_correction_points: vec![],
2857 pitch_correction_frame_likeness: None,
2858 pitch_correction_inertia_ms: None,
2859 pitch_correction_formant_compensation: None,
2860 plugin_graph_json: None,
2861 }))
2862 .await;
2863 if let Some(track) = self.state.lock().tracks.get(&track_name).cloned() {
2864 tokio::task::spawn_blocking(move || {
2865 track.lock().preload_clips();
2866 tracing::debug!(
2867 "Preloaded clips for track '{}' after MIDI recording",
2868 track_name
2869 );
2870 });
2871 }
2872 }
2873
2874 fn write_midi_file(
2875 path: &Path,
2876 sample_rate: u32,
2877 events: &[(u64, Vec<u8>)],
2878 ) -> Result<(), String> {
2879 let ppq: u16 = 480;
2880 let ticks_per_second: u64 = 960;
2881 let arena = Arena::new();
2882 let mut track_events: Vec<TrackEvent<'_>> = vec![TrackEvent {
2883 delta: u28::new(0),
2884 kind: TrackEventKind::Meta(MetaMessage::Tempo(u24::new(500_000))),
2885 }];
2886 let mut prev_ticks = 0_u64;
2887 for (sample, data) in events {
2888 let ticks = sample.saturating_mul(ticks_per_second) / sample_rate.max(1) as u64;
2889 let delta = ticks.saturating_sub(prev_ticks).min(u32::MAX as u64) as u32;
2890 prev_ticks = ticks;
2891 let Ok(live) = LiveEvent::parse(data) else {
2892 continue;
2893 };
2894 let kind = live.as_track_event(&arena);
2895 track_events.push(TrackEvent {
2896 delta: u28::new(delta),
2897 kind,
2898 });
2899 }
2900 track_events.push(TrackEvent {
2901 delta: u28::new(0),
2902 kind: TrackEventKind::Meta(MetaMessage::EndOfTrack),
2903 });
2904
2905 let smf = Smf {
2906 header: Header::new(Format::SingleTrack, Timing::Metrical(u15::new(ppq))),
2907 tracks: vec![track_events],
2908 };
2909 let mut file = File::create(path).map_err(|e| e.to_string())?;
2910 smf.write_std(&mut file).map_err(|e| e.to_string())
2911 }
2912
2913 pub async fn init(&mut self) {
2914 let max_threads = num_cpus::get();
2915 let realtime_count = if max_threads > 1 { 1 } else { max_threads };
2916 for id in 0..max_threads {
2917 let class = if id < realtime_count {
2918 WorkerClass::Realtime
2919 } else {
2920 WorkerClass::Refill
2921 };
2922 let priority = match class {
2923 WorkerClass::Realtime => 20,
2924 WorkerClass::Refill => 8,
2925 };
2926 let (tx, rx) = channel::<Message>(32);
2927 let tx_thread = self.tx.clone();
2928 let handler = tokio::spawn(async move {
2929 let wrk = Worker::new(id, rx, tx_thread, priority);
2930 wrk.await.work().await;
2931 });
2932 self.worker_classes.push(class);
2933 self.workers.push(WorkerData::new(tx.clone(), handler));
2934 }
2935 }
2936
2937 async fn notify_clients(&mut self, action: Result<Action, String>) {
2938 self.clients.retain(|client| !client.is_closed());
2939 for client in &self.clients {
2940 client
2941 .send(Message::Response(action.clone()))
2942 .await
2943 .expect("Error sending response to client");
2944 }
2945 }
2946
2947 fn set_osc_enabled_with<F>(&mut self, enabled: bool, start_server: F) -> Result<(), String>
2948 where
2949 F: FnOnce(Sender<Message>) -> Result<OscServer, String>,
2950 {
2951 if enabled {
2952 if self.osc_server.is_none() {
2953 self.osc_server = Some(start_server(self.tx.clone())?);
2954 }
2955 } else if let Some(mut server) = self.osc_server.take() {
2956 server.stop();
2957 }
2958 Ok(())
2959 }
2960
2961 fn track_handle_by_name(&self, track_name: &str) -> Option<Arc<UnsafeMutex<Box<Track>>>> {
2962 self.state.lock().tracks.get(track_name).cloned()
2963 }
2964
2965 fn track_handle_or_err(
2966 &self,
2967 track_name: &str,
2968 ) -> Result<Arc<UnsafeMutex<Box<Track>>>, String> {
2969 self.track_handle_by_name(track_name)
2970 .ok_or_else(|| format!("Track not found: {track_name}"))
2971 }
2972
2973 fn add_clip_to_track(&self, request: ClipAddRequest<'_>) {
2974 if let Some(track) = self.state.lock().tracks.get(request.track_name) {
2975 let track = track.lock();
2976 if track.is_master {
2977 return;
2978 }
2979 match request.kind {
2980 Kind::Audio => {
2981 let mut clip = AudioClip::new(
2982 request.name.to_string(),
2983 request.start,
2984 request.start.saturating_add(request.length.max(1)),
2985 );
2986 clip.offset = request.offset;
2987 let max_lane = track.audio.ins.len().saturating_sub(1);
2988 clip.input_channel = request.input_channel.min(max_lane);
2989 clip.muted = request.muted;
2990 clip.peaks_file = request.peaks_file;
2991 clip.fade_enabled = request.fade_enabled;
2992 clip.fade_in_samples = request.fade_in_samples;
2993 clip.fade_out_samples = request.fade_out_samples;
2994 clip.pitch_correction_preview_name = request.preview_name;
2995 clip.pitch_correction_source_name = request.source_name;
2996 clip.pitch_correction_source_offset = request.source_offset;
2997 clip.pitch_correction_source_length = request.source_length;
2998 clip.pitch_correction_points = request.pitch_correction_points;
2999 clip.pitch_correction_frame_likeness = request.pitch_correction_frame_likeness;
3000 clip.pitch_correction_inertia_ms = request.pitch_correction_inertia_ms;
3001 clip.pitch_correction_formant_compensation =
3002 request.pitch_correction_formant_compensation;
3003 clip.plugin_graph_json = request.plugin_graph_json;
3004 track.audio.clips.push(clip);
3005 #[cfg(unix)]
3006 track.clip_pitch_shifters.clear();
3007 }
3008 Kind::MIDI => {
3009 let mut clip = MIDIClip::new(
3010 request.name.to_string(),
3011 request.start,
3012 request.start.saturating_add(request.length.max(1)),
3013 );
3014 clip.offset = request.offset;
3015 let max_lane = track.midi.ins.len().saturating_sub(1);
3016 clip.input_channel = request.input_channel.min(max_lane);
3017 clip.muted = request.muted;
3018 track.midi.clips.push(clip);
3019 }
3020 }
3021 }
3022 }
3023
3024 fn audio_clip_from_data(data: &crate::message::AudioClipData) -> AudioClip {
3025 let mut clip = AudioClip::new(
3026 data.name.clone(),
3027 data.start,
3028 data.start.saturating_add(data.length.max(1)),
3029 );
3030 clip.offset = data.offset;
3031 clip.input_channel = data.input_channel;
3032 clip.muted = data.muted;
3033 clip.peaks_file = data.peaks_file.clone();
3034 clip.fade_enabled = data.fade_enabled;
3035 clip.fade_in_samples = data.fade_in_samples;
3036 clip.fade_out_samples = data.fade_out_samples;
3037 clip.pitch_correction_preview_name = data.preview_name.clone();
3038 clip.pitch_correction_source_name = data.source_name.clone();
3039 clip.pitch_correction_source_offset = data.source_offset;
3040 clip.pitch_correction_source_length = data.source_length;
3041 clip.pitch_correction_points = data.pitch_correction_points.clone();
3042 clip.pitch_correction_frame_likeness = data.pitch_correction_frame_likeness;
3043 clip.pitch_correction_inertia_ms = data.pitch_correction_inertia_ms;
3044 clip.pitch_correction_formant_compensation = data.pitch_correction_formant_compensation;
3045 clip.plugin_graph_json = data.plugin_graph_json.clone();
3046 clip.grouped_clips = data
3047 .grouped_clips
3048 .iter()
3049 .map(Self::audio_clip_from_data)
3050 .collect();
3051 for child in &mut clip.grouped_clips {
3052 child.fade_enabled = false;
3053 child.fade_in_samples = 0;
3054 child.fade_out_samples = 0;
3055 }
3056 clip
3057 }
3058
3059 fn midi_clip_from_data(data: &crate::message::MidiClipData) -> MIDIClip {
3060 let mut clip = MIDIClip::new(
3061 data.name.clone(),
3062 data.start,
3063 data.start.saturating_add(data.length.max(1)),
3064 );
3065 clip.offset = data.offset;
3066 clip.input_channel = data.input_channel;
3067 clip.muted = data.muted;
3068 clip.grouped_clips = data
3069 .grouped_clips
3070 .iter()
3071 .map(Self::midi_clip_from_data)
3072 .collect();
3073 clip
3074 }
3075
3076 fn add_grouped_clip_to_track(
3077 &self,
3078 track_name: &str,
3079 kind: Kind,
3080 audio_clip: Option<crate::message::AudioClipData>,
3081 midi_clip: Option<crate::message::MidiClipData>,
3082 ) {
3083 if let Some(track) = self.state.lock().tracks.get(track_name) {
3084 let track = track.lock();
3085 if track.is_master {
3086 return;
3087 }
3088 match kind {
3089 Kind::Audio => {
3090 if let Some(mut clip) = audio_clip.map(|clip| Self::audio_clip_from_data(&clip))
3091 {
3092 let max_lane = track.audio.ins.len().saturating_sub(1);
3093 clip.input_channel = clip.input_channel.min(max_lane);
3094 track.audio.clips.push(clip);
3095 #[cfg(unix)]
3096 track.clip_pitch_shifters.clear();
3097 }
3098 }
3099 Kind::MIDI => {
3100 if let Some(mut clip) = midi_clip.map(|clip| Self::midi_clip_from_data(&clip)) {
3101 let max_lane = track.midi.ins.len().saturating_sub(1);
3102 clip.input_channel = clip.input_channel.min(max_lane);
3103 track.midi.clips.push(clip);
3104 }
3105 }
3106 }
3107 }
3108 }
3109
3110 fn remove_clips_from_track(&self, track_name: &str, kind: Kind, clip_indices: &[usize]) {
3111 if let Some(track) = self.state.lock().tracks.get(track_name) {
3112 let track = track.lock();
3113 let mut indices = clip_indices.to_vec();
3114 indices.sort_unstable();
3115 indices.dedup();
3116 match kind {
3117 Kind::Audio => {
3118 for idx in indices.into_iter().rev() {
3119 if idx < track.audio.clips.len() {
3120 track.audio.clips.remove(idx);
3121 }
3122 }
3123 #[cfg(unix)]
3124 track.clip_pitch_shifters.clear();
3125 }
3126 Kind::MIDI => {
3127 for idx in indices.into_iter().rev() {
3128 if idx < track.midi.clips.len() {
3129 track.midi.clips.remove(idx);
3130 }
3131 }
3132 }
3133 }
3134 }
3135 }
3136
3137 fn rename_clip_references(
3138 &self,
3139 track_name: &str,
3140 kind: Kind,
3141 clip_index: usize,
3142 new_name: &str,
3143 ) {
3144 let Some(track) = self.state.lock().tracks.get(track_name) else {
3145 return;
3146 };
3147 let track = track.lock();
3148 let old_name = match kind {
3149 Kind::Audio => {
3150 if clip_index >= track.audio.clips.len() {
3151 return;
3152 }
3153 track.audio.clips[clip_index].name.clone()
3154 }
3155 Kind::MIDI => {
3156 if clip_index >= track.midi.clips.len() {
3157 return;
3158 }
3159 track.midi.clips[clip_index].name.clone()
3160 }
3161 };
3162
3163 let new_file_name = match kind {
3164 Kind::Audio => format!("audio/{}.wav", new_name),
3165 Kind::MIDI => {
3166 let ext = std::path::Path::new(&old_name)
3167 .extension()
3168 .and_then(|e| e.to_str())
3169 .map(|s| s.to_ascii_lowercase())
3170 .filter(|e| e == "mid" || e == "midi")
3171 .unwrap_or_else(|| "mid".to_string());
3172 format!("midi/{}.{}", new_name, ext)
3173 }
3174 };
3175 let _ = track;
3176
3177 for (_, other_track) in self.state.lock().tracks.iter() {
3178 let other_track = other_track.lock();
3179 match kind {
3180 Kind::Audio => {
3181 for clip in &mut other_track.audio.clips {
3182 if clip.name == old_name {
3183 clip.name = new_file_name.clone();
3184 }
3185 if clip.pitch_correction_source_name.as_deref() == Some(old_name.as_str()) {
3186 clip.pitch_correction_source_name = Some(new_file_name.clone());
3187 }
3188 }
3189 }
3190 Kind::MIDI => {
3191 for clip in &mut other_track.midi.clips {
3192 if clip.name == old_name {
3193 clip.name = new_file_name.clone();
3194 }
3195 }
3196 }
3197 }
3198 }
3199 }
3200
3201 fn set_clip_fade(
3202 &self,
3203 track_name: &str,
3204 clip_index: usize,
3205 kind: Kind,
3206 fade_enabled: bool,
3207 fade_in_samples: usize,
3208 fade_out_samples: usize,
3209 ) {
3210 let Some(track) = self.state.lock().tracks.get(track_name) else {
3211 return;
3212 };
3213 let track = track.lock();
3214 match kind {
3215 Kind::Audio => {
3216 if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3217 clip.fade_enabled = fade_enabled;
3218 clip.fade_in_samples = fade_in_samples;
3219 clip.fade_out_samples = fade_out_samples;
3220 }
3221 }
3222 Kind::MIDI => {}
3223 }
3224 }
3225
3226 fn set_clip_bounds(
3227 &self,
3228 track_name: &str,
3229 clip_index: usize,
3230 kind: Kind,
3231 start: usize,
3232 length: usize,
3233 offset: usize,
3234 ) {
3235 let Some(track) = self.state.lock().tracks.get(track_name) else {
3236 return;
3237 };
3238 let track = track.lock();
3239 match kind {
3240 Kind::Audio => {
3241 if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3242 clip.start = start;
3243 clip.end = start.saturating_add(length.max(1));
3244 clip.offset = offset;
3245 clip.pitch_correction_preview_name = None;
3246 clip.pitch_correction_source_name = None;
3247 clip.pitch_correction_source_offset = None;
3248 clip.pitch_correction_source_length = None;
3249 clip.pitch_correction_points.clear();
3250 clip.pitch_correction_frame_likeness = None;
3251 clip.pitch_correction_inertia_ms = None;
3252 clip.pitch_correction_formant_compensation = None;
3253 }
3254 #[cfg(unix)]
3255 track.clip_pitch_shifters.clear();
3256 }
3257 Kind::MIDI => {
3258 if let Some(clip) = track.midi.clips.get_mut(clip_index) {
3259 clip.start = start;
3260 clip.end = start.saturating_add(length.max(1));
3261 clip.offset = offset;
3262 }
3263 }
3264 }
3265 }
3266
3267 fn set_clip_source_name(&self, track_name: &str, clip_index: usize, kind: Kind, name: String) {
3268 let Some(track) = self.state.lock().tracks.get(track_name) else {
3269 return;
3270 };
3271 let track = track.lock();
3272 match kind {
3273 Kind::Audio => {
3274 if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3275 clip.name = name;
3276 }
3277 #[cfg(unix)]
3278 track.clip_pitch_shifters.clear();
3279 }
3280 Kind::MIDI => {
3281 if let Some(clip) = track.midi.clips.get_mut(clip_index) {
3282 clip.name = name;
3283 }
3284 }
3285 }
3286 }
3287
3288 fn set_clip_muted(&self, track_name: &str, clip_index: usize, kind: Kind, muted: bool) {
3289 let Some(track) = self.state.lock().tracks.get(track_name) else {
3290 return;
3291 };
3292 let track = track.lock();
3293 match kind {
3294 Kind::Audio => {
3295 if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3296 clip.muted = muted;
3297 }
3298 }
3299 Kind::MIDI => {
3300 if let Some(clip) = track.midi.clips.get_mut(clip_index) {
3301 clip.muted = muted;
3302 }
3303 }
3304 }
3305 }
3306
3307 #[allow(clippy::too_many_arguments)]
3308 fn set_clip_pitch_correction(
3309 &self,
3310 track_name: &str,
3311 clip_index: usize,
3312 preview_name: Option<String>,
3313 source_name: Option<String>,
3314 source_offset: Option<usize>,
3315 source_length: Option<usize>,
3316 pitch_correction_points: Vec<crate::message::PitchCorrectionPointData>,
3317 pitch_correction_frame_likeness: Option<f32>,
3318 pitch_correction_inertia_ms: Option<u16>,
3319 pitch_correction_formant_compensation: Option<bool>,
3320 ) {
3321 if let Some(track) = self.state.lock().tracks.get(track_name) {
3322 let track = track.lock();
3323 if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3324 clip.pitch_correction_preview_name = preview_name;
3325 clip.pitch_correction_source_name = source_name;
3326 clip.pitch_correction_source_offset = source_offset;
3327 clip.pitch_correction_source_length = source_length;
3328 clip.pitch_correction_points = pitch_correction_points;
3329 clip.pitch_correction_frame_likeness = pitch_correction_frame_likeness;
3330 clip.pitch_correction_inertia_ms = pitch_correction_inertia_ms;
3331 clip.pitch_correction_formant_compensation = pitch_correction_formant_compensation;
3332 }
3333 #[cfg(unix)]
3334 track.clip_pitch_shifters.clear();
3335 }
3336 }
3337
3338 async fn request_hw_cycle(&mut self) {
3339 if self.awaiting_hwfinished {
3340 return;
3341 }
3342 self.apply_hw_out_gain_and_meter().await;
3343 if let Some(worker) = &self.hw_worker {
3344 if !self.pending_hw_midi_out_events_by_device.is_empty() {
3345 let out_events = std::mem::take(&mut self.pending_hw_midi_out_events_by_device);
3346 if let Err(e) = worker.tx.send(Message::HWMidiOutEvents(out_events)).await {
3347 error!("Error sending HWMidiOutEvents {e}");
3348 }
3349 }
3350 match worker.tx.send(Message::TracksFinished).await {
3351 Ok(_) => {
3352 self.awaiting_hwfinished = true;
3353 }
3354 Err(e) => {
3355 error!("Error sending TracksFinished {e}");
3356 }
3357 }
3358 }
3359 }
3360
3361 async fn clear_hw_midi_output_state(&mut self, send_panic: bool) {
3362 self.pending_hw_midi_out_events.clear();
3363 self.pending_hw_midi_out_events_by_device.clear();
3364 {
3365 let state = self.state.lock();
3366 for track in state.tracks.values() {
3367 track.lock().take_hw_midi_out_events();
3368 }
3369 }
3370
3371 let panic_events = if send_panic {
3372 self.note_off_events_for_all_active_tracks()
3373 } else {
3374 vec![]
3375 };
3376
3377 if let Some(worker) = &self.hw_worker {
3378 if let Err(e) = worker.tx.send(Message::ClearHWMidiOutEvents).await {
3379 error!("Error clearing pending HWMidiOutEvents {e}");
3380 }
3381 if !panic_events.is_empty()
3382 && let Err(e) = worker.tx.send(Message::HWMidiOutEvents(panic_events)).await
3383 {
3384 error!("Error sending transport restart MIDI panic events {e}");
3385 }
3386 } else if !panic_events.is_empty() {
3387 self.pending_hw_midi_out_events_by_device
3388 .extend(panic_events);
3389 }
3390 }
3391
3392 fn invalidate_track_cycle_state(&mut self) {
3393 self.track_process_epoch = self.track_process_epoch.saturating_add(1);
3394 self.track_processing_started_at.clear();
3395 let state = self.state.lock();
3396 for track in state.tracks.values() {
3397 let t = track.lock();
3398 t.audio.finished = false;
3399 t.audio.processing = false;
3400 }
3401 }
3402
3403 fn force_stalled_track_completions(&mut self) {
3404 let now = Instant::now();
3405 let state = self.state.lock();
3406 for (track_name, track) in state.tracks.iter() {
3407 let started = self.track_processing_started_at.get(track_name).copied();
3408 let Some(started) = started else {
3409 continue;
3410 };
3411 if now.duration_since(started) < Self::TRACK_PROCESS_TIMEOUT {
3412 continue;
3413 }
3414 let t = track.lock();
3415 if t.audio.finished || !t.audio.processing {
3416 self.track_processing_started_at.remove(track_name);
3417 continue;
3418 }
3419 for out in &t.audio.outs {
3420 let out_buf = out.buffer.lock();
3421 out_buf.fill(0.0);
3422 *out.finished.lock() = true;
3423 }
3424 t.audio.processing = false;
3425 t.audio.finished = true;
3426 self.track_processing_started_at.remove(track_name);
3427 tracing::warn!(
3428 "Track '{}' exceeded process timeout ({} ms); forcing silent completion for cycle",
3429 track_name,
3430 Self::TRACK_PROCESS_TIMEOUT.as_millis()
3431 );
3432 }
3433 }
3434
3435 fn should_publish_hw_out_meters(&mut self) -> bool {
3436 let now = Instant::now();
3437 match self.last_hw_out_meter_publish {
3438 Some(last) if now.duration_since(last) < Self::METER_PUBLISH_INTERVAL => false,
3439 _ => {
3440 self.last_hw_out_meter_publish = Some(now);
3441 true
3442 }
3443 }
3444 }
3445
3446 fn should_publish_track_meters(&mut self) -> bool {
3447 let now = Instant::now();
3448 match self.last_track_meter_publish {
3449 Some(last) if now.duration_since(last) < Self::METER_PUBLISH_INTERVAL => false,
3450 _ => {
3451 self.last_track_meter_publish = Some(now);
3452 true
3453 }
3454 }
3455 }
3456
3457 fn should_publish_hw_out_linear(&mut self, peaks_linear: &[f32]) -> bool {
3458 #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
3459 {
3460 self.hw_out_meter_publish_phase = !self.hw_out_meter_publish_phase;
3461 if !self.hw_out_meter_publish_phase {
3462 return false;
3463 }
3464 let changed = if self.last_hw_out_meter_linear.len() != peaks_linear.len() {
3465 true
3466 } else {
3467 self.last_hw_out_meter_linear
3468 .iter()
3469 .zip(peaks_linear.iter())
3470 .any(|(prev, next)| (prev - next).abs() >= Self::HW_OUT_METER_LINEAR_EPSILON)
3471 };
3472 if !changed {
3473 return false;
3474 }
3475 self.last_hw_out_meter_linear.clear();
3476 self.last_hw_out_meter_linear
3477 .extend_from_slice(peaks_linear);
3478 true
3479 }
3480 #[cfg(not(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd")))]
3481 {
3482 let _ = peaks_linear;
3483 false
3484 }
3485 }
3486
3487 async fn maybe_notify_hw_out_meter(&mut self, _meter_db: Vec<f32>) {
3488 {}
3489 }
3490
3491 fn collect_changed_track_meters(
3492 &mut self,
3493 _tracks: &[(String, Arc<UnsafeMutex<Box<Track>>>)],
3494 ) -> Vec<(String, Vec<f32>)> {
3495 Vec::new()
3496 }
3497
3498 async fn apply_hw_out_gain_and_meter(&mut self) {
3499 let gain = if self.hw_out_muted {
3500 0.0
3501 } else {
3502 10.0_f32.powf(self.hw_out_level_db / 20.0)
3503 };
3504 let should_notify_interval = self.should_publish_hw_out_meters();
3505 if let Some(oss) = self.hw_driver.clone() {
3506 let hw = oss.lock();
3507 hw.set_output_gain_balance(gain, self.hw_out_balance);
3508 if !should_notify_interval {
3509 return;
3510 }
3511 } else {
3512 #[cfg(unix)]
3513 {
3514 if let Some(jack) = self.jack_runtime.clone() {
3515 jack.lock().set_output_gain_linear(gain);
3516 jack.lock().set_output_balance(self.hw_out_balance);
3517 if !should_notify_interval {
3518 return;
3519 }
3520 } else {
3521 return;
3522 }
3523 }
3524 #[cfg(not(unix))]
3525 {
3526 return;
3527 }
3528 }
3529 let peaks_linear = if let Some(oss) = self.hw_driver.clone() {
3530 oss.lock().output_meter_linear(gain, self.hw_out_balance)
3531 } else {
3532 #[cfg(unix)]
3533 {
3534 if let Some(jack) = self.jack_runtime.clone() {
3535 let outs = jack.lock().audio_outs();
3536 let out_count = outs.len();
3537 let b = if out_count == 2 {
3538 self.hw_out_balance.clamp(-1.0, 1.0)
3539 } else {
3540 0.0
3541 };
3542 let mut meters_linear = Vec::with_capacity(out_count);
3543 for (channel_idx, channel) in outs.iter().enumerate() {
3544 let balance_gain = if out_count == 2 {
3545 if channel_idx == 0 {
3546 (1.0 - b).clamp(0.0, 1.0)
3547 } else {
3548 (1.0 + b).clamp(0.0, 1.0)
3549 }
3550 } else {
3551 1.0
3552 };
3553 let buf = channel.buffer.lock();
3554 let peak = crate::simd::peak_abs(buf) * gain * balance_gain;
3555 meters_linear.push(peak);
3556 }
3557 meters_linear
3558 } else {
3559 return;
3560 }
3561 }
3562 #[cfg(not(unix))]
3563 {
3564 return;
3565 }
3566 };
3567 if self.hw_out_peak_hold_linear.len() != peaks_linear.len() {
3568 self.hw_out_peak_hold_linear.resize(peaks_linear.len(), 0.0);
3569 }
3570 let mut held_peaks = Vec::with_capacity(peaks_linear.len());
3571 for (idx, peak_now) in peaks_linear.iter().copied().enumerate() {
3572 let held = self.hw_out_peak_hold_linear[idx] * 0.92;
3573 let next = peak_now.max(held);
3574 self.hw_out_peak_hold_linear[idx] = next;
3575 held_peaks.push(next);
3576 }
3577 let should_notify =
3578 should_notify_interval && self.should_publish_hw_out_linear(&held_peaks);
3579 let meter_db: Vec<f32> = held_peaks
3580 .into_iter()
3581 .map(Self::meter_linear_to_db)
3582 .collect();
3583 self.latest_hw_out_meter_db = Arc::new(meter_db.clone());
3584 if should_notify {
3585 self.maybe_notify_hw_out_meter(meter_db).await;
3586 }
3587 }
3588
3589 fn preload_track_clips_spawn(&self) {
3590 let tracks: Vec<_> = self.state.lock().tracks.values().cloned().collect();
3591 for track in tracks {
3592 tokio::task::spawn_blocking(move || {
3593 track.lock().preload_clips();
3594 });
3595 }
3596 }
3597
3598 async fn preload_track_clips(&self) {
3599 let tracks: Vec<_> = self.state.lock().tracks.values().cloned().collect();
3600 if tracks.is_empty() {
3601 return;
3602 }
3603 let mut handles = Vec::with_capacity(tracks.len());
3604 for track in tracks {
3605 handles.push(tokio::task::spawn_blocking(move || {
3606 track.lock().preload_clips();
3607 }));
3608 }
3609 for handle in handles {
3610 if let Err(e) = handle.await {
3611 tracing::warn!("Clip preload task panicked: {e}");
3612 }
3613 }
3614 }
3615
3616 async fn send_tracks(&mut self) -> bool {
3617 if !self.playing {
3618 return false;
3619 }
3620 self.refresh_realtime_infection();
3621 let mut cycle_underflows = 0usize;
3622 {
3623 let state = self.state.lock();
3624 for track in state.tracks.values() {
3625 cycle_underflows =
3626 cycle_underflows.saturating_add(track.lock().take_hybrid_underflow_delta());
3627 }
3628 }
3629 if cycle_underflows > 0 {
3630 self.refill_budget_per_pass = (self.refill_budget_per_pass + 1).min(8);
3631 } else {
3632 self.refill_budget_per_pass = self.refill_budget_per_pass.saturating_sub(1).max(1);
3633 }
3634 self.force_stalled_track_completions();
3635 let mut finished = true;
3636 let mut dispatched = 0;
3637 let mut refill_dispatched = 0usize;
3638 let mut realtime_fallback_dispatched = 0usize;
3639 loop {
3640 let next_track = {
3641 let state = self.state.lock();
3642 let mut next_realtime = None;
3643 let mut next_playback = None;
3644 for track in state.tracks.values() {
3645 let t = track.lock();
3646 if t.audio.finished {
3647 continue;
3648 }
3649 let needs_refill_event = t.hybrid_needs_refill();
3650 if !t.is_realtime_domain()
3651 && !needs_refill_event
3652 && t.try_consume_hybrid_playback_cycle()
3653 {
3654 continue;
3655 }
3656 finished = false;
3657 if t.audio.processing || !t.audio.ready() {
3658 continue;
3659 }
3660 if t.is_realtime_domain() {
3661 if next_realtime.is_none() {
3662 next_realtime = Some(track.clone());
3663 }
3664 } else if next_playback.is_none() {
3665 next_playback = Some(track.clone());
3666 }
3667 }
3668 if next_realtime.is_none()
3669 && next_playback.is_some()
3670 && refill_dispatched >= self.refill_budget_per_pass
3671 {
3672 self.refill_budget_throttle_count =
3673 self.refill_budget_throttle_count.saturating_add(1);
3674 }
3675 next_realtime.or(next_playback)
3676 };
3677
3678 let Some(track) = next_track else {
3679 if dispatched > 0 {
3680 tracing::info!("send_tracks dispatched {} tracks", dispatched);
3681 }
3682 return finished;
3683 };
3684 let worker_class = {
3685 let t = track.lock();
3686 if t.is_realtime_domain() {
3687 WorkerClass::Realtime
3688 } else {
3689 WorkerClass::Refill
3690 }
3691 };
3692 let worker_index = if let Some(index) = self.take_ready_worker_index(worker_class) {
3693 Some(index)
3694 } else if matches!(worker_class, WorkerClass::Realtime)
3695 && self.realtime_fallback_enabled
3696 && realtime_fallback_dispatched < self.realtime_fallback_budget_per_pass
3697 {
3698 self.take_ready_worker_index(WorkerClass::Refill)
3699 } else {
3700 None
3701 };
3702 let Some(worker_index) = worker_index else {
3703 self.force_stalled_track_completions();
3704 if dispatched > 0 {
3705 tracing::info!(
3706 "send_tracks dispatched {} tracks (no more workers)",
3707 dispatched
3708 );
3709 }
3710 return false;
3711 };
3712
3713 let t = track.lock();
3714 if t.audio.finished || t.audio.processing || !t.audio.ready() {
3715 continue;
3716 }
3717 if matches!(worker_class, WorkerClass::Refill) {
3718 let _ = t.hybrid_take_refill_wakeup();
3720 }
3721 dispatched += 1;
3722 if matches!(worker_class, WorkerClass::Refill) {
3723 refill_dispatched = refill_dispatched.saturating_add(1);
3724 } else if !matches!(
3725 self.worker_classes
3726 .get(worker_index)
3727 .copied()
3728 .unwrap_or(WorkerClass::Realtime),
3729 WorkerClass::Realtime
3730 ) {
3731 realtime_fallback_dispatched = realtime_fallback_dispatched.saturating_add(1);
3732 self.realtime_fallback_dispatch_count =
3733 self.realtime_fallback_dispatch_count.saturating_add(1);
3734 }
3735 t.set_transport_sample(self.transport_sample);
3736 t.set_loop_config(self.loop_enabled, self.loop_range_samples);
3737 t.set_transport_timing(self.tempo_bpm, self.tsig_num, self.tsig_denom);
3738 let low_watermark = if self.hybrid_low_watermark_frames > 0 {
3739 self.hybrid_low_watermark_frames
3740 } else {
3741 self.current_cycle_samples().saturating_mul(4).max(1)
3742 };
3743 let realtime_frames = if self.hybrid_realtime_frames > 0 {
3744 self.hybrid_realtime_frames
3745 } else {
3746 self.current_cycle_samples().max(1)
3747 };
3748 let playback_frames = if self.hybrid_playback_frames > 0 {
3749 self.hybrid_playback_frames
3750 } else {
3751 self.current_cycle_samples().max(1)
3752 };
3753 t.configure_hybrid_timing(realtime_frames, low_watermark, playback_frames);
3754 t.process_epoch = self.track_process_epoch;
3755
3756 t.set_clip_playback_enabled(self.clip_playback_enabled && self.playing);
3757
3758 t.set_record_tap_enabled(self.playing && self.record_enabled);
3759 t.audio.processing = true;
3760 self.track_processing_started_at
3761 .insert(t.name.clone(), Instant::now());
3762 let worker = &self.workers[worker_index];
3763 if let Err(e) = worker.tx.send(Message::ProcessTrack(track.clone())).await {
3764 t.audio.processing = false;
3765 self.track_processing_started_at.remove(&t.name);
3766 self.notify_clients(Err(format!("Failed to send track to worker: {}", e)))
3767 .await;
3768 }
3769 }
3770 }
3771
3772 async fn on_all_tracks_finished(&mut self) {
3773 if self.transport_restart_pending {
3774 let state = self.state.lock();
3775 for track in state.tracks.values() {
3776 track.lock().take_hw_midi_out_events();
3777 }
3778 } else if self.hw_worker.is_some() {
3779 self.active_hw_notes_cycle_start = self.active_hw_notes_by_track.clone();
3780 let mut out_events = self.collect_hw_midi_output_events_by_device();
3781 if self.loop_enabled
3782 && let Some((_, loop_end)) = self.loop_range_samples
3783 {
3784 let cycle_end = self
3785 .transport_sample
3786 .saturating_add(self.current_cycle_samples());
3787 if self.transport_sample < loop_end && cycle_end > loop_end {
3788 let wrap_frame = loop_end
3789 .saturating_sub(self.transport_sample)
3790 .min(self.current_cycle_samples())
3791 as u32;
3792 out_events.extend(self.note_off_events_for_active_snapshot(
3793 &self.active_hw_notes_cycle_start,
3794 wrap_frame,
3795 ));
3796 out_events.sort_by(|a, b| {
3797 a.event
3798 .frame
3799 .cmp(&b.event.frame)
3800 .then_with(|| a.device.cmp(&b.device))
3801 });
3802 }
3803 }
3804 self.pending_hw_midi_out_events_by_device.extend(out_events);
3805 } else {
3806 self.pending_hw_midi_out_events = self.collect_hw_midi_output_events();
3807 }
3808 self.request_hw_cycle().await;
3809 }
3810
3811 fn take_ready_worker_index(&mut self, class: WorkerClass) -> Option<usize> {
3812 let queue = match class {
3813 WorkerClass::Realtime => &mut self.ready_realtime_workers,
3814 WorkerClass::Refill => &mut self.ready_refill_workers,
3815 };
3816 while !queue.is_empty() {
3817 let worker_index = queue.remove(0);
3818 if worker_index < self.workers.len() {
3819 return Some(worker_index);
3820 }
3821 }
3822 None
3823 }
3824
3825 fn push_ready_worker(&mut self, worker_index: usize) {
3826 match self
3827 .worker_classes
3828 .get(worker_index)
3829 .copied()
3830 .unwrap_or(WorkerClass::Refill)
3831 {
3832 WorkerClass::Realtime => self.ready_realtime_workers.push(worker_index),
3833 WorkerClass::Refill => self.ready_refill_workers.push(worker_index),
3834 }
3835 }
3836
3837 async fn publish_track_meters(&mut self) {
3838 if !self.should_publish_track_meters() {
3839 return;
3840 }
3841 let tracks: Vec<(String, Arc<UnsafeMutex<Box<Track>>>)> = self
3842 .state
3843 .lock()
3844 .tracks
3845 .iter()
3846 .map(|(name, track)| (name.clone(), track.clone()))
3847 .collect();
3848 let mut snapshot = Vec::with_capacity(tracks.len());
3849 for (name, track) in &tracks {
3850 let linear = self
3851 .track_meter_linear_by_track
3852 .get(name)
3853 .cloned()
3854 .unwrap_or_else(|| track.lock().output_meter_linear());
3855 let output_db = linear
3856 .iter()
3857 .copied()
3858 .map(Self::meter_linear_to_db)
3859 .collect::<Vec<_>>();
3860 snapshot.push((name.clone(), output_db));
3861 }
3862 self.latest_track_meter_snapshot = Arc::new(snapshot);
3863 let meters = self.collect_changed_track_meters(&tracks);
3864 for (track_name, output_db) in meters {
3865 self.notify_clients(Ok(Action::TrackMeters {
3866 track_name,
3867 output_db,
3868 }))
3869 .await;
3870 }
3871 }
3872
3873 fn reset_meters_after_stop(&mut self) {
3874 self.last_hw_out_meter_publish = None;
3875 self.last_track_meter_publish = None;
3876 self.hw_out_peak_hold_linear.fill(0.0);
3877 #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
3878 {
3879 self.last_hw_out_meter_linear.clear();
3880 }
3881 let hw_channels = self.latest_hw_out_meter_db.len();
3882 self.latest_hw_out_meter_db = Arc::new(vec![-90.0; hw_channels]);
3883
3884 let tracks: Vec<(String, Arc<UnsafeMutex<Box<Track>>>)> = self
3885 .state
3886 .lock()
3887 .tracks
3888 .iter()
3889 .map(|(name, track)| (name.clone(), track.clone()))
3890 .collect();
3891 self.track_meter_linear_by_track.clear();
3892 let mut snapshot = Vec::with_capacity(tracks.len());
3893 for (name, track) in tracks {
3894 let t = track.lock();
3895 t.clear_output_meters();
3896 let width = t.output_meter_linear().len();
3897 let zero_linear = vec![0.0; width];
3898 self.track_meter_linear_by_track
3899 .insert(name.clone(), zero_linear);
3900 snapshot.push((name, vec![-90.0; width]));
3901 }
3902 self.latest_track_meter_snapshot = Arc::new(snapshot);
3903 }
3904
3905 pub fn check_if_leads_to_kind(
3906 &self,
3907 kind: Kind,
3908 current_track_name: &str,
3909 target_track_name: &str,
3910 ) -> bool {
3911 routing::would_create_cycle(
3912 &target_track_name.to_string(),
3913 ¤t_track_name.to_string(),
3914 |track_name| self.connected_neighbors(kind, track_name),
3915 )
3916 }
3917
3918 fn connected_neighbors(&self, kind: Kind, current_track_name: &str) -> Vec<String> {
3919 let state = self.state.lock();
3920 let mut found_neighbors = Vec::new();
3921
3922 if let Some(current_track_handle) = state.tracks.get(current_track_name) {
3923 let current_track = current_track_handle.lock();
3924
3925 match kind {
3926 Kind::Audio => {
3927 for out_port in ¤t_track.audio.outs {
3928 let conns = out_port.connections.lock();
3929 for conn in conns.iter() {
3930 for (name, next_track_handle) in &state.tracks {
3931 let next_track = next_track_handle.lock();
3932 let is_connected =
3933 next_track.audio.ins.iter().any(|ins_port| {
3934 Arc::ptr_eq(&ins_port.buffer, &conn.buffer)
3935 });
3936
3937 if is_connected {
3938 found_neighbors.push(name.clone());
3939 }
3940 }
3941 }
3942 }
3943 }
3944 Kind::MIDI => {
3945 for out_port in ¤t_track.midi.outs {
3946 let conns = out_port.lock().connections.clone();
3947 for conn in conns.iter() {
3948 for (name, next_track_handle) in &state.tracks {
3949 let next_track = next_track_handle.lock();
3950 let is_connected = next_track
3951 .midi
3952 .ins
3953 .iter()
3954 .any(|ins_port| Arc::ptr_eq(ins_port, conn));
3955
3956 if is_connected {
3957 found_neighbors.push(name.clone());
3958 }
3959 }
3960 }
3961 }
3962 }
3963 }
3964 }
3965 found_neighbors
3966 }
3967
3968 async fn handle_request(&mut self, a: Action) {
3969 match a {
3970 Action::Undo => {
3971 let actions = match self.history.undo() {
3972 Some(actions) => actions,
3973 None => {
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 return;
3980 }
3981 };
3982
3983 let was_suspended = self.history_suspended;
3984 self.history_suspended = true;
3985 for action in actions {
3986 self.handle_request_inner(action, false).await;
3987 }
3988 self.history_suspended = was_suspended;
3989 self.notify_clients(Ok(Action::Undo)).await;
3990 self.notify_clients(Ok(Action::HistoryState {
3991 dirty: self.history.is_dirty(),
3992 }))
3993 .await;
3994 }
3995 Action::Redo => {
3996 let actions = match self.history.redo() {
3997 Some(actions) => actions,
3998 None => {
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 return;
4005 }
4006 };
4007
4008 let was_suspended = self.history_suspended;
4009 self.history_suspended = true;
4010 for action in actions {
4011 self.handle_request_inner(action, false).await;
4012 }
4013 self.history_suspended = was_suspended;
4014 self.notify_clients(Ok(Action::Redo)).await;
4015 self.notify_clients(Ok(Action::HistoryState {
4016 dirty: self.history.is_dirty(),
4017 }))
4018 .await;
4019 }
4020 Action::ApplyGroupedActions(actions) => {
4021 self.handle_request_inner(Action::BeginHistoryGroup, true)
4022 .await;
4023 for action in actions {
4024 self.handle_request_inner(action, true).await;
4025 }
4026 self.handle_request_inner(Action::EndHistoryGroup, true)
4027 .await;
4028 }
4029 other => {
4030 self.handle_request_inner(other, true).await;
4031 }
4032 }
4033 }
4034
4035 async fn handle_request_inner(&mut self, action_to_process: Action, record_history: bool) {
4036 let a = action_to_process.clone();
4037 let suppress_timing_history = self.playing
4038 && matches!(
4039 &action_to_process,
4040 Action::SetTempo(_) | Action::SetTimeSignature { .. }
4041 );
4042 let mut extra_inverse_actions: Vec<Action> = Vec::new();
4043 if record_history
4044 && !self.history_suspended
4045 && let Action::RemoveTrack(ref track_name) = action_to_process
4046 {
4047 for route in self
4048 .midi_hw_in_routes
4049 .iter()
4050 .filter(|route| &route.to_track == track_name)
4051 {
4052 extra_inverse_actions.push(Action::Connect {
4053 from_track: format!("midi:hw:in:{}", route.device),
4054 from_port: 0,
4055 to_track: route.to_track.clone(),
4056 to_port: route.to_port,
4057 kind: Kind::MIDI,
4058 });
4059 }
4060 for route in self
4061 .midi_hw_out_routes
4062 .iter()
4063 .filter(|route| &route.from_track == track_name)
4064 {
4065 extra_inverse_actions.push(Action::Connect {
4066 from_track: route.from_track.clone(),
4067 from_port: route.from_port,
4068 to_track: format!("midi:hw:out:{}", route.device),
4069 to_port: 0,
4070 kind: Kind::MIDI,
4071 });
4072 }
4073 }
4074 if record_history
4075 && !self.history_suspended
4076 && matches!(action_to_process, Action::ClearAllMidiLearnBindings)
4077 {
4078 if let Some(binding) = self.global_midi_learn_play_pause.clone() {
4079 extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
4080 target: crate::message::GlobalMidiLearnTarget::PlayPause,
4081 binding: Some(binding),
4082 });
4083 }
4084 if let Some(binding) = self.global_midi_learn_stop.clone() {
4085 extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
4086 target: crate::message::GlobalMidiLearnTarget::Stop,
4087 binding: Some(binding),
4088 });
4089 }
4090 if let Some(binding) = self.global_midi_learn_record_toggle.clone() {
4091 extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
4092 target: crate::message::GlobalMidiLearnTarget::RecordToggle,
4093 binding: Some(binding),
4094 });
4095 }
4096 }
4097 let mut inverse_actions = if record_history
4098 && !suppress_timing_history
4099 && should_record(&action_to_process)
4100 && !self.history_suspended
4101 {
4102 let state = self.state.lock();
4103 create_inverse_actions(&action_to_process, state).map(|mut actions| {
4104 actions.extend(extra_inverse_actions);
4105 actions
4106 })
4107 } else {
4108 None
4109 };
4110 if record_history && !suppress_timing_history && !self.history_suspended {
4111 match &action_to_process {
4112 Action::SetTempo(_) => {
4113 inverse_actions = Some(vec![Action::SetTempo(self.tempo_bpm)]);
4114 }
4115 Action::SetLoopEnabled(_) => {
4116 inverse_actions = Some(vec![Action::SetLoopEnabled(self.loop_enabled)]);
4117 }
4118 Action::SetLoopRange(_) => {
4119 inverse_actions = Some(vec![
4120 Action::SetLoopRange(self.loop_range_samples),
4121 Action::SetLoopEnabled(self.loop_enabled),
4122 ]);
4123 }
4124 Action::SetPunchEnabled(_) => {
4125 inverse_actions = Some(vec![Action::SetPunchEnabled(self.punch_enabled)]);
4126 }
4127 Action::SetPunchRange(_) => {
4128 inverse_actions = Some(vec![
4129 Action::SetPunchRange(self.punch_range_samples),
4130 Action::SetPunchEnabled(self.punch_enabled),
4131 ]);
4132 }
4133 Action::SetMetronomeEnabled(_) => {
4134 inverse_actions =
4135 Some(vec![Action::SetMetronomeEnabled(self.metronome_enabled)]);
4136 }
4137 Action::SetTimeSignature { .. } => {
4138 inverse_actions = Some(vec![Action::SetTimeSignature {
4139 numerator: self.tsig_num,
4140 denominator: self.tsig_denom,
4141 }]);
4142 }
4143 Action::SetClipPlaybackEnabled(_) => {
4144 inverse_actions = Some(vec![Action::SetClipPlaybackEnabled(
4145 self.clip_playback_enabled,
4146 )]);
4147 }
4148 Action::SetRecordEnabled(_) => {
4149 inverse_actions = Some(vec![Action::SetRecordEnabled(self.record_enabled)]);
4150 }
4151 Action::SetGlobalMidiLearnBinding { target, .. } => {
4152 let binding = match target {
4153 crate::message::GlobalMidiLearnTarget::PlayPause => {
4154 self.global_midi_learn_play_pause.clone()
4155 }
4156 crate::message::GlobalMidiLearnTarget::Stop => {
4157 self.global_midi_learn_stop.clone()
4158 }
4159 crate::message::GlobalMidiLearnTarget::RecordToggle => {
4160 self.global_midi_learn_record_toggle.clone()
4161 }
4162 };
4163 inverse_actions = Some(vec![Action::SetGlobalMidiLearnBinding {
4164 target: *target,
4165 binding,
4166 }]);
4167 }
4168 _ => {}
4169 }
4170 }
4171
4172 match action_to_process {
4173 Action::Play => {
4174 tracing::info!(
4175 "Action::Play pressed, transport_sample={}",
4176 self.transport_sample
4177 );
4178 self.playing = true;
4179 self.transport_restart_pending = true;
4180 self.invalidate_track_cycle_state();
4181 if let Some(driver) = self.hw_driver.as_mut() {
4182 driver.lock().set_playing(true);
4183 }
4184 #[cfg(unix)]
4185 if let Some(jack) = &self.jack_runtime
4186 && let Err(e) = jack.lock().transport_start()
4187 {
4188 self.notify_clients(Err(e)).await;
4189 }
4190 self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
4191 .await;
4192 self.preload_track_clips().await;
4193 let send_result = self.send_tracks().await;
4194 tracing::info!("send_tracks after Play returned finished={}", send_result);
4195 if !self.awaiting_hwfinished
4196 && !self.handling_hwfinished
4197 && send_result
4198 && self.hw_worker.is_some()
4199 {
4200 self.transport_restart_pending = false;
4201 self.request_hw_cycle().await;
4202 }
4203 }
4204 Action::Pause => {
4205 self.clip_playback_enabled = false;
4206 for track in self.state.lock().tracks.values() {
4207 track.lock().set_clip_playback_enabled(false);
4208 }
4209 if !self.playing {
4210 self.playing = true;
4211 self.transport_restart_pending = true;
4212 self.invalidate_track_cycle_state();
4213 if let Some(driver) = self.hw_driver.as_mut() {
4214 driver.lock().set_playing(true);
4215 }
4216 #[cfg(unix)]
4217 if let Some(jack) = &self.jack_runtime
4218 && let Err(e) = jack.lock().transport_start()
4219 {
4220 self.notify_clients(Err(e)).await;
4221 }
4222 self.preload_track_clips().await;
4223 if !self.awaiting_hwfinished
4224 && !self.handling_hwfinished
4225 && self.send_tracks().await
4226 && self.hw_worker.is_some()
4227 {
4228 self.transport_restart_pending = false;
4229 self.request_hw_cycle().await;
4230 }
4231 }
4232 self.notify_clients(Ok(Action::Pause)).await;
4233 self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
4234 .await;
4235 }
4236 Action::Stop => {
4237 self.playing = false;
4238 self.transport_panic_flush_pending = false;
4239 self.transport_restart_pending = false;
4240 self.invalidate_track_cycle_state();
4241 if let Some(driver) = self.hw_driver.as_mut() {
4242 driver.lock().set_playing(false);
4243 }
4244 #[cfg(unix)]
4245 if let Some(jack) = &self.jack_runtime
4246 && let Err(e) = jack.lock().transport_stop()
4247 {
4248 self.notify_clients(Err(e)).await;
4249 }
4250 let panic_events = self.note_off_events_for_all_active_tracks();
4251 if let Some(worker) = &self.hw_worker {
4252 if !panic_events.is_empty()
4253 && let Err(e) = worker.tx.send(Message::HWMidiOutEvents(panic_events)).await
4254 {
4255 error!("Error sending stop MIDI panic events {e}");
4256 }
4257 } else {
4258 self.pending_hw_midi_out_events_by_device
4259 .extend(panic_events);
4260 }
4261 self.reset_meters_after_stop();
4262 self.flush_recordings().await;
4263 self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
4264 .await;
4265 }
4266 Action::JumpToEnd => {
4267 self.transport_sample = self.normalize_transport_sample(self.session_end_sample());
4268 self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
4269 .await;
4270 }
4271 Action::Panic => {
4272 let panic_events = self.panic_events_for_all_hw_midi_outputs();
4273 if let Some(worker) = &self.hw_worker {
4274 if !panic_events.is_empty() {
4275 if let Err(e) = worker.tx.send(Message::ClearHWMidiOutEvents).await {
4276 error!("Error clearing HW MIDI queue for panic {e}");
4277 }
4278 self.midi_hub
4279 .lock()
4280 .write_events_blocking(&panic_events, Duration::from_millis(250));
4281 }
4282 } else if !panic_events.is_empty() {
4283 self.pending_hw_midi_out_events_by_device
4284 .extend(panic_events);
4285 }
4286 }
4287 Action::SetClipPlaybackEnabled(enabled) => {
4288 self.clip_playback_enabled = enabled;
4289 for track in self.state.lock().tracks.values() {
4290 track.lock().set_clip_playback_enabled(enabled);
4291 }
4292 }
4293 Action::TransportPosition(sample) => {
4294 self.transport_sample = self.normalize_transport_sample(sample);
4295 #[cfg(unix)]
4296 if let Some(jack) = &self.jack_runtime
4297 && let Err(e) = jack.lock().transport_locate(self.transport_sample)
4298 {
4299 self.notify_clients(Err(e)).await;
4300 }
4301 if self.playing {
4302 self.transport_restart_pending = true;
4303 self.invalidate_track_cycle_state();
4304 self.transport_panic_flush_pending = self.hw_worker.is_some();
4305 self.clear_hw_midi_output_state(true).await;
4306 if !self.awaiting_hwfinished && !self.handling_hwfinished {
4307 if self.hw_worker.is_some() {
4308 self.request_hw_cycle().await;
4309 } else if self.send_tracks().await {
4310 self.transport_restart_pending = false;
4311 self.request_hw_cycle().await;
4312 }
4313 }
4314 }
4315 }
4316 Action::SetLoopEnabled(enabled) => {
4317 self.loop_enabled = enabled && self.loop_range_samples.is_some();
4318 }
4319 Action::SetLoopRange(range) => {
4320 self.loop_range_samples = range.and_then(|(start, end)| {
4321 if end > start {
4322 Some((start, end))
4323 } else {
4324 None
4325 }
4326 });
4327 self.loop_enabled = self.loop_range_samples.is_some();
4328 if self.loop_enabled
4329 && let Some((loop_start, loop_end)) = self.loop_range_samples
4330 && self.transport_sample >= loop_end
4331 {
4332 self.transport_sample = loop_start;
4333 self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
4334 .await;
4335 }
4336 }
4337 Action::SetPunchEnabled(enabled) => {
4338 self.punch_enabled = enabled && self.punch_range_samples.is_some();
4339 }
4340 Action::SetPunchRange(range) => {
4341 self.punch_range_samples = range.and_then(|(start, end)| {
4342 if end > start {
4343 Some((start, end))
4344 } else {
4345 None
4346 }
4347 });
4348 self.punch_enabled = self.punch_range_samples.is_some();
4349 }
4350 Action::SetMetronomeEnabled(enabled) => {
4351 self.metronome_enabled = enabled;
4352 if enabled {
4353 self.ensure_metronome_track().await;
4354 }
4355 if let Some(track) = self.state.lock().tracks.get(Self::METRONOME_TRACK).cloned() {
4356 track.lock().set_metronome_enabled(enabled);
4357 }
4358 }
4359 Action::SetTempo(bpm) => {
4360 self.tempo_bpm = bpm.max(1.0);
4361 }
4362 Action::SetTimeSignature {
4363 numerator,
4364 denominator,
4365 } => {
4366 self.tsig_num = numerator.max(1);
4367 self.tsig_denom = denominator.max(1);
4368 }
4369 Action::SetOscEnabled(enabled) => {
4370 if let Err(err) = self.set_osc_enabled_with(enabled, OscServer::start) {
4371 self.notify_clients(Err(err)).await;
4372 }
4373 }
4374 Action::SetRecordEnabled(enabled) => {
4375 self.record_enabled = enabled;
4376 if !enabled {
4377 if self.awaiting_hwfinished {
4378 self.append_recorded_cycle();
4379 }
4380 self.flush_recordings().await;
4381 } else if self.session_dir.is_none() {
4382 self.notify_clients(Err(
4383 "Recording enabled but session path is not set".to_string()
4384 ))
4385 .await;
4386 }
4387 }
4388 Action::BeginHistoryGroup if self.history_group.is_none() => {
4389 self.history_group = Some(UndoEntry {
4390 forward_actions: vec![],
4391 inverse_actions: vec![],
4392 });
4393 }
4394 Action::EndHistoryGroup => {
4395 if let Some(mut group) = self.history_group.take()
4396 && !group.forward_actions.is_empty()
4397 && !group.inverse_actions.is_empty()
4398 {
4399 let mut add_tracks = Vec::new();
4400 let mut connections = Vec::new();
4401 let mut rest = Vec::new();
4402 for action in group.inverse_actions {
4403 if matches!(action, Action::AddTrack { .. }) {
4404 add_tracks.push(action);
4405 } else if matches!(action, Action::Connect { .. }) {
4406 connections.push(action);
4407 } else {
4408 rest.push(action);
4409 }
4410 }
4411 group.inverse_actions = add_tracks;
4412 group.inverse_actions.extend(rest);
4413 group.inverse_actions.extend(connections);
4414 self.history.record(group);
4415 }
4416 }
4417 Action::SetSessionPath(ref path) => {
4418 self.session_dir = Some(Path::new(path).to_path_buf());
4419 self.ensure_session_subdirs();
4420 #[cfg(all(unix, not(target_os = "macos")))]
4421 let _lv2_dir = self.session_plugins_dir();
4422 for track in self.state.lock().tracks.values() {
4423 track.lock().set_session_base_dir(self.session_dir.clone());
4424 }
4425 }
4426 Action::MarkHistorySavePoint => {
4427 self.history.mark_save_point();
4428 self.notify_clients(Ok(Action::HistoryState {
4429 dirty: self.history.is_dirty(),
4430 }))
4431 .await;
4432 }
4433 Action::ClearHistory => {
4434 self.history.clear();
4435 self.history.mark_save_point();
4436 }
4437 Action::BeginSessionRestore => {
4438 self.history_suspended = true;
4439 self.history.clear();
4440 }
4441 Action::EndSessionRestore => {
4442 self.history.clear();
4443 self.history_suspended = false;
4444 self.preload_track_clips_spawn();
4445 }
4446 Action::Quit => {
4447 self.flush_recordings().await;
4448 self.ready_realtime_workers.clear();
4449 self.ready_refill_workers.clear();
4450 while !self.workers.is_empty() {
4451 let worker = self.workers.remove(0);
4452 worker
4453 .tx
4454 .send(Message::Request(a.clone()))
4455 .await
4456 .expect("Failed sending quit message to worker");
4457 worker
4458 .handle
4459 .await
4460 .expect("Failed waiting for worker to quit");
4461 }
4462
4463 if let Some(worker) = self.hw_worker.take() {
4464 let mut panic_events = self.note_off_events_for_all_active_tracks();
4465 panic_events.extend(self.panic_events_for_all_hw_midi_outputs());
4466 if !panic_events.is_empty() {
4467 if let Err(e) = worker.tx.send(Message::ClearHWMidiOutEvents).await {
4468 error!("Error clearing HW MIDI queue during quit {e}");
4469 }
4470 self.midi_hub
4471 .lock()
4472 .write_events_blocking(&panic_events, Duration::from_millis(250));
4473 }
4474 worker
4475 .tx
4476 .send(Message::Request(a.clone()))
4477 .await
4478 .expect("Failed sending quit message to HW worker");
4479 worker
4480 .handle
4481 .await
4482 .expect("Failed waiting for HW worker to quit");
4483 }
4484 #[cfg(unix)]
4485 {
4486 self.jack_runtime = None;
4487 }
4488 }
4489 Action::AddTrack {
4490 ref name,
4491 audio_ins,
4492 midi_ins,
4493 audio_outs,
4494 midi_outs,
4495 } => {
4496 let tracks = &mut self.state.lock().tracks;
4497 if tracks.contains_key(name) {
4498 self.notify_clients(Err(format!("Track {} already exists", name)))
4499 .await;
4500 return;
4501 }
4502 let maybe_hw = if let Some(oss) = &self.hw_driver {
4503 let hw = oss.lock();
4504 Some((hw.cycle_samples(), hw.sample_rate() as f64))
4505 } else {
4506 #[cfg(unix)]
4507 if let Some(jack) = &self.jack_runtime {
4508 let j = jack.lock();
4509 Some((j.buffer_size, j.sample_rate as f64))
4510 } else {
4511 None
4512 }
4513 #[cfg(not(unix))]
4514 None
4515 };
4516
4517 if let Some((chsamples, sample_rate)) = maybe_hw {
4518 tracks.insert(
4519 name.clone(),
4520 Arc::new(UnsafeMutex::new(Box::new(Track::new(
4521 name.clone(),
4522 audio_ins,
4523 audio_outs,
4524 midi_ins,
4525 midi_outs,
4526 chsamples,
4527 sample_rate,
4528 )))),
4529 );
4530 if let Some(track) = tracks.get(name) {
4531 track.lock().ensure_default_audio_passthrough();
4532 track.lock().ensure_default_midi_passthrough();
4533 track
4534 .lock()
4535 .set_clip_playback_enabled(self.clip_playback_enabled);
4536 track.lock().set_transport_timing(
4537 self.tempo_bpm,
4538 self.tsig_num,
4539 self.tsig_denom,
4540 );
4541 track.lock().set_session_base_dir(self.session_dir.clone());
4542 }
4543 } else {
4544 self.notify_clients(Err(
4545 "Engine needs to open audio device before adding audio track".to_string(),
4546 ))
4547 .await;
4548 }
4549 }
4550 Action::TrackAddAudioInput(ref name) => {
4551 let track = match self.track_handle_or_err(name) {
4552 Ok(track) => track,
4553 Err(e) => {
4554 self.notify_clients(Err(e)).await;
4555 return;
4556 }
4557 };
4558 if let Err(e) = track.lock().add_audio_input() {
4559 self.notify_clients(Err(e)).await;
4560 return;
4561 }
4562 }
4563 Action::TrackAddAudioOutput(ref name) => {
4564 let track = match self.track_handle_or_err(name) {
4565 Ok(track) => track,
4566 Err(e) => {
4567 self.notify_clients(Err(e)).await;
4568 return;
4569 }
4570 };
4571 if let Err(e) = track.lock().add_audio_output() {
4572 self.notify_clients(Err(e)).await;
4573 return;
4574 }
4575 }
4576 Action::TrackRemoveAudioInput(ref name) => {
4577 let track = match self.track_handle_or_err(name) {
4578 Ok(track) => track,
4579 Err(e) => {
4580 self.notify_clients(Err(e)).await;
4581 return;
4582 }
4583 };
4584 if let Err(e) = track.lock().remove_audio_input() {
4585 self.notify_clients(Err(e)).await;
4586 return;
4587 }
4588 }
4589 Action::TrackRemoveAudioOutput(ref name) => {
4590 let track = match self.track_handle_or_err(name) {
4591 Ok(track) => track,
4592 Err(e) => {
4593 self.notify_clients(Err(e)).await;
4594 return;
4595 }
4596 };
4597 let (hw_outputs, track_inputs) = {
4598 let state = self.state.lock();
4599 let hw_outputs = self.all_hw_output_audio_ports();
4600 let track_inputs = state
4601 .tracks
4602 .iter()
4603 .filter(|(track_name, _)| *track_name != name)
4604 .flat_map(|(_, handle)| handle.lock().audio.ins.clone())
4605 .collect::<Vec<_>>();
4606 (hw_outputs, track_inputs)
4607 };
4608 if let Err(e) = track.lock().remove_audio_output(&hw_outputs, &track_inputs) {
4609 self.notify_clients(Err(e)).await;
4610 return;
4611 }
4612 }
4613 Action::RenameTrack {
4614 ref old_name,
4615 ref new_name,
4616 } => {
4617 if self.state.lock().tracks.contains_key(new_name) {
4618 self.notify_clients(Err(format!("Track '{}' already exists", new_name)))
4619 .await;
4620 return;
4621 }
4622
4623 let Some(track) = self.state.lock().tracks.remove(old_name) else {
4624 self.notify_clients(Err(format!("Track '{}' not found", old_name)))
4625 .await;
4626 return;
4627 };
4628
4629 track.lock().name = new_name.clone();
4630 self.state.lock().tracks.insert(new_name.clone(), track);
4631 for other in self.state.lock().tracks.values() {
4632 let other = other.lock();
4633 if other.vca_master.as_deref() == Some(old_name.as_str()) {
4634 other.set_vca_master(Some(new_name.clone()));
4635 }
4636 }
4637
4638 if let Some(recording) = self.audio_recordings.remove(old_name) {
4639 self.audio_recordings.insert(new_name.clone(), recording);
4640 }
4641 if let Some(recording) = self.midi_recordings.remove(old_name) {
4642 self.midi_recordings.insert(new_name.clone(), recording);
4643 }
4644
4645 for route in &mut self.midi_hw_in_routes {
4646 if route.to_track == *old_name {
4647 route.to_track = new_name.clone();
4648 }
4649 }
4650 for route in &mut self.midi_hw_out_routes {
4651 if route.from_track == *old_name {
4652 route.from_track = new_name.clone();
4653 }
4654 }
4655 if let Some((armed_track, target, device)) = self.pending_midi_learn.clone()
4656 && armed_track == *old_name
4657 {
4658 self.pending_midi_learn = Some((new_name.clone(), target, device));
4659 }
4660
4661 self.notify_clients(Ok(Action::RenameTrack {
4662 old_name: old_name.clone(),
4663 new_name: new_name.clone(),
4664 }))
4665 .await;
4666 }
4667 Action::RemoveTrack(ref name) => {
4668 self.state.lock().tracks.remove(name);
4669 self.audio_recordings.remove(name);
4670 self.midi_recordings.remove(name);
4671 self.midi_hw_in_routes.retain(|r| r.to_track != *name);
4672 self.midi_hw_out_routes.retain(|r| r.from_track != *name);
4673 if self
4674 .pending_midi_learn
4675 .as_ref()
4676 .is_some_and(|(track_name, _, _)| track_name == name)
4677 {
4678 self.pending_midi_learn = None;
4679 }
4680 for track in self.state.lock().tracks.values() {
4681 let track = track.lock();
4682 if track.vca_master.as_deref() == Some(name.as_str()) {
4683 track.set_vca_master(None);
4684 }
4685 }
4686 }
4687 Action::TrackLevel(ref name, level) => {
4688 if name == "hw:out" {
4689 self.hw_out_level_db = level;
4690 } else if let Some(track) = self.state.lock().tracks.get(name) {
4691 let previous = track.lock().level();
4692 track.lock().set_level(level);
4693 let delta = level - previous;
4694 if delta.abs() > f32::EPSILON {
4695 for follower_name in self.vca_followers(name) {
4696 if let Some(follower) = self.state.lock().tracks.get(&follower_name) {
4697 let next = (follower.lock().level() + delta).clamp(-90.0, 20.0);
4698 follower.lock().set_level(next);
4699 self.notify_clients(Ok(Action::TrackLevel(
4700 follower_name.clone(),
4701 next,
4702 )))
4703 .await;
4704 }
4705 }
4706 }
4707 }
4708 }
4709 Action::TrackBalance(ref name, balance) => {
4710 if name == "hw:out" {
4711 self.hw_out_balance = balance.clamp(-1.0, 1.0);
4712 } else if let Some(track) = self.state.lock().tracks.get(name) {
4713 track.lock().set_balance(balance);
4714 }
4715 }
4716 Action::TrackAutomationLevel(ref name, level) => {
4717 if let Some(track) = self.state.lock().tracks.get(name) {
4718 let previous = track.lock().level();
4719 track.lock().set_level(level);
4720 let delta = level - previous;
4721 if delta.abs() > f32::EPSILON {
4722 for follower_name in self.vca_followers(name) {
4723 if let Some(follower) = self.state.lock().tracks.get(&follower_name) {
4724 let next = (follower.lock().level() + delta).clamp(-90.0, 20.0);
4725 follower.lock().set_level(next);
4726 self.notify_clients(Ok(Action::TrackAutomationLevel(
4727 follower_name.clone(),
4728 next,
4729 )))
4730 .await;
4731 }
4732 }
4733 }
4734 }
4735 }
4736 Action::TrackAutomationBalance(ref name, balance) => {
4737 if let Some(track) = self.state.lock().tracks.get(name) {
4738 track.lock().set_balance(balance);
4739 }
4740 }
4741 Action::TrackAutomationMute(ref name, muted) => {
4742 if let Some(track) = self.state.lock().tracks.get(name) {
4743 track.lock().set_muted(muted);
4744 for follower_name in self.vca_followers(name) {
4745 if let Some(follower) = self.state.lock().tracks.get(&follower_name) {
4746 follower.lock().set_muted(muted);
4747 self.notify_clients(Ok(Action::TrackAutomationMute(
4748 follower_name.clone(),
4749 muted,
4750 )))
4751 .await;
4752 }
4753 }
4754 }
4755 }
4756 Action::RequestMeterSnapshot => {
4757 self.notify_clients(Ok(Action::MeterSnapshot {
4758 hw_out_db: self.latest_hw_out_meter_db.clone(),
4759 track_meters: self.latest_track_meter_snapshot.clone(),
4760 }))
4761 .await;
4762 return;
4763 }
4764 Action::TrackMeters { .. } => {}
4765 Action::MeterSnapshot { .. } => {}
4766 Action::TrackToggleArm(ref name) => {
4767 if self.reject_if_track_frozen(name, "arming/disarming").await {
4768 return;
4769 }
4770 if let Some(track) = self.state.lock().tracks.get(name).cloned() {
4771 track.lock().arm();
4772 if !track.lock().armed && self.audio_recordings.contains_key(name) {
4773 self.flush_track_recording(name).await;
4774 }
4775 }
4776 }
4777 Action::TrackToggleMute(ref name) => {
4778 if name == "hw:out" {
4779 self.hw_out_muted = !self.hw_out_muted;
4780 } else if let Some(track) = self.state.lock().tracks.get(name) {
4781 track.lock().mute();
4782 let muted = track.lock().muted;
4783 for follower_name in self.vca_followers(name) {
4784 if let Some(follower) = self.state.lock().tracks.get(&follower_name)
4785 && follower.lock().muted != muted
4786 {
4787 follower.lock().set_muted(muted);
4788 self.notify_clients(Ok(Action::TrackToggleMute(follower_name.clone())))
4789 .await;
4790 }
4791 }
4792 }
4793 }
4794 Action::TrackTogglePhase(ref name) => {
4795 if let Some(track) = self.state.lock().tracks.get(name) {
4796 track.lock().invert_phase();
4797 }
4798 }
4799 Action::TrackToggleSolo(ref name) => {
4800 if name == "hw:out" {
4801 return;
4802 }
4803 if let Some(track) = self.state.lock().tracks.get(name) {
4804 track.lock().solo();
4805 let soloed = track.lock().soloed;
4806 for follower_name in self.vca_followers(name) {
4807 if let Some(follower) = self.state.lock().tracks.get(&follower_name)
4808 && follower.lock().soloed != soloed
4809 {
4810 follower.lock().solo();
4811 self.notify_clients(Ok(Action::TrackToggleSolo(follower_name.clone())))
4812 .await;
4813 }
4814 }
4815 }
4816 }
4817 Action::TrackToggleMaster(ref name) => {
4818 if let Some(track) = self.state.lock().tracks.get(name) {
4819 let blocked = {
4820 let t = track.lock();
4821 t.vca_master.is_some() || !self.vca_followers(name).is_empty()
4822 };
4823 if blocked {
4824 self.notify_clients(Err(format!(
4825 "Track '{}' cannot be promoted to Master while part of a VCA group",
4826 name
4827 )))
4828 .await;
4829 return;
4830 }
4831 track.lock().toggle_master();
4832 }
4833 }
4834 Action::TrackToggleInputMonitor(ref name) => {
4835 if let Some(track) = self.state.lock().tracks.get(name) {
4836 track.lock().toggle_input_monitor();
4837 }
4838 }
4839 Action::TrackToggleDiskMonitor(ref name) => {
4840 if let Some(track) = self.state.lock().tracks.get(name) {
4841 track.lock().toggle_disk_monitor();
4842 }
4843 }
4844 Action::TrackSetColor {
4845 ref track_name,
4846 color,
4847 } => {
4848 if let Some(track) = self.state.lock().tracks.get(track_name) {
4849 track.lock().color = color;
4850 }
4851 }
4852 Action::TrackArmMidiLearn {
4853 ref track_name,
4854 target,
4855 } => {
4856 if let Err(e) = self.track_handle_or_err(track_name) {
4857 self.notify_clients(Err(e)).await;
4858 return;
4859 }
4860 self.pending_midi_learn = Some((track_name.clone(), target, None));
4861 }
4862 Action::GlobalArmMidiLearn { target } => {
4863 self.pending_global_midi_learn = Some(target);
4864 }
4865 Action::TrackSetMidiLearnBinding {
4866 ref track_name,
4867 target,
4868 ref binding,
4869 } => {
4870 if let Some(binding) = binding.as_ref() {
4871 let conflicts = self.midi_learn_slot_conflicts(
4872 binding,
4873 Some(MidiLearnSlot::Track(track_name.clone(), target)),
4874 );
4875 if !conflicts.is_empty() {
4876 self.notify_clients(Err(format!(
4877 "MIDI learn conflict for '{}' {:?}: {}",
4878 track_name,
4879 target,
4880 conflicts.join(", ")
4881 )))
4882 .await;
4883 return;
4884 }
4885 }
4886 let track = match self.track_handle_or_err(track_name) {
4887 Ok(track) => track,
4888 Err(e) => {
4889 self.notify_clients(Err(e)).await;
4890 return;
4891 }
4892 };
4893 match target {
4894 crate::message::TrackMidiLearnTarget::Volume => {
4895 track.lock().midi_learn_volume = binding.clone();
4896 }
4897 crate::message::TrackMidiLearnTarget::Balance => {
4898 track.lock().midi_learn_balance = binding.clone();
4899 }
4900 crate::message::TrackMidiLearnTarget::Mute => {
4901 track.lock().midi_learn_mute = binding.clone();
4902 }
4903 crate::message::TrackMidiLearnTarget::Solo => {
4904 track.lock().midi_learn_solo = binding.clone();
4905 }
4906 crate::message::TrackMidiLearnTarget::Arm => {
4907 track.lock().midi_learn_arm = binding.clone();
4908 }
4909 crate::message::TrackMidiLearnTarget::InputMonitor => {
4910 track.lock().midi_learn_input_monitor = binding.clone();
4911 }
4912 crate::message::TrackMidiLearnTarget::DiskMonitor => {
4913 track.lock().midi_learn_disk_monitor = binding.clone();
4914 }
4915 }
4916 }
4917 Action::SetGlobalMidiLearnBinding {
4918 target,
4919 ref binding,
4920 } => {
4921 if let Some(binding) = binding.as_ref() {
4922 let conflicts = self
4923 .midi_learn_slot_conflicts(binding, Some(MidiLearnSlot::Global(target)));
4924 if !conflicts.is_empty() {
4925 self.notify_clients(Err(format!(
4926 "Global MIDI learn conflict for {:?}: {}",
4927 target,
4928 conflicts.join(", ")
4929 )))
4930 .await;
4931 return;
4932 }
4933 }
4934 match target {
4935 crate::message::GlobalMidiLearnTarget::PlayPause => {
4936 self.global_midi_learn_play_pause = binding.clone();
4937 }
4938 crate::message::GlobalMidiLearnTarget::Stop => {
4939 self.global_midi_learn_stop = binding.clone();
4940 }
4941 crate::message::GlobalMidiLearnTarget::RecordToggle => {
4942 self.global_midi_learn_record_toggle = binding.clone();
4943 }
4944 }
4945 }
4946 Action::TrackSetVcaMaster {
4947 ref track_name,
4948 ref master_track,
4949 } => {
4950 let track = match self.track_handle_or_err(track_name) {
4951 Ok(track) => track,
4952 Err(e) => {
4953 self.notify_clients(Err(e)).await;
4954 return;
4955 }
4956 };
4957 if track.lock().is_master {
4958 self.notify_clients(Err(format!(
4959 "Master track '{}' cannot be part of a VCA group",
4960 track_name
4961 )))
4962 .await;
4963 return;
4964 }
4965 if let Some(master_name) = master_track
4966 && master_name == track_name
4967 {
4968 self.notify_clients(Err("Track cannot be its own VCA master".to_string()))
4969 .await;
4970 return;
4971 }
4972 if let Some(master_name) = master_track
4973 && let Some(master) = self.state.lock().tracks.get(master_name)
4974 && master.lock().is_master
4975 {
4976 self.notify_clients(Err(format!(
4977 "Track '{}' cannot be grouped to Master track '{}'",
4978 track_name, master_name
4979 )))
4980 .await;
4981 return;
4982 }
4983 track.lock().set_vca_master(master_track.clone());
4984 }
4985 Action::TrackSetMidiLaneChannel {
4986 ref track_name,
4987 lane,
4988 channel,
4989 } => {
4990 let track = match self.track_handle_or_err(track_name) {
4991 Ok(track) => track,
4992 Err(e) => {
4993 self.notify_clients(Err(e)).await;
4994 return;
4995 }
4996 };
4997 track.lock().set_midi_lane_channel(lane, channel);
4998 }
4999 Action::TrackSetFrozen {
5000 ref track_name,
5001 frozen,
5002 } => {
5003 let track = match self.track_handle_or_err(track_name) {
5004 Ok(track) => track,
5005 Err(e) => {
5006 self.notify_clients(Err(e)).await;
5007 return;
5008 }
5009 };
5010 track.lock().set_frozen(frozen);
5011 }
5012 Action::TrackOfflineBounce {
5013 track_name,
5014 output_path,
5015 start_sample,
5016 length_samples,
5017 automation_lanes,
5018 apply_fader,
5019 } => {
5020 if self.offline_bounce_jobs.contains_key(&track_name) {
5021 self.notify_clients(Err(format!(
5022 "Offline bounce for track '{}' is already in progress",
5023 track_name
5024 )))
5025 .await;
5026 return;
5027 }
5028 if let Err(e) = self.track_handle_or_err(&track_name) {
5029 self.notify_clients(Err(e)).await;
5030 return;
5031 }
5032 if length_samples == 0 {
5033 self.notify_clients(Err(format!(
5034 "Track '{}' has no renderable content for offline bounce",
5035 track_name
5036 )))
5037 .await;
5038 return;
5039 }
5040 let Some(worker_index) = self.take_ready_worker_index(WorkerClass::Refill) else {
5041 self.pending_requests
5042 .push_front(Action::TrackOfflineBounce {
5043 track_name,
5044 output_path,
5045 start_sample,
5046 length_samples,
5047 automation_lanes,
5048 apply_fader,
5049 });
5050 return;
5051 };
5052 let cancel = Arc::new(AtomicBool::new(false));
5053 self.offline_bounce_jobs.insert(
5054 track_name.clone(),
5055 OfflineBounceJob {
5056 cancel: cancel.clone(),
5057 },
5058 );
5059 let track_name_clone = track_name.clone();
5060 let worker = &self.workers[worker_index];
5061 let job = crate::message::OfflineBounceWork {
5062 state: self.state.clone(),
5063 track_name,
5064 output_path,
5065 start_sample,
5066 length_samples,
5067 tempo_bpm: self.tempo_bpm,
5068 tsig_num: self.tsig_num,
5069 tsig_denom: self.tsig_denom,
5070 automation_lanes,
5071 cancel,
5072 apply_fader,
5073 };
5074 if let Err(e) = worker.tx.send(Message::ProcessOfflineBounce(job)).await {
5075 self.offline_bounce_jobs.remove(&track_name_clone);
5076 self.notify_clients(Err(format!("Failed to schedule offline bounce: {e}")))
5077 .await;
5078 }
5079 return;
5080 }
5081 Action::TrackOfflineBounceCancel { .. } => {}
5082 Action::TrackOfflineBounceCancelAll => {}
5083 Action::TrackOfflineBounceCanceled { .. } => {}
5084 Action::TrackOfflineBounceProgress { .. } => {}
5085 Action::PianoKey {
5086 ref track_name,
5087 note,
5088 velocity,
5089 on,
5090 } => {
5091 if let Some(track) = self.state.lock().tracks.get(track_name) {
5092 let status = if on { 0x90 } else { 0x80 };
5093 let event = MidiEvent::new(0, vec![status, note.min(127), velocity.min(127)]);
5094 track.lock().push_hw_midi_events(&[event]);
5095 }
5096 }
5097 Action::ModifyMidiNotes { .. }
5098 | Action::ModifyMidiControllers { .. }
5099 | Action::DeleteMidiControllers { .. }
5100 | Action::InsertMidiControllers { .. }
5101 | Action::DeleteMidiNotes { .. }
5102 | Action::InsertMidiNotes { .. } => {
5103 if let Err(e) = self.apply_midi_edit_action(&action_to_process) {
5104 self.notify_clients(Err(e)).await;
5105 return;
5106 }
5107 }
5108 Action::SetMidiSysExEvents { .. } => {
5109 if let Err(e) = self.apply_midi_edit_action(&action_to_process) {
5110 self.notify_clients(Err(e)).await;
5111 return;
5112 }
5113 }
5114 Action::TrackClearDefaultPassthrough { ref track_name } => {
5115 if self
5116 .reject_if_track_frozen(track_name, "plugin graph editing")
5117 .await
5118 {
5119 return;
5120 }
5121 let track = match self.track_handle_or_err(track_name) {
5122 Ok(track) => track,
5123 Err(e) => {
5124 self.notify_clients(Err(e)).await;
5125 return;
5126 }
5127 };
5128 track.lock().clear_default_passthrough();
5129 }
5130 #[cfg(all(unix, not(target_os = "macos")))]
5131 Action::ClipSetLv2PluginState { ref track_name, .. } => {
5132 self.notify_clients(Err(format!(
5133 "Track '{}': clip LV2 plugin state changes are not supported",
5134 track_name
5135 )))
5136 .await;
5137 }
5138 Action::TrackGetClapNoteNames { ref track_name } => {
5139 let track = match self.track_handle_or_err(track_name) {
5140 Ok(track) => track,
5141 Err(e) => {
5142 self.notify_clients(Err(e)).await;
5143 return;
5144 }
5145 };
5146 let note_names = track.lock().get_clap_note_names();
5147 self.notify_clients(Ok(Action::TrackClapNoteNames {
5148 track_name: track_name.clone(),
5149 note_names,
5150 }))
5151 .await;
5152 }
5153 Action::TrackGetPluginGraph { ref track_name } => {
5154 let track = match self.track_handle_or_err(track_name) {
5155 Ok(track) => track,
5156 Err(e) => {
5157 self.notify_clients(Err(e)).await;
5158 return;
5159 }
5160 };
5161 let (plugins, connections) = {
5162 let track = track.lock();
5163 (
5164 track.plugin_graph_plugins(),
5165 track.plugin_graph_connections(),
5166 )
5167 };
5168 self.notify_clients(Ok(Action::TrackPluginGraph {
5169 track_name: track_name.clone(),
5170 plugins,
5171 connections,
5172 }))
5173 .await;
5174 return;
5175 }
5176 Action::TrackPluginGraph { .. } => {}
5177 Action::TrackConnectPluginAudio {
5178 ref track_name,
5179 ref from_node,
5180 from_port,
5181 ref to_node,
5182 to_port,
5183 } => {
5184 if self
5185 .reject_if_track_frozen(track_name, "plugin routing changes")
5186 .await
5187 {
5188 return;
5189 }
5190 let track = match self.track_handle_or_err(track_name) {
5191 Ok(track) => track,
5192 Err(e) => {
5193 self.notify_clients(Err(e)).await;
5194 return;
5195 }
5196 };
5197 if let Err(e) = track.lock().connect_plugin_audio(
5198 from_node.clone(),
5199 from_port,
5200 to_node.clone(),
5201 to_port,
5202 ) {
5203 self.notify_clients(Err(e)).await;
5204 return;
5205 }
5206 }
5207 Action::TrackConnectPluginMidi {
5208 ref track_name,
5209 ref from_node,
5210 from_port,
5211 ref to_node,
5212 to_port,
5213 } => {
5214 if self
5215 .reject_if_track_frozen(track_name, "plugin routing changes")
5216 .await
5217 {
5218 return;
5219 }
5220 let track = match self.track_handle_or_err(track_name) {
5221 Ok(track) => track,
5222 Err(e) => {
5223 self.notify_clients(Err(e)).await;
5224 return;
5225 }
5226 };
5227 if let Err(e) = track.lock().connect_plugin_midi(
5228 from_node.clone(),
5229 from_port,
5230 to_node.clone(),
5231 to_port,
5232 ) {
5233 self.notify_clients(Err(e)).await;
5234 return;
5235 }
5236 }
5237 Action::TrackDisconnectPluginAudio {
5238 ref track_name,
5239 ref from_node,
5240 from_port,
5241 ref to_node,
5242 to_port,
5243 } => {
5244 if self
5245 .reject_if_track_frozen(track_name, "plugin routing changes")
5246 .await
5247 {
5248 return;
5249 }
5250 let track = match self.track_handle_or_err(track_name) {
5251 Ok(track) => track,
5252 Err(e) => {
5253 self.notify_clients(Err(e)).await;
5254 return;
5255 }
5256 };
5257 if let Err(e) = track.lock().disconnect_plugin_audio(
5258 from_node.clone(),
5259 from_port,
5260 to_node.clone(),
5261 to_port,
5262 ) {
5263 self.notify_clients(Err(e)).await;
5264 return;
5265 }
5266 }
5267 Action::TrackDisconnectPluginMidi {
5268 ref track_name,
5269 ref from_node,
5270 from_port,
5271 ref to_node,
5272 to_port,
5273 } => {
5274 if self
5275 .reject_if_track_frozen(track_name, "plugin routing changes")
5276 .await
5277 {
5278 return;
5279 }
5280 let track = match self.track_handle_or_err(track_name) {
5281 Ok(track) => track,
5282 Err(e) => {
5283 self.notify_clients(Err(e)).await;
5284 return;
5285 }
5286 };
5287 if let Err(e) = track.lock().disconnect_plugin_midi(
5288 from_node.clone(),
5289 from_port,
5290 to_node.clone(),
5291 to_port,
5292 ) {
5293 self.notify_clients(Err(e)).await;
5294 return;
5295 }
5296 }
5297 #[cfg(all(unix, not(target_os = "macos")))]
5298 Action::ListLv2Plugins => {
5299 match crate::plugins::scan_plugins::<crate::plugins::types::Lv2PluginInfo>("lv2") {
5300 Ok(plugins) => {
5301 self.notify_clients(Ok(Action::Lv2Plugins(plugins))).await;
5302 }
5303 Err(e) => {
5304 self.notify_clients(Err(e)).await;
5305 }
5306 }
5307 return;
5308 }
5309 #[cfg(all(unix, not(target_os = "macos")))]
5310 Action::Lv2Plugins(_) => {}
5311 Action::ListVst3Plugins => {
5312 match crate::plugins::scan_plugins::<crate::plugins::types::Vst3PluginInfo>("vst3")
5313 {
5314 Ok(plugins) => {
5315 self.notify_clients(Ok(Action::Vst3Plugins(plugins))).await;
5316 }
5317 Err(e) => {
5318 self.notify_clients(Err(e)).await;
5319 }
5320 }
5321 return;
5322 }
5323 Action::Vst3Plugins(_) => {}
5324 Action::ListClapPlugins => {
5325 match crate::plugins::scan_plugins::<crate::plugins::types::ClapPluginInfo>("clap")
5326 {
5327 Ok(plugins) => {
5328 self.notify_clients(Ok(Action::ClapPlugins(plugins))).await;
5329 }
5330 Err(e) => {
5331 self.notify_clients(Err(e)).await;
5332 }
5333 }
5334 return;
5335 }
5336 Action::ListClapPluginsWithCapabilities => {
5337 match crate::plugins::scan_plugins::<crate::plugins::types::ClapPluginInfo>("clap")
5338 {
5339 Ok(plugins) => {
5340 self.notify_clients(Ok(Action::ClapPlugins(plugins))).await;
5341 }
5342 Err(e) => {
5343 self.notify_clients(Err(e)).await;
5344 }
5345 }
5346 return;
5347 }
5348 Action::ClapPlugins(_) => {}
5349 Action::TrackLoadClapPlugin {
5350 ref track_name,
5351 ref plugin_path,
5352 instance_id,
5353 } => {
5354 if self
5355 .reject_if_track_frozen(track_name, "CLAP plugin loading")
5356 .await
5357 {
5358 return;
5359 }
5360 let track = match self.track_handle_or_err(track_name) {
5361 Ok(track) => track,
5362 Err(e) => {
5363 self.notify_clients(Err(e)).await;
5364 return;
5365 }
5366 };
5367 let track = track.lock();
5368 if track.audio.processing {
5369 self.notify_clients(Err(format!(
5370 "Track '{}' is currently processing audio; stop playback before loading CLAP plugins",
5371 track_name
5372 )))
5373 .await;
5374 return;
5375 }
5376 if let Err(e) = track.load_clap_plugin(plugin_path, instance_id) {
5377 self.notify_clients(Err(e)).await;
5378 return;
5379 }
5380 }
5381 Action::TrackUnloadClapPlugin {
5382 ref track_name,
5383 ref plugin_path,
5384 } => {
5385 if self
5386 .reject_if_track_frozen(track_name, "CLAP plugin unloading")
5387 .await
5388 {
5389 return;
5390 }
5391 let track = match self.track_handle_or_err(track_name) {
5392 Ok(track) => track,
5393 Err(e) => {
5394 self.notify_clients(Err(e)).await;
5395 return;
5396 }
5397 };
5398 let track = track.lock();
5399 if track.audio.processing {
5400 self.notify_clients(Err(format!(
5401 "Track '{}' is currently processing audio; stop playback before unloading CLAP plugins",
5402 track_name
5403 )))
5404 .await;
5405 return;
5406 }
5407 if let Err(e) = track.unload_clap_plugin(plugin_path) {
5408 self.notify_clients(Err(e)).await;
5409 return;
5410 }
5411 }
5412 Action::TrackUnloadClapPluginInstance {
5413 ref track_name,
5414 instance_id,
5415 } => {
5416 if self
5417 .reject_if_track_frozen(track_name, "CLAP plugin unloading")
5418 .await
5419 {
5420 return;
5421 }
5422 let track = match self.track_handle_or_err(track_name) {
5423 Ok(track) => track,
5424 Err(e) => {
5425 self.notify_clients(Err(e)).await;
5426 return;
5427 }
5428 };
5429 let track = track.lock();
5430 if track.audio.processing {
5431 self.notify_clients(Err(format!(
5432 "Track '{}' is currently processing audio; stop playback before unloading CLAP plugins",
5433 track_name
5434 )))
5435 .await;
5436 return;
5437 }
5438 if let Err(e) = track.unload_clap_plugin_instance(instance_id) {
5439 self.notify_clients(Err(e)).await;
5440 return;
5441 }
5442 }
5443 Action::TrackShowClapGui {
5444 ref track_name,
5445 instance_id,
5446 } => {
5447 let track = match self.track_handle_or_err(track_name) {
5448 Ok(track) => track,
5449 Err(e) => {
5450 self.notify_clients(Err(e)).await;
5451 return;
5452 }
5453 };
5454 if let Err(e) = track.lock().show_clap_gui(instance_id) {
5455 self.notify_clients(Err(e)).await;
5456 return;
5457 }
5458 }
5459 Action::TrackLoadVst3Plugin {
5460 ref track_name,
5461 ref plugin_path,
5462 instance_id,
5463 } => {
5464 if self
5465 .reject_if_track_frozen(track_name, "VST3 plugin loading")
5466 .await
5467 {
5468 return;
5469 }
5470 let track = match self.track_handle_or_err(track_name) {
5471 Ok(track) => track,
5472 Err(e) => {
5473 self.notify_clients(Err(e)).await;
5474 return;
5475 }
5476 };
5477 let track = track.lock();
5478 if track.audio.processing {
5479 self.notify_clients(Err(format!(
5480 "Track '{}' is currently processing audio; stop playback before loading VST3 plugins",
5481 track_name
5482 )))
5483 .await;
5484 return;
5485 }
5486 if let Err(e) = track.load_vst3_plugin(plugin_path, instance_id) {
5487 self.notify_clients(Err(e)).await;
5488 return;
5489 }
5490 }
5491 Action::TrackUnloadVst3Plugin {
5492 ref track_name,
5493 ref plugin_path,
5494 } => {
5495 if self
5496 .reject_if_track_frozen(track_name, "VST3 plugin unloading")
5497 .await
5498 {
5499 return;
5500 }
5501 let track = match self.track_handle_or_err(track_name) {
5502 Ok(track) => track,
5503 Err(e) => {
5504 self.notify_clients(Err(e)).await;
5505 return;
5506 }
5507 };
5508 let track = track.lock();
5509 if track.audio.processing {
5510 self.notify_clients(Err(format!(
5511 "Track '{}' is currently processing audio; stop playback before unloading VST3 plugins",
5512 track_name
5513 )))
5514 .await;
5515 return;
5516 }
5517 if let Err(e) = track.unload_vst3_plugin(plugin_path) {
5518 self.notify_clients(Err(e)).await;
5519 return;
5520 }
5521 }
5522 Action::TrackUnloadVst3PluginInstance {
5523 ref track_name,
5524 instance_id,
5525 } => {
5526 if self
5527 .reject_if_track_frozen(track_name, "VST3 plugin unloading")
5528 .await
5529 {
5530 return;
5531 }
5532 let track = match self.track_handle_or_err(track_name) {
5533 Ok(track) => track,
5534 Err(e) => {
5535 self.notify_clients(Err(e)).await;
5536 return;
5537 }
5538 };
5539 let track = track.lock();
5540 if track.audio.processing {
5541 self.notify_clients(Err(format!(
5542 "Track '{}' is currently processing audio; stop playback before unloading VST3 plugins",
5543 track_name
5544 )))
5545 .await;
5546 return;
5547 }
5548 if let Err(e) = track.unload_vst3_plugin_instance(instance_id) {
5549 self.notify_clients(Err(e)).await;
5550 return;
5551 }
5552 }
5553 Action::TrackShowVst3Gui {
5554 ref track_name,
5555 instance_id,
5556 } => {
5557 let track = match self.track_handle_or_err(track_name) {
5558 Ok(track) => track,
5559 Err(e) => {
5560 self.notify_clients(Err(e)).await;
5561 return;
5562 }
5563 };
5564 if let Err(e) = track.lock().show_vst3_gui(instance_id) {
5565 self.notify_clients(Err(e)).await;
5566 return;
5567 }
5568 }
5569 #[cfg(all(unix, not(target_os = "macos")))]
5570 Action::TrackLoadLv2Plugin {
5571 ref track_name,
5572 ref plugin_uri,
5573 instance_id,
5574 } => {
5575 if self
5576 .reject_if_track_frozen(track_name, "LV2 plugin loading")
5577 .await
5578 {
5579 return;
5580 }
5581 let track = match self.track_handle_or_err(track_name) {
5582 Ok(track) => track,
5583 Err(e) => {
5584 self.notify_clients(Err(e)).await;
5585 return;
5586 }
5587 };
5588 let track = track.lock();
5589 if track.audio.processing {
5590 self.notify_clients(Err(format!(
5591 "Track '{}' is currently processing audio; stop playback before loading LV2 plugins",
5592 track_name
5593 )))
5594 .await;
5595 return;
5596 }
5597 if let Err(e) = track.load_lv2_plugin(plugin_uri, instance_id) {
5598 self.notify_clients(Err(e)).await;
5599 return;
5600 }
5601 }
5602 #[cfg(all(unix, not(target_os = "macos")))]
5603 Action::TrackUnloadLv2Plugin {
5604 ref track_name,
5605 ref plugin_uri,
5606 } => {
5607 if self
5608 .reject_if_track_frozen(track_name, "LV2 plugin unloading")
5609 .await
5610 {
5611 return;
5612 }
5613 let track = match self.track_handle_or_err(track_name) {
5614 Ok(track) => track,
5615 Err(e) => {
5616 self.notify_clients(Err(e)).await;
5617 return;
5618 }
5619 };
5620 let track = track.lock();
5621 if track.audio.processing {
5622 self.notify_clients(Err(format!(
5623 "Track '{}' is currently processing audio; stop playback before unloading LV2 plugins",
5624 track_name
5625 )))
5626 .await;
5627 return;
5628 }
5629 if let Err(e) = track.unload_lv2_plugin(plugin_uri) {
5630 self.notify_clients(Err(e)).await;
5631 return;
5632 }
5633 }
5634 #[cfg(all(unix, not(target_os = "macos")))]
5635 Action::TrackUnloadLv2PluginInstance {
5636 ref track_name,
5637 instance_id,
5638 } => {
5639 if self
5640 .reject_if_track_frozen(track_name, "LV2 plugin unloading")
5641 .await
5642 {
5643 return;
5644 }
5645 let track = match self.track_handle_or_err(track_name) {
5646 Ok(track) => track,
5647 Err(e) => {
5648 self.notify_clients(Err(e)).await;
5649 return;
5650 }
5651 };
5652 let track = track.lock();
5653 if track.audio.processing {
5654 self.notify_clients(Err(format!(
5655 "Track '{}' is currently processing audio; stop playback before unloading LV2 plugins",
5656 track_name
5657 )))
5658 .await;
5659 return;
5660 }
5661 if let Err(e) = track.unload_lv2_plugin_instance(instance_id) {
5662 self.notify_clients(Err(e)).await;
5663 return;
5664 }
5665 }
5666 #[cfg(all(unix, not(target_os = "macos")))]
5667 Action::TrackShowLv2Gui {
5668 ref track_name,
5669 instance_id,
5670 } => {
5671 let track = match self.track_handle_or_err(track_name) {
5672 Ok(track) => track,
5673 Err(e) => {
5674 self.notify_clients(Err(e)).await;
5675 return;
5676 }
5677 };
5678 if let Err(e) = track.lock().show_lv2_gui(instance_id) {
5679 self.notify_clients(Err(e)).await;
5680 return;
5681 }
5682 }
5683 Action::TrackSetClapParameter {
5684 ref track_name,
5685 instance_id,
5686 param_id,
5687 value,
5688 } => {
5689 if self
5690 .reject_if_track_frozen(track_name, "CLAP parameter changes")
5691 .await
5692 {
5693 return;
5694 }
5695 match self.track_handle_or_err(track_name) {
5696 Ok(track) => {
5697 if let Err(e) =
5698 track
5699 .lock()
5700 .set_clap_parameter(instance_id, param_id, value)
5701 {
5702 self.notify_clients(Err(e)).await;
5703 return;
5704 }
5705 self.notify_clients(Ok(a.clone())).await;
5706 }
5707 Err(e) => {
5708 self.notify_clients(Err(e)).await;
5709 }
5710 }
5711 }
5712 Action::ClipSetClapParameter {
5713 ref track_name,
5714 clip_idx,
5715 instance_id,
5716 param_id,
5717 value,
5718 } => {
5719 if self
5720 .reject_if_track_frozen(track_name, "CLAP parameter changes")
5721 .await
5722 {
5723 return;
5724 }
5725 match self.track_handle_or_err(track_name) {
5726 Ok(track) => {
5727 if let Err(e) = track.lock().clip_set_clap_parameter(
5728 clip_idx,
5729 instance_id,
5730 param_id,
5731 value,
5732 ) {
5733 self.notify_clients(Err(e)).await;
5734 return;
5735 }
5736 self.notify_clients(Ok(a.clone())).await;
5737 }
5738 Err(e) => {
5739 self.notify_clients(Err(e)).await;
5740 }
5741 }
5742 }
5743 Action::TrackSetClapParameterAt {
5744 ref track_name,
5745 instance_id,
5746 param_id,
5747 value,
5748 frame,
5749 } => {
5750 if self
5751 .reject_if_track_frozen(track_name, "CLAP parameter changes")
5752 .await
5753 {
5754 return;
5755 }
5756 match self.track_handle_or_err(track_name) {
5757 Ok(track) => {
5758 if let Err(e) =
5759 track
5760 .lock()
5761 .set_clap_parameter_at(instance_id, param_id, value, frame)
5762 {
5763 self.notify_clients(Err(e)).await;
5764 return;
5765 }
5766 self.notify_clients(Ok(a.clone())).await;
5767 }
5768 Err(e) => {
5769 self.notify_clients(Err(e)).await;
5770 }
5771 }
5772 }
5773 Action::TrackBeginClapParameterEdit {
5774 ref track_name,
5775 instance_id,
5776 param_id,
5777 frame,
5778 } => {
5779 if self
5780 .reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
5781 .await
5782 {
5783 return;
5784 }
5785 match self.track_handle_or_err(track_name) {
5786 Ok(track) => {
5787 if let Err(e) =
5788 track
5789 .lock()
5790 .begin_clap_parameter_edit(instance_id, param_id, frame)
5791 {
5792 self.notify_clients(Err(e)).await;
5793 return;
5794 }
5795 self.notify_clients(Ok(a.clone())).await;
5796 }
5797 Err(e) => {
5798 self.notify_clients(Err(e)).await;
5799 }
5800 }
5801 }
5802 Action::TrackEndClapParameterEdit {
5803 ref track_name,
5804 instance_id,
5805 param_id,
5806 frame,
5807 } => {
5808 if self
5809 .reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
5810 .await
5811 {
5812 return;
5813 }
5814 match self.track_handle_or_err(track_name) {
5815 Ok(track) => {
5816 if let Err(e) =
5817 track
5818 .lock()
5819 .end_clap_parameter_edit(instance_id, param_id, frame)
5820 {
5821 self.notify_clients(Err(e)).await;
5822 return;
5823 }
5824 self.notify_clients(Ok(a.clone())).await;
5825 }
5826 Err(e) => {
5827 self.notify_clients(Err(e)).await;
5828 }
5829 }
5830 }
5831 Action::TrackGetClapParameters {
5832 ref track_name,
5833 instance_id,
5834 } => match self.track_handle_or_err(track_name) {
5835 Ok(track) => match track.lock().get_clap_parameters(instance_id) {
5836 Ok(parameters) => {
5837 self.notify_clients(Ok(Action::TrackClapParameters {
5838 track_name: track_name.clone(),
5839 instance_id,
5840 parameters,
5841 }))
5842 .await;
5843 }
5844 Err(e) => {
5845 self.notify_clients(Err(e)).await;
5846 }
5847 },
5848 Err(e) => {
5849 self.notify_clients(Err(e)).await;
5850 }
5851 },
5852 Action::TrackClapParameters { .. } => {}
5853 Action::TrackClapSnapshotState {
5854 ref track_name,
5855 instance_id,
5856 } => match self.track_handle_or_err(track_name) {
5857 Ok(track) => {
5858 let plugin_path = track
5859 .lock()
5860 .clap_plugins
5861 .iter()
5862 .find(|instance| instance.id == instance_id)
5863 .map(|instance| instance.processor.lock().path().to_string())
5864 .unwrap_or_default();
5865 match track.lock().clap_snapshot_state(instance_id) {
5866 Ok(state) => {
5867 self.notify_clients(Ok(Action::TrackClapStateSnapshot {
5868 track_name: track_name.clone(),
5869 instance_id,
5870 plugin_path,
5871 state,
5872 }))
5873 .await;
5874 }
5875 Err(e) => {
5876 self.notify_clients(Err(e)).await;
5877 }
5878 }
5879 }
5880 Err(e) => {
5881 self.notify_clients(Err(e)).await;
5882 }
5883 },
5884 Action::ClipClapSnapshotState {
5885 ref track_name,
5886 clip_idx,
5887 instance_id,
5888 } => match self.track_handle_or_err(track_name) {
5889 Ok(track) => match track.lock().clip_clap_snapshot_state(clip_idx, instance_id) {
5890 Ok((plugin_path, state)) => {
5891 self.notify_clients(Ok(Action::ClipClapStateSnapshot {
5892 track_name: track_name.clone(),
5893 clip_idx,
5894 instance_id,
5895 plugin_path,
5896 state,
5897 }))
5898 .await;
5899 }
5900 Err(e) => {
5901 self.notify_clients(Err(e)).await;
5902 }
5903 },
5904 Err(e) => {
5905 self.notify_clients(Err(e)).await;
5906 }
5907 },
5908 Action::TrackClapStateSnapshot { .. } => {}
5909 Action::ClipClapStateSnapshot { .. } => {}
5910 Action::TrackClapRestoreState {
5911 ref track_name,
5912 instance_id,
5913 ref state,
5914 } => {
5915 if self
5916 .reject_if_track_frozen(track_name, "CLAP state restore")
5917 .await
5918 {
5919 return;
5920 }
5921 let track = match self.track_handle_or_err(track_name) {
5922 Ok(track) => track,
5923 Err(e) => {
5924 self.notify_clients(Err(e)).await;
5925 return;
5926 }
5927 };
5928 let track = track.lock();
5929 if track.audio.processing {
5930 self.notify_clients(Err(format!(
5931 "Track '{}' is currently processing audio; stop playback before restoring CLAP state",
5932 track_name
5933 )))
5934 .await;
5935 return;
5936 }
5937 if let Err(e) = track.clap_restore_state(instance_id, state) {
5938 self.notify_clients(Err(e)).await;
5939 return;
5940 }
5941 }
5942 Action::ClipClapRestoreState {
5943 ref track_name,
5944 clip_idx,
5945 instance_id,
5946 ref state,
5947 } => {
5948 if self
5949 .reject_if_track_frozen(track_name, "CLAP state restore")
5950 .await
5951 {
5952 return;
5953 }
5954 let track = match self.track_handle_or_err(track_name) {
5955 Ok(track) => track,
5956 Err(e) => {
5957 self.notify_clients(Err(e)).await;
5958 return;
5959 }
5960 };
5961 let track = track.lock();
5962 if track.audio.processing {
5963 self.notify_clients(Err(format!(
5964 "Track '{}' is currently processing audio; stop playback before restoring CLAP state",
5965 track_name
5966 )))
5967 .await;
5968 return;
5969 }
5970 if let Err(e) = track.clip_clap_restore_state(clip_idx, instance_id, state) {
5971 self.notify_clients(Err(e)).await;
5972 return;
5973 }
5974 }
5975 Action::TrackSnapshotAllClapStates { ref track_name } => {
5976 let track = match self.track_handle_or_err(track_name) {
5977 Ok(track) => track,
5978 Err(e) => {
5979 self.notify_clients(Err(e)).await;
5980 return;
5981 }
5982 };
5983 for (instance_id, plugin_path, state) in track.lock().clap_snapshot_all_states() {
5984 self.notify_clients(Ok(Action::TrackClapStateSnapshot {
5985 track_name: track_name.clone(),
5986 instance_id,
5987 plugin_path,
5988 state,
5989 }))
5990 .await;
5991 }
5992 self.notify_clients(Ok(Action::TrackSnapshotAllClapStatesDone {
5993 track_name: track_name.clone(),
5994 }))
5995 .await;
5996 }
5997 Action::TrackSnapshotAllClapStatesDone { .. } => {}
5998 Action::TrackGetVst3Graph { ref track_name } => {
5999 match self.track_handle_or_err(track_name) {
6000 Ok(track) => {
6001 let t = track.lock();
6002 let plugins = t.vst3_graph_plugins();
6003 let connections = t.vst3_graph_connections();
6004 self.notify_clients(Ok(Action::TrackVst3Graph {
6005 track_name: track_name.clone(),
6006 plugins,
6007 connections,
6008 }))
6009 .await;
6010 }
6011 Err(e) => {
6012 self.notify_clients(Err(e)).await;
6013 }
6014 }
6015 }
6016 Action::TrackVst3Graph { .. } => {}
6017 Action::TrackSetVst3Parameter {
6018 ref track_name,
6019 instance_id,
6020 param_id,
6021 value,
6022 } => {
6023 if self
6024 .reject_if_track_frozen(track_name, "VST3 parameter changes")
6025 .await
6026 {
6027 return;
6028 }
6029 match self.track_handle_or_err(track_name) {
6030 Ok(track) => {
6031 if let Err(e) =
6032 track
6033 .lock()
6034 .set_vst3_parameter(instance_id, param_id, value)
6035 {
6036 self.notify_clients(Err(e)).await;
6037 return;
6038 }
6039 self.notify_clients(Ok(a.clone())).await;
6040 }
6041 Err(e) => {
6042 self.notify_clients(Err(e)).await;
6043 }
6044 }
6045 }
6046 Action::TrackSetPluginBypassed {
6047 ref track_name,
6048 instance_id,
6049 ref format,
6050 bypassed,
6051 } => match self.track_handle_or_err(track_name) {
6052 Ok(track) => {
6053 let result = match format.as_str() {
6054 "CLAP" => track.lock().set_clap_plugin_bypassed(instance_id, bypassed),
6055 "VST3" => track.lock().set_vst3_plugin_bypassed(instance_id, bypassed),
6056 #[cfg(all(unix, not(target_os = "macos")))]
6057 "LV2" => track.lock().set_lv2_plugin_bypassed(instance_id, bypassed),
6058 _ => Err(format!("Unknown plugin format for bypass: {format}")),
6059 };
6060 if let Err(e) = result {
6061 self.notify_clients(Err(e)).await;
6062 return;
6063 }
6064 self.notify_clients(Ok(a.clone())).await;
6065 }
6066 Err(e) => {
6067 self.notify_clients(Err(e)).await;
6068 }
6069 },
6070 Action::TrackGetVst3Parameters {
6071 ref track_name,
6072 instance_id,
6073 } => match self.track_handle_or_err(track_name) {
6074 Ok(track) => match track.lock().get_vst3_parameters(instance_id) {
6075 Ok(parameters) => {
6076 self.notify_clients(Ok(Action::TrackVst3Parameters {
6077 track_name: track_name.clone(),
6078 instance_id,
6079 parameters,
6080 }))
6081 .await;
6082 }
6083 Err(e) => {
6084 self.notify_clients(Err(e)).await;
6085 }
6086 },
6087 Err(e) => {
6088 self.notify_clients(Err(e)).await;
6089 }
6090 },
6091 Action::TrackVst3Parameters { .. } => {}
6092 Action::TrackVst3SnapshotState {
6093 ref track_name,
6094 instance_id,
6095 } => match self.track_handle_or_err(track_name) {
6096 Ok(track) => match track.lock().vst3_snapshot_state(instance_id) {
6097 Ok(state) => {
6098 self.notify_clients(Ok(Action::TrackVst3StateSnapshot {
6099 track_name: track_name.clone(),
6100 instance_id,
6101 state,
6102 }))
6103 .await;
6104 }
6105 Err(e) => {
6106 self.notify_clients(Err(e)).await;
6107 }
6108 },
6109 Err(e) => {
6110 self.notify_clients(Err(e)).await;
6111 }
6112 },
6113 Action::ClipVst3SnapshotState {
6114 ref track_name,
6115 clip_idx,
6116 instance_id,
6117 } => match self.track_handle_or_err(track_name) {
6118 Ok(track) => match track.lock().clip_vst3_snapshot_state(clip_idx, instance_id) {
6119 Ok(state) => {
6120 self.notify_clients(Ok(Action::ClipVst3StateSnapshot {
6121 track_name: track_name.clone(),
6122 clip_idx,
6123 instance_id,
6124 state,
6125 }))
6126 .await;
6127 }
6128 Err(e) => {
6129 self.notify_clients(Err(e)).await;
6130 }
6131 },
6132 Err(e) => {
6133 self.notify_clients(Err(e)).await;
6134 }
6135 },
6136 Action::TrackVst3StateSnapshot { .. } => {}
6137 Action::ClipVst3StateSnapshot { .. } => {}
6138 Action::TrackVst3RestoreState {
6139 ref track_name,
6140 instance_id,
6141 ref state,
6142 } => match self.track_handle_or_err(track_name) {
6143 Ok(track) => {
6144 if let Err(e) = track.lock().vst3_restore_state(instance_id, state) {
6145 self.notify_clients(Err(e)).await;
6146 return;
6147 }
6148 self.notify_clients(Ok(a.clone())).await;
6149 }
6150 Err(e) => {
6151 self.notify_clients(Err(e)).await;
6152 }
6153 },
6154 Action::TrackConnectVst3Audio {
6155 ref track_name,
6156 ref from_node,
6157 from_port,
6158 ref to_node,
6159 to_port,
6160 } => {
6161 if self
6162 .reject_if_track_frozen(track_name, "VST3 routing changes")
6163 .await
6164 {
6165 return;
6166 }
6167 match self.track_handle_or_err(track_name) {
6168 Ok(track) => {
6169 if let Err(e) = track
6170 .lock()
6171 .connect_vst3_audio(from_node, from_port, to_node, to_port)
6172 {
6173 self.notify_clients(Err(e)).await;
6174 return;
6175 }
6176 self.notify_clients(Ok(a.clone())).await;
6177 }
6178 Err(e) => {
6179 self.notify_clients(Err(e)).await;
6180 }
6181 }
6182 }
6183 Action::TrackDisconnectVst3Audio {
6184 ref track_name,
6185 ref from_node,
6186 from_port,
6187 ref to_node,
6188 to_port,
6189 } => {
6190 if self
6191 .reject_if_track_frozen(track_name, "VST3 routing changes")
6192 .await
6193 {
6194 return;
6195 }
6196 match self.track_handle_or_err(track_name) {
6197 Ok(track) => {
6198 if let Err(e) = track
6199 .lock()
6200 .disconnect_vst3_audio(from_node, from_port, to_node, to_port)
6201 {
6202 self.notify_clients(Err(e)).await;
6203 return;
6204 }
6205 self.notify_clients(Ok(a.clone())).await;
6206 }
6207 Err(e) => {
6208 self.notify_clients(Err(e)).await;
6209 }
6210 }
6211 }
6212 Action::ClipMove {
6213 ref kind,
6214 ref from,
6215 ref to,
6216 copy,
6217 } => {
6218 if let Some(from_track_handle) = self.state.lock().tracks.get(&from.track_name)
6219 && let Some(to_track_handle) = self.state.lock().tracks.get(&to.track_name)
6220 {
6221 let from_track = from_track_handle.lock();
6222 let to_track = to_track_handle.lock();
6223 match kind {
6224 Kind::Audio => {
6225 if from.clip_index >= from_track.audio.clips.len() {
6226 self.notify_clients(Err(format!(
6227 "Clip index {} is too high, as track {} has only {} clips!",
6228 from.clip_index,
6229 from_track.name.clone(),
6230 from_track.audio.clips.len(),
6231 )))
6232 .await;
6233 return;
6234 }
6235 if from_track.audio.ins.len() != to_track.audio.ins.len() {
6236 self.notify_clients(Err(format!(
6237 "Cannot move/copy audio clip from '{}' ({} inputs) to '{}' ({} inputs)",
6238 from_track.name,
6239 from_track.audio.ins.len(),
6240 to_track.name,
6241 to_track.audio.ins.len()
6242 )))
6243 .await;
6244 return;
6245 }
6246 let clip_copy = from_track.audio.clips[from.clip_index].clone();
6247 if !copy {
6248 from_track.audio.clips.remove(from.clip_index);
6249 }
6250 let mut clip_copy = clip_copy;
6251 clip_copy.start = to.sample_offset;
6252 let max_lane = to_track.audio.ins.len().saturating_sub(1);
6253 clip_copy.input_channel = to.input_channel.min(max_lane);
6254 to_track.audio.clips.push(clip_copy);
6255 }
6256 Kind::MIDI => {
6257 if from.clip_index >= from_track.midi.clips.len() {
6258 self.notify_clients(Err(format!(
6259 "Clip index {} is too high, as track {} has only {} clips!",
6260 from.clip_index,
6261 from_track.name.clone(),
6262 from_track.midi.clips.len(),
6263 )))
6264 .await;
6265 return;
6266 }
6267 let clip_copy = from_track.midi.clips[from.clip_index].clone();
6268 if !copy {
6269 from_track.midi.clips.remove(from.clip_index);
6270 }
6271 let mut clip_copy = clip_copy;
6272 clip_copy.start = to.sample_offset;
6273 let max_lane = to_track.midi.ins.len().saturating_sub(1);
6274 clip_copy.input_channel = to.input_channel.min(max_lane);
6275 to_track.midi.clips.push(clip_copy);
6276 }
6277 }
6278 }
6279 }
6280 Action::AddClip {
6281 ref name,
6282 ref track_name,
6283 start,
6284 length,
6285 offset,
6286 input_channel,
6287 muted,
6288 ref peaks_file,
6289 kind,
6290 fade_enabled,
6291 fade_in_samples,
6292 fade_out_samples,
6293 ref source_name,
6294 source_offset,
6295 source_length,
6296 ref preview_name,
6297 ref pitch_correction_points,
6298 pitch_correction_frame_likeness,
6299 pitch_correction_inertia_ms,
6300 pitch_correction_formant_compensation,
6301 ref plugin_graph_json,
6302 } => {
6303 self.add_clip_to_track(ClipAddRequest {
6304 name,
6305 track_name,
6306 start,
6307 length,
6308 offset,
6309 input_channel,
6310 muted,
6311 peaks_file: peaks_file.clone(),
6312 kind,
6313 fade_enabled,
6314 fade_in_samples,
6315 fade_out_samples,
6316 source_name: source_name.clone(),
6317 source_offset,
6318 source_length,
6319 preview_name: preview_name.clone(),
6320 pitch_correction_points: pitch_correction_points.clone(),
6321 pitch_correction_frame_likeness,
6322 pitch_correction_inertia_ms,
6323 pitch_correction_formant_compensation,
6324 plugin_graph_json: plugin_graph_json.clone(),
6325 });
6326 if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
6327 let track_name = track_name.clone();
6328 tokio::task::spawn_blocking(move || {
6329 track.lock().preload_clips();
6330 tracing::debug!("Preloaded clips for track '{}' after AddClip", track_name);
6331 });
6332 }
6333 }
6334 Action::AddGroupedClip {
6335 ref track_name,
6336 kind,
6337 ref audio_clip,
6338 ref midi_clip,
6339 } => {
6340 self.add_grouped_clip_to_track(
6341 track_name,
6342 kind,
6343 audio_clip.clone(),
6344 midi_clip.clone(),
6345 );
6346 if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
6347 let track_name = track_name.clone();
6348 tokio::task::spawn_blocking(move || {
6349 track.lock().preload_clips();
6350 tracing::debug!(
6351 "Preloaded clips for track '{}' after AddGroupedClip",
6352 track_name
6353 );
6354 });
6355 }
6356 }
6357 Action::RemoveClip {
6358 ref track_name,
6359 kind,
6360 ref clip_indices,
6361 } => {
6362 self.remove_clips_from_track(track_name, kind, clip_indices);
6363 }
6364 Action::RenameClip {
6365 ref track_name,
6366 kind,
6367 clip_index,
6368 ref new_name,
6369 } => {
6370 self.rename_clip_references(track_name, kind, clip_index, new_name);
6371 }
6372 Action::SetClipSourceName {
6373 ref track_name,
6374 kind,
6375 clip_index,
6376 ref name,
6377 } => {
6378 self.set_clip_source_name(track_name, clip_index, kind, name.clone());
6379 }
6380 Action::SetClipFade {
6381 ref track_name,
6382 clip_index,
6383 kind,
6384 fade_enabled,
6385 fade_in_samples,
6386 fade_out_samples,
6387 } => {
6388 self.set_clip_fade(
6389 track_name,
6390 clip_index,
6391 kind,
6392 fade_enabled,
6393 fade_in_samples,
6394 fade_out_samples,
6395 );
6396 }
6397 Action::SetClipBounds {
6398 ref track_name,
6399 clip_index,
6400 kind,
6401 start,
6402 length,
6403 offset,
6404 } => {
6405 self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
6406 }
6407 Action::SyncClipBounds {
6408 ref track_name,
6409 clip_index,
6410 kind,
6411 start,
6412 length,
6413 offset,
6414 } => {
6415 self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
6416 }
6417 Action::SetClipMuted {
6418 ref track_name,
6419 clip_index,
6420 kind,
6421 muted,
6422 } => {
6423 self.set_clip_muted(track_name, clip_index, kind, muted);
6424 }
6425 Action::SetClipPluginGraphJson {
6426 ref track_name,
6427 clip_index,
6428 ref plugin_graph_json,
6429 } => {
6430 self.set_clip_plugin_graph_json(track_name, clip_index, plugin_graph_json.clone());
6431 }
6432 Action::SetClipPitchCorrection {
6433 ref track_name,
6434 clip_index,
6435 ref preview_name,
6436 ref source_name,
6437 source_offset,
6438 source_length,
6439 ref pitch_correction_points,
6440 pitch_correction_frame_likeness,
6441 pitch_correction_inertia_ms,
6442 pitch_correction_formant_compensation,
6443 } => {
6444 self.set_clip_pitch_correction(
6445 track_name,
6446 clip_index,
6447 preview_name.clone(),
6448 source_name.clone(),
6449 source_offset,
6450 source_length,
6451 pitch_correction_points.clone(),
6452 pitch_correction_frame_likeness,
6453 pitch_correction_inertia_ms,
6454 pitch_correction_formant_compensation,
6455 );
6456 }
6457 Action::Connect {
6458 ref from_track,
6459 from_port,
6460 ref to_track,
6461 to_port,
6462 kind,
6463 } => {
6464 match kind {
6465 Kind::Audio => {
6466 let from_audio_io = if from_track == "hw:in" {
6467 self.hw_input_audio_port(from_port)
6468 } else {
6469 self.state
6470 .lock()
6471 .tracks
6472 .get(from_track)
6473 .and_then(|t| t.lock().audio.outs.get(from_port).cloned())
6474 };
6475 let to_audio_io = if to_track == "hw:out" {
6476 self.hw_output_audio_port(to_port)
6477 } else {
6478 self.state
6479 .lock()
6480 .tracks
6481 .get(to_track)
6482 .and_then(|t| t.lock().audio.ins.get(to_port).cloned())
6483 };
6484 match (from_audio_io, to_audio_io) {
6485 (Some(source), Some(target)) => {
6486 if from_track != "hw:in"
6487 && to_track != "hw:out"
6488 && self.check_if_leads_to_kind(
6489 Kind::Audio,
6490 to_track,
6491 from_track,
6492 )
6493 {
6494 self.notify_clients(Err(
6495 "Circular routing is not allowed!".into()
6496 ))
6497 .await;
6498 return;
6499 }
6500 crate::audio::io::AudioIO::connect(&source, &target);
6501 }
6502 (None, _) => {
6503 self.notify_clients(Err(format!(
6504 "Source track '{}' not found",
6505 from_track
6506 )))
6507 .await;
6508 return;
6509 }
6510 (_, None) => {
6511 self.notify_clients(Err(format!(
6512 "Destination track '{}' not found",
6513 to_track
6514 )))
6515 .await;
6516 return;
6517 }
6518 }
6519 }
6520 Kind::MIDI => {
6521 let from_hw_in_device = Self::midi_hw_in_device(from_track);
6522 let to_hw_out_device = Self::midi_hw_out_device(to_track);
6523 let from_is_invalid_hw = Self::midi_hw_out_device(from_track).is_some();
6524 let to_is_invalid_hw = Self::midi_hw_in_device(to_track).is_some();
6525
6526 if from_is_invalid_hw || to_is_invalid_hw {
6527 self.notify_clients(Err(
6528 "Invalid MIDI hardware connection direction".to_string()
6529 ))
6530 .await;
6531 return;
6532 }
6533
6534 if from_hw_in_device.is_none()
6535 && to_hw_out_device.is_none()
6536 && self.check_if_leads_to_kind(Kind::MIDI, to_track, from_track)
6537 {
6538 self.notify_clients(Err("Circular routing is not allowed!".into()))
6539 .await;
6540 return;
6541 }
6542
6543 let state = self.state.lock();
6544 let from_track_handle = state.tracks.get(from_track);
6545 let to_track_handle = state.tracks.get(to_track);
6546
6547 if let (Some(from_device), Some(to_device)) =
6548 (from_hw_in_device, to_hw_out_device)
6549 {
6550 let route = MidiHwThruRoute {
6551 from_device: from_device.to_string(),
6552 to_device: to_device.to_string(),
6553 };
6554 if !self.midi_hw_thru_routes.iter().any(|r| r == &route) {
6555 self.midi_hw_thru_routes.push(route);
6556 }
6557 } else if let Some(device) = from_hw_in_device {
6558 if let Some(t_t) = to_track_handle {
6559 if t_t.lock().midi.ins.get(to_port).is_none() {
6560 self.notify_clients(Err(format!(
6561 "MIDI input port {} not found on track '{}'",
6562 to_port, to_track
6563 )))
6564 .await;
6565 return;
6566 }
6567 let route = MidiHwInRoute {
6568 device: device.to_string(),
6569 to_track: to_track.to_string(),
6570 to_port,
6571 };
6572 if !self.midi_hw_in_routes.iter().any(|r| r == &route) {
6573 self.midi_hw_in_routes.push(route);
6574 }
6575 } else {
6576 self.notify_clients(Err(format!(
6577 "MIDI destination track not found: {}",
6578 to_track
6579 )))
6580 .await;
6581 return;
6582 }
6583 } else if let Some(device) = to_hw_out_device {
6584 if let Some(f_t) = from_track_handle {
6585 if f_t.lock().midi.outs.get(from_port).is_none() {
6586 self.notify_clients(Err(format!(
6587 "MIDI output port {} not found on track '{}'",
6588 from_port, from_track
6589 )))
6590 .await;
6591 return;
6592 }
6593 let route = MidiHwOutRoute {
6594 from_track: from_track.to_string(),
6595 from_port,
6596 device: device.to_string(),
6597 };
6598 if !self.midi_hw_out_routes.iter().any(|r| r == &route) {
6599 self.midi_hw_out_routes.push(route);
6600 }
6601 } else {
6602 self.notify_clients(Err(format!(
6603 "MIDI source track not found: {}",
6604 from_track
6605 )))
6606 .await;
6607 return;
6608 }
6609 } else {
6610 match (from_track_handle, to_track_handle) {
6611 (Some(f_t), Some(t_t)) => {
6612 let to_in_res = t_t.lock().midi.ins.get(to_port).cloned();
6613 if let Some(to_in) = to_in_res {
6614 let from_track = f_t.lock();
6615 if let Err(e) =
6616 from_track.midi.connect_out(from_port, to_in)
6617 {
6618 self.notify_clients(Err(e)).await;
6619 return;
6620 }
6621 from_track.invalidate_midi_route_cache();
6622 } else {
6623 self.notify_clients(Err(format!(
6624 "MIDI input port {} not found on track '{}'",
6625 to_port, to_track
6626 )))
6627 .await;
6628 return;
6629 }
6630 }
6631 _ => {
6632 self.notify_clients(Err(format!(
6633 "MIDI tracks not found: {} or {}",
6634 from_track, to_track
6635 )))
6636 .await;
6637 return;
6638 }
6639 }
6640 }
6641 }
6642 };
6643 }
6644 Action::Disconnect {
6645 ref from_track,
6646 from_port,
6647 ref to_track,
6648 to_port,
6649 kind,
6650 } => {
6651 if kind == Kind::Audio {
6652 if let Err(e) = self.disconnect_audio_route_and_notify(a.clone()).await {
6653 self.notify_clients(Err(e)).await;
6654 }
6655 } else if kind == Kind::MIDI {
6656 let from_hw_in_device = Self::midi_hw_in_device(from_track);
6657 let to_hw_out_device = Self::midi_hw_out_device(to_track);
6658
6659 if let (Some(from_device), Some(to_device)) =
6660 (from_hw_in_device, to_hw_out_device)
6661 {
6662 let before = self.midi_hw_thru_routes.len();
6663 self.midi_hw_thru_routes.retain(|r| {
6664 !(r.from_device == from_device && r.to_device == to_device)
6665 });
6666 if self.midi_hw_thru_routes.len() < before {
6667 self.notify_clients(Ok(a.clone())).await;
6668 } else {
6669 self.notify_clients(Err(format!(
6670 "Disconnect failed: MIDI route not found ({} -> {})",
6671 from_track, to_track
6672 )))
6673 .await;
6674 }
6675 return;
6676 }
6677
6678 if let Some(device) = from_hw_in_device {
6679 let before = self.midi_hw_in_routes.len();
6680 self.midi_hw_in_routes.retain(|r| {
6681 !(r.device == device && r.to_track == *to_track && r.to_port == to_port)
6682 });
6683 if self.midi_hw_in_routes.len() < before {
6684 self.notify_clients(Ok(a.clone())).await;
6685 } else {
6686 self.notify_clients(Err(format!(
6687 "Disconnect failed: MIDI route not found ({} -> {})",
6688 from_track, to_track
6689 )))
6690 .await;
6691 }
6692 return;
6693 }
6694
6695 if let Some(device) = to_hw_out_device {
6696 let before = self.midi_hw_out_routes.len();
6697 self.midi_hw_out_routes.retain(|r| {
6698 !(r.from_track == *from_track
6699 && r.from_port == from_port
6700 && r.device == device)
6701 });
6702 if self.midi_hw_out_routes.len() < before {
6703 self.notify_clients(Ok(a.clone())).await;
6704 } else {
6705 self.notify_clients(Err(format!(
6706 "Disconnect failed: MIDI route not found ({} -> {})",
6707 from_track, to_track
6708 )))
6709 .await;
6710 }
6711 return;
6712 }
6713
6714 let state = self.state.lock();
6715 if let (Some(f_t), Some(t_t)) =
6716 (state.tracks.get(from_track), state.tracks.get(to_track))
6717 && let Some(to_in) = t_t.lock().midi.ins.get(to_port).cloned()
6718 {
6719 let from_track = f_t.lock();
6720 if let Err(e) = from_track.midi.disconnect_out(from_port, &to_in) {
6721 self.notify_clients(Err(e)).await;
6722 } else {
6723 from_track.invalidate_midi_route_cache();
6724 self.notify_clients(Ok(a.clone())).await;
6725 }
6726 } else {
6727 self.notify_clients(Err(format!(
6728 "Disconnect failed: MIDI ports not found ({} -> {})",
6729 from_track, to_track
6730 )))
6731 .await;
6732 }
6733 }
6734 }
6735
6736 Action::OpenAudioDevice {
6737 ref device,
6738 ref input_device,
6739 sample_rate_hz,
6740 bits,
6741 exclusive,
6742 period_frames,
6743 realtime_frames,
6744 low_watermark_frames,
6745 nperiods,
6746 sync_mode,
6747 } => {
6748 #[cfg(unix)]
6749 {
6750 let request = AudioOpenRequest {
6751 device,
6752 input_device: input_device.as_deref(),
6753 sample_rate_hz,
6754 bits,
6755 exclusive,
6756 period_frames,
6757 realtime_frames,
6758 low_watermark_frames,
6759 nperiods,
6760 sync_mode,
6761 };
6762 if self.maybe_open_jack_runtime(request).await.is_some() {
6763 return;
6764 }
6765 }
6766 let hw_opts =
6767 Self::build_hw_options(exclusive, realtime_frames, nperiods, sync_mode);
6768 self.hybrid_playback_frames = period_frames.max(1);
6769 self.hybrid_realtime_frames = realtime_frames.max(1);
6770 self.hybrid_low_watermark_frames = low_watermark_frames.max(1);
6771 let open_result = self
6772 .open_non_jack_audio_device(
6773 device,
6774 input_device.as_deref(),
6775 sample_rate_hz,
6776 bits,
6777 hw_opts,
6778 )
6779 .await;
6780 match open_result {
6781 Ok(()) => {}
6782 Err(e) => {
6783 self.notify_clients(Err(e)).await;
6784 return;
6785 }
6786 }
6787 self.finalize_open_audio_device().await;
6788 }
6789 Action::JackAddAudioInputPort => {
6790 #[cfg(unix)]
6791 {
6792 if let Some(jack) = self.jack_runtime.clone() {
6793 let (input_channels, output_channels, rate) = {
6794 let jack = jack.lock();
6795 if let Err(e) = jack.add_audio_input_port() {
6796 self.notify_clients(Err(e)).await;
6797 return;
6798 }
6799 (
6800 jack.input_channels(),
6801 jack.output_channels(),
6802 jack.sample_rate,
6803 )
6804 };
6805 self.publish_hw_infos(input_channels, output_channels, rate)
6806 .await;
6807 self.notify_clients(Ok(a.clone())).await;
6808 } else {
6809 self.notify_clients(Err(
6810 "JACK runtime is not active; open the JACK backend first".to_string(),
6811 ))
6812 .await;
6813 }
6814 }
6815 #[cfg(not(unix))]
6816 {
6817 self.notify_clients(Err(
6818 "JACK backend is not available on this platform build".to_string(),
6819 ))
6820 .await;
6821 }
6822 }
6823 Action::JackRemoveAudioInputPort(_removed_port) => {
6824 #[cfg(unix)]
6825 {
6826 let removed_port = _removed_port;
6827 if let Some(jack) = self.jack_runtime.clone() {
6828 let (removed_port, removed_io) = {
6829 let jack = jack.lock();
6830 let removed_port = Some(removed_port);
6831 let removed_io =
6832 removed_port.and_then(|port| jack.input_audio_port(port));
6833 match (removed_port, removed_io) {
6834 (Some(port), Some(io)) => (port, io),
6835 _ => {
6836 self.notify_clients(Err(
6837 "JACK audio input port index is out of range".to_string(),
6838 ))
6839 .await;
6840 return;
6841 }
6842 }
6843 };
6844 let reindex_notifications =
6845 self.reindex_notifications_for_removed_hw_input(removed_port);
6846 for disconnect in
6847 self.disconnect_actions_for_removed_hw_input(removed_port, &removed_io)
6848 {
6849 if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await
6850 {
6851 self.notify_clients(Err(e)).await;
6852 return;
6853 }
6854 }
6855 let (input_channels, output_channels, rate) = {
6856 let jack = jack.lock();
6857 if let Err(e) = jack.remove_audio_input_port(removed_port) {
6858 self.notify_clients(Err(e)).await;
6859 return;
6860 }
6861 (
6862 jack.input_channels(),
6863 jack.output_channels(),
6864 jack.sample_rate,
6865 )
6866 };
6867 for action in reindex_notifications {
6868 self.notify_clients(Ok(action)).await;
6869 }
6870 self.publish_hw_infos(input_channels, output_channels, rate)
6871 .await;
6872 self.notify_clients(Ok(a.clone())).await;
6873 } else {
6874 self.notify_clients(Err(
6875 "JACK runtime is not active; open the JACK backend first".to_string(),
6876 ))
6877 .await;
6878 }
6879 }
6880 #[cfg(not(unix))]
6881 {
6882 self.notify_clients(Err(
6883 "JACK backend is not available on this platform build".to_string(),
6884 ))
6885 .await;
6886 }
6887 }
6888 Action::JackAddAudioOutputPort => {
6889 #[cfg(unix)]
6890 {
6891 if let Some(jack) = self.jack_runtime.clone() {
6892 let (input_channels, output_channels, rate) = {
6893 let jack = jack.lock();
6894 if let Err(e) = jack.add_audio_output_port() {
6895 self.notify_clients(Err(e)).await;
6896 return;
6897 }
6898 (
6899 jack.input_channels(),
6900 jack.output_channels(),
6901 jack.sample_rate,
6902 )
6903 };
6904 self.publish_hw_infos(input_channels, output_channels, rate)
6905 .await;
6906 self.notify_clients(Ok(a.clone())).await;
6907 } else {
6908 self.notify_clients(Err(
6909 "JACK runtime is not active; open the JACK backend first".to_string(),
6910 ))
6911 .await;
6912 }
6913 }
6914 #[cfg(not(unix))]
6915 {
6916 self.notify_clients(Err(
6917 "JACK backend is not available on this platform build".to_string(),
6918 ))
6919 .await;
6920 }
6921 }
6922 Action::JackRemoveAudioOutputPort(_removed_port) => {
6923 #[cfg(unix)]
6924 {
6925 let removed_port = _removed_port;
6926 if let Some(jack) = self.jack_runtime.clone() {
6927 let (removed_port, removed_io) = {
6928 let jack = jack.lock();
6929 let removed_port = Some(removed_port);
6930 let removed_io =
6931 removed_port.and_then(|port| jack.output_audio_port(port));
6932 match (removed_port, removed_io) {
6933 (Some(port), Some(io)) => (port, io),
6934 _ => {
6935 self.notify_clients(Err(
6936 "JACK audio output port index is out of range".to_string(),
6937 ))
6938 .await;
6939 return;
6940 }
6941 }
6942 };
6943 let reindex_notifications =
6944 self.reindex_notifications_for_removed_hw_output(removed_port);
6945 for disconnect in
6946 self.disconnect_actions_for_removed_hw_output(removed_port, &removed_io)
6947 {
6948 if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await
6949 {
6950 self.notify_clients(Err(e)).await;
6951 return;
6952 }
6953 }
6954 let (input_channels, output_channels, rate) = {
6955 let jack = jack.lock();
6956 if let Err(e) = jack.remove_audio_output_port(removed_port) {
6957 self.notify_clients(Err(e)).await;
6958 return;
6959 }
6960 (
6961 jack.input_channels(),
6962 jack.output_channels(),
6963 jack.sample_rate,
6964 )
6965 };
6966 for action in reindex_notifications {
6967 self.notify_clients(Ok(action)).await;
6968 }
6969 self.publish_hw_infos(input_channels, output_channels, rate)
6970 .await;
6971 self.notify_clients(Ok(a.clone())).await;
6972 } else {
6973 self.notify_clients(Err(
6974 "JACK runtime is not active; open the JACK backend first".to_string(),
6975 ))
6976 .await;
6977 }
6978 }
6979 #[cfg(not(unix))]
6980 {
6981 self.notify_clients(Err(
6982 "JACK backend is not available on this platform build".to_string(),
6983 ))
6984 .await;
6985 }
6986 }
6987 Action::OpenMidiInputDevice(ref device) => {
6988 let midi_hub = self.midi_hub.lock();
6989 if let Err(e) = midi_hub.open_input(device) {
6990 self.notify_clients(Err(e)).await;
6991 return;
6992 }
6993 }
6994 Action::OpenMidiOutputDevice(ref device) => {
6995 let midi_hub = self.midi_hub.lock();
6996 if let Err(e) = midi_hub.open_output(device) {
6997 self.notify_clients(Err(e)).await;
6998 return;
6999 }
7000 }
7001 Action::RequestSessionDiagnostics => {
7002 let (
7003 track_count,
7004 frozen_track_count,
7005 audio_clip_count,
7006 midi_clip_count,
7007 lv2_instance_count,
7008 vst3_instance_count,
7009 clap_instance_count,
7010 ) = {
7011 let tracks = &self.state.lock().tracks;
7012 let mut track_count = 0usize;
7013 let mut frozen_track_count = 0usize;
7014 let mut audio_clip_count = 0usize;
7015 let mut midi_clip_count = 0usize;
7016 #[cfg(all(unix, not(target_os = "macos")))]
7017 let mut lv2_instance_count = 0usize;
7018 #[cfg(not(all(unix, not(target_os = "macos"))))]
7019 let lv2_instance_count = 0usize;
7020 let mut vst3_instance_count = 0usize;
7021 let mut clap_instance_count = 0usize;
7022 for track in tracks.values() {
7023 let t = track.lock();
7024 track_count += 1;
7025 if t.frozen {
7026 frozen_track_count += 1;
7027 }
7028 audio_clip_count += t.audio.clips.len();
7029 midi_clip_count += t.midi.clips.len();
7030 #[cfg(all(unix, not(target_os = "macos")))]
7031 {
7032 lv2_instance_count += t.lv2_plugins.len();
7033 }
7034 vst3_instance_count += t.vst3_plugins.len();
7035 clap_instance_count += t.clap_plugins.len();
7036 }
7037 (
7038 track_count,
7039 frozen_track_count,
7040 audio_clip_count,
7041 midi_clip_count,
7042 lv2_instance_count,
7043 vst3_instance_count,
7044 clap_instance_count,
7045 )
7046 };
7047 #[cfg(not(all(unix, not(target_os = "macos"))))]
7048 let _lv2_instance_count = lv2_instance_count;
7049 let pending_hw_midi_events = self.pending_hw_midi_events.len()
7050 + self
7051 .pending_hw_midi_events_by_device
7052 .values()
7053 .map(std::vec::Vec::len)
7054 .sum::<usize>();
7055 let sample_rate_hz = if let Some(hw) = &self.hw_driver {
7056 hw.lock().sample_rate() as usize
7057 } else {
7058 #[cfg(unix)]
7059 {
7060 self.jack_runtime
7061 .as_ref()
7062 .map(|j| j.lock().sample_rate)
7063 .unwrap_or(0)
7064 }
7065 #[cfg(not(unix))]
7066 0
7067 };
7068 let cycle_samples = self.current_cycle_samples();
7069 tracing::info!(
7070 "Hybrid diagnostics: refill_budget_per_pass={}, refill_budget_throttle_count={}, realtime_fallback_dispatch_count={}, realtime_ready={}, refill_ready={}",
7071 self.refill_budget_per_pass,
7072 self.refill_budget_throttle_count,
7073 self.realtime_fallback_dispatch_count,
7074 self.ready_realtime_workers.len(),
7075 self.ready_refill_workers.len()
7076 );
7077 self.notify_clients(Ok(Action::SessionDiagnosticsReport {
7078 track_count,
7079 frozen_track_count,
7080 audio_clip_count,
7081 midi_clip_count,
7082 #[cfg(all(unix, not(target_os = "macos")))]
7083 lv2_instance_count,
7084 vst3_instance_count,
7085 clap_instance_count,
7086 pending_requests: self.pending_requests.len(),
7087 workers_total: self.workers.len(),
7088 workers_ready: self.ready_realtime_workers.len()
7089 + self.ready_refill_workers.len(),
7090 pending_hw_midi_events,
7091 playing: self.playing,
7092 transport_sample: self.transport_sample,
7093 tempo_bpm: self.tempo_bpm,
7094 sample_rate_hz,
7095 cycle_samples,
7096 }))
7097 .await;
7098 }
7099 Action::RequestMidiLearnMappingsReport => {
7100 let mut lines = Vec::<String>::new();
7101 let fmt_binding = |b: &crate::message::MidiLearnBinding| {
7102 let device = b.device.as_deref().unwrap_or("*");
7103 format!("{device} CH{} CC{}", b.channel + 1, b.cc)
7104 };
7105 if let Some(b) = self.global_midi_learn_play_pause.as_ref() {
7106 lines.push(format!("Global PlayPause: {}", fmt_binding(b)));
7107 }
7108 if let Some(b) = self.global_midi_learn_stop.as_ref() {
7109 lines.push(format!("Global Stop: {}", fmt_binding(b)));
7110 }
7111 if let Some(b) = self.global_midi_learn_record_toggle.as_ref() {
7112 lines.push(format!("Global RecordToggle: {}", fmt_binding(b)));
7113 }
7114 for (track_name, track) in self.state.lock().tracks.iter() {
7115 let t = track.lock();
7116 if let Some(b) = t.midi_learn_volume.as_ref() {
7117 lines.push(format!("{} Volume: {}", track_name, fmt_binding(b)));
7118 }
7119 if let Some(b) = t.midi_learn_balance.as_ref() {
7120 lines.push(format!("{} Balance: {}", track_name, fmt_binding(b)));
7121 }
7122 if let Some(b) = t.midi_learn_mute.as_ref() {
7123 lines.push(format!("{} Mute: {}", track_name, fmt_binding(b)));
7124 }
7125 if let Some(b) = t.midi_learn_solo.as_ref() {
7126 lines.push(format!("{} Solo: {}", track_name, fmt_binding(b)));
7127 }
7128 if let Some(b) = t.midi_learn_arm.as_ref() {
7129 lines.push(format!("{} Arm: {}", track_name, fmt_binding(b)));
7130 }
7131 if let Some(b) = t.midi_learn_input_monitor.as_ref() {
7132 lines.push(format!("{} InputMonitor: {}", track_name, fmt_binding(b)));
7133 }
7134 if let Some(b) = t.midi_learn_disk_monitor.as_ref() {
7135 lines.push(format!("{} DiskMonitor: {}", track_name, fmt_binding(b)));
7136 }
7137 }
7138 if lines.is_empty() {
7139 lines.push("No MIDI learn mappings configured".to_string());
7140 }
7141 self.notify_clients(Ok(Action::MidiLearnMappingsReport { lines }))
7142 .await;
7143 }
7144 Action::ClearAllMidiLearnBindings => {
7145 self.pending_midi_learn = None;
7146 self.pending_global_midi_learn = None;
7147 self.global_midi_learn_play_pause = None;
7148 self.global_midi_learn_stop = None;
7149 self.global_midi_learn_record_toggle = None;
7150 self.midi_cc_gate.clear();
7151 for track in self.state.lock().tracks.values() {
7152 let t = track.lock();
7153 t.midi_learn_volume = None;
7154 t.midi_learn_balance = None;
7155 t.midi_learn_mute = None;
7156 t.midi_learn_solo = None;
7157 t.midi_learn_arm = None;
7158 t.midi_learn_input_monitor = None;
7159 t.midi_learn_disk_monitor = None;
7160 }
7161 }
7162 #[cfg(all(unix, not(target_os = "macos")))]
7163 Action::TrackLv2PluginControls { .. } => {}
7164 #[cfg(all(unix, not(target_os = "macos")))]
7165 Action::ClipLv2PluginControls { .. } => {}
7166 #[cfg(all(unix, not(target_os = "macos")))]
7167 Action::TrackLv2Midnam { .. } => {}
7168 Action::TrackClapNoteNames { .. } => {}
7169 Action::SessionDiagnosticsReport { .. } => {}
7170 Action::MidiLearnMappingsReport { .. } => {}
7171 Action::HWInfo { .. } => {}
7172 Action::HistoryState { .. } => {}
7173 Action::Undo => {}
7174 Action::Redo => {}
7175 Action::ApplyGroupedActions(_) => {}
7176 _ => {}
7177 }
7178
7179 if let Some(inverse) = inverse_actions {
7180 if let Some(group) = self.history_group.as_mut() {
7181 group.forward_actions.push(action_to_process.clone());
7182 group.inverse_actions.splice(0..0, inverse);
7183 } else {
7184 self.history.record(UndoEntry {
7185 forward_actions: vec![action_to_process.clone()],
7186 inverse_actions: inverse,
7187 });
7188 }
7189 }
7190
7191 self.notify_clients(Ok(action_to_process)).await;
7192 }
7193
7194 pub async fn work(&mut self) {
7195 while let Some(message) = self.rx.recv().await {
7196 match message {
7197 Message::Ready(id) => self.push_ready_worker(id),
7198 Message::Finished {
7199 worker_id,
7200 track_name,
7201 output_linear,
7202 process_epoch,
7203 parameter_updates,
7204 } => {
7205 self.push_ready_worker(worker_id);
7206 self.track_processing_started_at.remove(&track_name);
7207 if process_epoch != self.track_process_epoch {
7208 if let Some(track) = self.state.lock().tracks.get(&track_name).cloned() {
7209 let t = track.lock();
7210 t.audio.finished = false;
7211 t.audio.processing = false;
7212 }
7213 continue;
7214 }
7215 self.track_meter_linear_by_track
7216 .insert(track_name, output_linear);
7217 for action in parameter_updates {
7218 self.notify_clients(Ok(action)).await;
7219 }
7220 self.force_stalled_track_completions();
7221 let all_finished = self.send_tracks().await;
7222 if all_finished {
7223 self.on_all_tracks_finished().await;
7224 }
7225 }
7226 Message::Channel(s) => {
7227 self.clients.push(s);
7228 }
7229
7230 Message::Request(a) => match a {
7231 Action::TrackOfflineBounceCancel { track_name } => {
7232 if let Some(job) = self.offline_bounce_jobs.get(&track_name) {
7233 job.cancel.store(true, Ordering::Relaxed);
7234 }
7235 }
7236 Action::TrackOfflineBounceCancelAll => {
7237 for job in self.offline_bounce_jobs.values() {
7238 job.cancel.store(true, Ordering::Relaxed);
7239 }
7240 }
7241 _ if !self.offline_bounce_jobs.is_empty() => {
7242 self.pending_requests.push_back(a);
7243 }
7244 Action::OpenAudioDevice { .. }
7245 | Action::OpenMidiInputDevice(_)
7246 | Action::OpenMidiOutputDevice(_)
7247 | Action::RequestMeterSnapshot
7248 | Action::Quit
7249 | Action::Play
7250 | Action::Pause
7251 | Action::Stop
7252 | Action::TransportPosition(_)
7253 | Action::JumpToEnd
7254 | Action::SetLoopEnabled(_)
7255 | Action::SetLoopRange(_)
7256 | Action::SetPunchEnabled(_)
7257 | Action::SetPunchRange(_)
7258 | Action::SetMetronomeEnabled(_)
7259 | Action::SetTempo(_)
7260 | Action::SetTimeSignature { .. }
7261 | Action::SetOscEnabled(_)
7262 | Action::SetClipPlaybackEnabled(_)
7263 | Action::SetRecordEnabled(_)
7264 | Action::SetSessionPath(_)
7265 | Action::ClearHistory
7266 | Action::BeginSessionRestore
7267 | Action::PianoKey { .. }
7268 | Action::ModifyMidiNotes { .. }
7269 | Action::ModifyMidiControllers { .. }
7270 | Action::DeleteMidiControllers { .. }
7271 | Action::InsertMidiControllers { .. }
7272 | Action::DeleteMidiNotes { .. }
7273 | Action::InsertMidiNotes { .. }
7274 | Action::SetMidiSysExEvents { .. } => {
7275 self.handle_request(a).await;
7276 }
7277 #[cfg(all(unix, not(target_os = "macos")))]
7278 Action::ListLv2Plugins => {
7279 self.handle_request(a).await;
7280 }
7281 Action::ListVst3Plugins => {
7282 self.handle_request(a).await;
7283 }
7284 Action::ListClapPlugins => {
7285 self.handle_request(a).await;
7286 }
7287 Action::ListClapPluginsWithCapabilities => {
7288 self.handle_request(a).await;
7289 }
7290 _ => {
7291 self.pending_requests.push_back(a);
7292 if self.can_schedule_hw_cycle() {
7293 self.request_hw_cycle().await;
7294 } else {
7295 while let Some(next) = self.pending_requests.pop_front() {
7296 self.handle_request(next).await;
7297 }
7298 }
7299 }
7300 },
7301 Message::OfflineBounceFinished { result } => {
7302 if let Ok(Action::TrackOfflineBounce { track_name, .. }) = &result {
7303 self.offline_bounce_jobs.remove(track_name);
7304 }
7305 self.notify_clients(result).await;
7306 if self.offline_bounce_jobs.is_empty() {
7307 while let Some(next) = self.pending_requests.pop_front() {
7308 self.handle_request(next).await;
7309 }
7310 }
7311 }
7312 Message::HWFinished => {
7313 if !self.awaiting_hwfinished {
7314 continue;
7315 }
7316 self.handling_hwfinished = true;
7317 self.awaiting_hwfinished = false;
7318 #[cfg(unix)]
7319 {
7320 if let Some(jack) = &self.jack_runtime {
7321 if !self.pending_hw_midi_out_events.is_empty() {
7322 let out_events =
7323 std::mem::take(&mut self.pending_hw_midi_out_events);
7324 jack.lock().write_events(&out_events);
7325 }
7326 let mut in_events = vec![];
7327 jack.lock().read_events_into(&mut in_events);
7328 if !in_events.is_empty() {
7329 self.pending_hw_midi_events.extend(in_events);
7330 }
7331 }
7332 }
7333 #[cfg(unix)]
7334 if self.jack_runtime.is_some() {
7335 self.sync_from_jack_transport().await;
7336 }
7337 while let Some(a) = self.pending_requests.pop_front() {
7338 self.handle_request(a).await;
7339 }
7340 self.apply_mute_solo_policy();
7341 self.append_recorded_cycle();
7342 self.flush_completed_recordings().await;
7343 let hw_in_routes = self.midi_hw_in_routes.clone();
7344 let pending_hw_in_by_device = self.pending_hw_midi_events_by_device.clone();
7345 let mut reconfigured_tracks = Vec::new();
7346 for (track_name, track) in self.state.lock().tracks.iter() {
7347 let track_lock = track.lock();
7348 if self.jack_runtime_is_some() {
7349 if !self.pending_hw_midi_events.is_empty() {
7350 track_lock.push_hw_midi_events(&self.pending_hw_midi_events);
7351 }
7352 } else {
7353 for route in hw_in_routes.iter().filter(|r| &r.to_track == track_name) {
7354 if let Some(events) = pending_hw_in_by_device.get(&route.device) {
7355 track_lock.push_hw_midi_events_to_port(route.to_port, events);
7356 }
7357 }
7358 }
7359 if track_lock.setup() {
7360 reconfigured_tracks.push(track_name.clone());
7361 }
7362 }
7363 self.publish_track_meters().await;
7364 for track_name in reconfigured_tracks {
7365 let track = self.state.lock().tracks.get(&track_name).cloned();
7366 if let Some(track) = track {
7367 let (plugins, connections) = {
7368 let track_lock = track.lock();
7369 (
7370 track_lock.plugin_graph_plugins(),
7371 track_lock.plugin_graph_connections(),
7372 )
7373 };
7374 self.notify_clients(Ok(Action::TrackPluginGraph {
7375 track_name: track_name.clone(),
7376 plugins,
7377 connections,
7378 }))
7379 .await;
7380 }
7381 }
7382 self.pending_hw_midi_events.clear();
7383 self.pending_hw_midi_events_by_device.clear();
7384 if self.playing {
7385 if self.transport_panic_flush_pending {
7386 self.transport_panic_flush_pending = false;
7387 } else if self.transport_restart_pending {
7388 self.transport_restart_pending = false;
7389 } else {
7390 let next = self
7391 .transport_sample
7392 .saturating_add(self.current_cycle_samples());
7393 let normalized = self.normalize_transport_sample(next);
7394 let wrapped = normalized != next;
7395 self.transport_sample = normalized;
7396 if wrapped {
7397 self.notify_clients(Ok(Action::TransportPosition(
7398 self.transport_sample,
7399 )))
7400 .await;
7401 }
7402 }
7403 }
7404 if self.send_tracks().await && self.hw_worker.is_some() {
7405 self.request_hw_cycle().await;
7406 }
7407 #[cfg(unix)]
7408 {
7409 if self.jack_runtime.is_some() {
7410 self.awaiting_hwfinished = true;
7411 }
7412 }
7413 self.handling_hwfinished = false;
7414 }
7415 Message::HWMidiEvents(events) => {
7416 for hw_event in events {
7417 let thru_targets: Vec<String> = self
7418 .midi_hw_thru_routes
7419 .iter()
7420 .filter(|route| route.from_device == hw_event.device)
7421 .map(|route| route.to_device.clone())
7422 .collect();
7423 for device in thru_targets {
7424 self.pending_hw_midi_out_events_by_device.push(HwMidiEvent {
7425 device,
7426 event: hw_event.event.clone(),
7427 });
7428 }
7429 if hw_event.event.data.len() >= 3 {
7430 let status = hw_event.event.data[0];
7431 if status & 0xF0 == 0xB0 {
7432 let channel = status & 0x0F;
7433 let cc = hw_event.event.data[1];
7434 let value = hw_event.event.data[2];
7435 self.handle_incoming_hw_cc(&hw_event.device, channel, cc, value)
7436 .await;
7437 }
7438 }
7439 self.pending_hw_midi_events_by_device
7440 .entry(hw_event.device)
7441 .or_default()
7442 .push(hw_event.event);
7443 }
7444 }
7445 _ => {}
7446 }
7447 }
7448 }
7449
7450 fn collect_hw_midi_output_events(&self) -> Vec<MidiEvent> {
7451 let mut events = vec![];
7452 for track in self.state.lock().tracks.values() {
7453 events.extend(
7454 track
7455 .lock()
7456 .take_hw_midi_out_events()
7457 .into_iter()
7458 .map(|evt| evt.event),
7459 );
7460 }
7461 events.sort_by_key(|a| a.frame);
7462 events
7463 }
7464
7465 fn collect_hw_midi_output_events_by_device(&mut self) -> Vec<HwMidiEvent> {
7466 let mut events = Vec::<HwMidiEvent>::new();
7467 let routes = self.midi_hw_out_routes.clone();
7468 let mut events_by_track = HashMap::<String, Vec<crate::track::HwMidiOutEvent>>::new();
7469 {
7470 let state = self.state.lock();
7471 for route in &routes {
7472 if events_by_track.contains_key(&route.from_track) {
7473 continue;
7474 }
7475 let Some(track) = state.tracks.get(&route.from_track) else {
7476 continue;
7477 };
7478 events_by_track.insert(
7479 route.from_track.clone(),
7480 track.lock().take_hw_midi_out_events(),
7481 );
7482 }
7483 }
7484
7485 for route in routes {
7486 let Some(track_events) = events_by_track.get(&route.from_track) else {
7487 continue;
7488 };
7489 for hw_event in track_events
7490 .iter()
7491 .filter(|evt| evt.port == route.from_port)
7492 {
7493 self.update_active_hw_notes_for_track(
7494 &route.from_track,
7495 &route.device,
7496 &hw_event.event.data,
7497 );
7498 events.push(HwMidiEvent {
7499 device: route.device.clone(),
7500 event: hw_event.event.clone(),
7501 });
7502 }
7503 }
7504 events.sort_by(|a, b| {
7505 a.event
7506 .frame
7507 .cmp(&b.event.frame)
7508 .then_with(|| a.device.cmp(&b.device))
7509 });
7510 events
7511 }
7512}
7513
7514#[cfg(test)]
7515mod tests {
7516 use super::*;
7517 use crate::mutex::UnsafeMutex;
7518 use tokio::sync::mpsc::channel;
7519 use tokio::time::{Duration as TokioDuration, timeout};
7520
7521 #[test]
7522 #[cfg(unix)]
7523 fn jack_transport_sync_decision_starts_and_syncs_position_on_external_play() {
7524 let decision = Engine::jack_transport_sync_decision(false, 128, true, 256, 64);
7525
7526 assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Start));
7527 assert_eq!(decision.position_sync, Some(256));
7528 }
7529
7530 #[test]
7531 #[cfg(unix)]
7532 fn jack_transport_sync_decision_stops_and_syncs_position_on_external_stop() {
7533 let decision = Engine::jack_transport_sync_decision(true, 512, false, 96, 64);
7534
7535 assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Stop));
7536 assert_eq!(decision.position_sync, Some(96));
7537 }
7538
7539 #[test]
7540 #[cfg(unix)]
7541 fn jack_transport_sync_decision_ignores_small_rolling_drift() {
7542 let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1040, 64);
7543
7544 assert_eq!(decision.play_sync, None);
7545 assert_eq!(decision.position_sync, None);
7546 }
7547
7548 #[test]
7549 #[cfg(unix)]
7550 fn jack_transport_sync_decision_syncs_large_rolling_jump() {
7551 let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1200, 64);
7552
7553 assert_eq!(decision.play_sync, None);
7554 assert_eq!(decision.position_sync, Some(1200));
7555 }
7556
7557 #[test]
7558 #[cfg(unix)]
7559 fn jack_transport_sync_decision_syncs_locate_while_stopped() {
7560 let decision = Engine::jack_transport_sync_decision(false, 400, false, 900, 64);
7561
7562 assert_eq!(decision.play_sync, None);
7563 assert_eq!(decision.position_sync, Some(900));
7564 }
7565
7566 fn make_engine_with_client() -> (Engine, tokio::sync::mpsc::Receiver<Message>) {
7567 let (engine_tx, engine_rx) = channel(16);
7568 let mut engine = Engine::new(engine_rx, engine_tx);
7569 let (client_tx, client_rx) = channel(16);
7570 engine.clients.push(client_tx);
7571 (engine, client_rx)
7572 }
7573
7574 fn insert_track(engine: &mut Engine, track: Track) {
7575 engine.state.lock().tracks.insert(
7576 track.name.clone(),
7577 Arc::new(UnsafeMutex::new(Box::new(track))),
7578 );
7579 }
7580
7581 fn osc_packet(address: &str) -> Vec<u8> {
7582 fn push_padded_osc_string(packet: &mut Vec<u8>, value: &str) {
7583 packet.extend_from_slice(value.as_bytes());
7584 packet.push(0);
7585 while !packet.len().is_multiple_of(4) {
7586 packet.push(0);
7587 }
7588 }
7589
7590 let mut packet = Vec::new();
7591 push_padded_osc_string(&mut packet, address);
7592 push_padded_osc_string(&mut packet, ",");
7593 packet
7594 }
7595
7596 #[tokio::test]
7597 async fn set_osc_enabled_starts_and_stops_server() {
7598 let (mut engine, _client_rx) = make_engine_with_client();
7599
7600 engine
7601 .set_osc_enabled_with(true, |tx| OscServer::start_on_addr(tx, "127.0.0.1:0"))
7602 .expect("start osc server on ephemeral port");
7603 assert!(engine.osc_server.is_some());
7604
7605 engine
7606 .set_osc_enabled_with(false, OscServer::start)
7607 .expect("stop osc server");
7608 assert!(engine.osc_server.is_none());
7609 }
7610
7611 #[tokio::test]
7612 async fn osc_server_forwards_transport_packets_to_engine_channel() {
7613 let (tx, mut rx) = channel(4);
7614 let mut server =
7615 OscServer::start_on_addr(tx, "127.0.0.1:0").expect("start osc test server");
7616 let socket = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind sender socket");
7617 let packet = osc_packet("/transport/play");
7618 socket
7619 .send_to(&packet, server.listen_addr())
7620 .expect("send osc packet");
7621
7622 let message = timeout(TokioDuration::from_secs(1), rx.recv())
7623 .await
7624 .expect("packet delivery timeout")
7625 .expect("osc message");
7626 match message {
7627 Message::Request(Action::Play) => {}
7628 other => panic!("unexpected osc message: {other:?}"),
7629 }
7630
7631 server.stop();
7632 }
7633
7634 #[tokio::test]
7635 async fn track_offline_bounce_rejects_zero_length_requests() {
7636 let (mut engine, mut client_rx) = make_engine_with_client();
7637 insert_track(
7638 &mut engine,
7639 Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
7640 );
7641
7642 engine
7643 .handle_request(Action::TrackOfflineBounce {
7644 track_name: "track".to_string(),
7645 output_path: "/tmp/out.wav".to_string(),
7646 start_sample: 0,
7647 length_samples: 0,
7648 automation_lanes: vec![],
7649 apply_fader: false,
7650 })
7651 .await;
7652
7653 match client_rx.recv().await.expect("response") {
7654 Message::Response(Err(err)) => {
7655 assert!(err.contains("has no renderable content for offline bounce"));
7656 }
7657 other => panic!("unexpected message: {other:?}"),
7658 }
7659 }
7660
7661 #[tokio::test]
7662 async fn track_offline_bounce_rejects_when_same_track_is_active() {
7663 let (mut engine, mut client_rx) = make_engine_with_client();
7664 engine.offline_bounce_jobs.insert(
7665 "other".to_string(),
7666 OfflineBounceJob {
7667 cancel: Arc::new(AtomicBool::new(false)),
7668 },
7669 );
7670
7671 engine
7672 .handle_request(Action::TrackOfflineBounce {
7673 track_name: "other".to_string(),
7674 output_path: "/tmp/out.wav".to_string(),
7675 start_sample: 0,
7676 length_samples: 128,
7677 automation_lanes: vec![],
7678 apply_fader: false,
7679 })
7680 .await;
7681
7682 match client_rx.recv().await.expect("response") {
7683 Message::Response(Err(err)) => {
7684 assert!(err.contains("already in progress"));
7685 }
7686 other => panic!("unexpected message: {other:?}"),
7687 }
7688 }
7689
7690 #[tokio::test]
7691 async fn track_offline_bounce_allows_different_track_concurrently() {
7692 let (mut engine, _client_rx) = make_engine_with_client();
7693 insert_track(
7694 &mut engine,
7695 Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
7696 );
7697 engine.offline_bounce_jobs.insert(
7698 "other".to_string(),
7699 OfflineBounceJob {
7700 cancel: Arc::new(AtomicBool::new(false)),
7701 },
7702 );
7703
7704 engine
7705 .handle_request(Action::TrackOfflineBounce {
7706 track_name: "track".to_string(),
7707 output_path: "/tmp/out.wav".to_string(),
7708 start_sample: 0,
7709 length_samples: 128,
7710 automation_lanes: vec![],
7711 apply_fader: false,
7712 })
7713 .await;
7714
7715 assert!(engine.offline_bounce_jobs.contains_key("other"));
7716 assert_eq!(engine.pending_requests.len(), 1);
7717 }
7718
7719 #[tokio::test]
7720 async fn reject_if_track_frozen_sends_error_and_blocks_operation() {
7721 let (mut engine, mut client_rx) = make_engine_with_client();
7722 let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
7723 track.set_frozen(true);
7724 insert_track(&mut engine, track);
7725
7726 let rejected = engine
7727 .reject_if_track_frozen("track", "arming/disarming")
7728 .await;
7729
7730 assert!(rejected);
7731 match client_rx.recv().await.expect("response") {
7732 Message::Response(Err(err)) => {
7733 assert_eq!(err, "Track 'track' is frozen; arming/disarming is blocked");
7734 }
7735 other => panic!("unexpected message: {other:?}"),
7736 }
7737 }
7738
7739 #[tokio::test]
7740 async fn undo_restores_original_clip_bounds_after_stretch_style_group() {
7741 let (mut engine, _client_rx) = make_engine_with_client();
7742 let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
7743 let mut clip = AudioClip::new("audio/original.wav".to_string(), 100, 220);
7744 clip.offset = 12;
7745 clip.fade_in_samples = 20;
7746 clip.fade_out_samples = 30;
7747 track.audio.clips.push(clip);
7748 insert_track(&mut engine, track);
7749
7750 engine.handle_request(Action::BeginHistoryGroup).await;
7751 engine
7752 .handle_request(Action::SetClipBounds {
7753 track_name: "track".to_string(),
7754 clip_index: 0,
7755 kind: Kind::Audio,
7756 start: 120,
7757 length: 180,
7758 offset: 0,
7759 })
7760 .await;
7761 engine
7762 .handle_request(Action::SetClipSourceName {
7763 track_name: "track".to_string(),
7764 clip_index: 0,
7765 kind: Kind::Audio,
7766 name: "audio/stretched.wav".to_string(),
7767 })
7768 .await;
7769 engine
7770 .handle_request(Action::SetClipFade {
7771 track_name: "track".to_string(),
7772 clip_index: 0,
7773 kind: Kind::Audio,
7774 fade_enabled: true,
7775 fade_in_samples: 12,
7776 fade_out_samples: 12,
7777 })
7778 .await;
7779 engine.handle_request(Action::EndHistoryGroup).await;
7780
7781 engine.handle_request(Action::Undo).await;
7782
7783 let state = engine.state.lock();
7784 let track = state.tracks.get("track").expect("track exists").lock();
7785 let clip = track.audio.clips.first().expect("clip exists");
7786 assert_eq!(clip.name, "audio/original.wav");
7787 assert_eq!(clip.start, 100);
7788 assert_eq!(clip.end, 220);
7789 assert_eq!(clip.end.saturating_sub(clip.start), 120);
7790 assert_eq!(clip.offset, 12);
7791 }
7792
7793 #[tokio::test]
7794 async fn track_offline_bounce_queues_when_no_worker_is_ready() {
7795 let (mut engine, _client_rx) = make_engine_with_client();
7796 insert_track(
7797 &mut engine,
7798 Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
7799 );
7800
7801 engine
7802 .handle_request(Action::TrackOfflineBounce {
7803 track_name: "track".to_string(),
7804 output_path: "/tmp/out.wav".to_string(),
7805 start_sample: 0,
7806 length_samples: 128,
7807 automation_lanes: vec![],
7808 apply_fader: false,
7809 })
7810 .await;
7811
7812 assert!(engine.offline_bounce_jobs.is_empty());
7813 assert_eq!(engine.pending_requests.len(), 1);
7814 assert!(matches!(
7815 engine.pending_requests.front(),
7816 Some(Action::TrackOfflineBounce { track_name, length_samples, .. })
7817 if track_name == "track" && *length_samples == 128
7818 ));
7819 }
7820
7821 #[tokio::test]
7822 async fn track_offline_bounce_returns_missing_track_error() {
7823 let (mut engine, mut client_rx) = make_engine_with_client();
7824
7825 engine
7826 .handle_request(Action::TrackOfflineBounce {
7827 track_name: "missing".to_string(),
7828 output_path: "/tmp/out.wav".to_string(),
7829 start_sample: 0,
7830 length_samples: 128,
7831 automation_lanes: vec![],
7832 apply_fader: false,
7833 })
7834 .await;
7835
7836 match client_rx.recv().await.expect("response") {
7837 Message::Response(Err(err)) => {
7838 assert_eq!(err, "Track not found: missing");
7839 }
7840 other => panic!("unexpected message: {other:?}"),
7841 }
7842 }
7843
7844 #[tokio::test]
7845 async fn track_offline_bounce_clears_job_when_worker_send_fails() {
7846 let (mut engine, mut client_rx) = make_engine_with_client();
7847 insert_track(
7848 &mut engine,
7849 Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
7850 );
7851 let (worker_tx, worker_rx) = channel(1);
7852 drop(worker_rx);
7853 engine
7854 .workers
7855 .push(WorkerData::new(worker_tx, tokio::spawn(async {})));
7856 engine.ready_refill_workers.push(0);
7857
7858 engine
7859 .handle_request(Action::TrackOfflineBounce {
7860 track_name: "track".to_string(),
7861 output_path: "/tmp/out.wav".to_string(),
7862 start_sample: 0,
7863 length_samples: 128,
7864 automation_lanes: vec![],
7865 apply_fader: false,
7866 })
7867 .await;
7868
7869 assert!(engine.offline_bounce_jobs.is_empty());
7870 match client_rx.recv().await.expect("response") {
7871 Message::Response(Err(err)) => {
7872 assert!(err.contains("Failed to schedule offline bounce"));
7873 }
7874 other => panic!("unexpected message: {other:?}"),
7875 }
7876 }
7877}