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