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 => {
4094 if self.history_group.is_none() {
4095 self.history_group = Some(UndoEntry {
4096 forward_actions: vec![],
4097 inverse_actions: vec![],
4098 });
4099 }
4100 }
4101 Action::EndHistoryGroup => {
4102 if let Some(mut group) = self.history_group.take()
4103 && !group.forward_actions.is_empty()
4104 && !group.inverse_actions.is_empty()
4105 {
4106 let mut add_tracks = Vec::new();
4107 let mut connections = Vec::new();
4108 let mut rest = Vec::new();
4109 for action in group.inverse_actions {
4110 if matches!(action, Action::AddTrack { .. }) {
4111 add_tracks.push(action);
4112 } else if matches!(action, Action::Connect { .. }) {
4113 connections.push(action);
4114 } else {
4115 rest.push(action);
4116 }
4117 }
4118 group.inverse_actions = add_tracks;
4119 group.inverse_actions.extend(rest);
4120 group.inverse_actions.extend(connections);
4121 self.history.record(group);
4122 }
4123 }
4124 Action::SetSessionPath(ref path) => {
4125 self.session_dir = Some(Path::new(path).to_path_buf());
4126 self.ensure_session_subdirs();
4127 #[cfg(all(unix, not(target_os = "macos")))]
4128 let lv2_dir = self.session_plugins_dir();
4129 for track in self.state.lock().tracks.values() {
4130 #[cfg(all(unix, not(target_os = "macos")))]
4131 track.lock().set_lv2_state_base_dir(lv2_dir.clone());
4132 track.lock().set_session_base_dir(self.session_dir.clone());
4133 }
4134 }
4135 Action::MarkHistorySavePoint => {
4136 self.history.mark_save_point();
4137 self.notify_clients(Ok(Action::HistoryState {
4138 dirty: self.history.is_dirty(),
4139 }))
4140 .await;
4141 }
4142 Action::ClearHistory => {
4143 self.history.clear();
4144 self.history.mark_save_point();
4145 }
4146 Action::BeginSessionRestore => {
4147 self.history_suspended = true;
4148 self.history.clear();
4149 }
4150 Action::EndSessionRestore => {
4151 self.history.clear();
4152 self.history_suspended = false;
4153 }
4154 Action::Quit => {
4155 self.flush_recordings().await;
4156 self.ready_workers.clear();
4157 while !self.workers.is_empty() {
4158 let worker = self.workers.remove(0);
4159 worker
4160 .tx
4161 .send(Message::Request(a.clone()))
4162 .await
4163 .expect("Failed sending quit message to worker");
4164 worker
4165 .handle
4166 .await
4167 .expect("Failed waiting for worker to quit");
4168 }
4169
4170 if let Some(worker) = self.hw_worker.take() {
4171 let mut panic_events = self.note_off_events_for_all_active_tracks();
4172 panic_events.extend(self.panic_events_for_all_hw_midi_outputs());
4173 if !panic_events.is_empty() {
4174 if let Err(e) = worker.tx.send(Message::ClearHWMidiOutEvents).await {
4175 error!("Error clearing HW MIDI queue during quit {e}");
4176 }
4177 self.midi_hub
4178 .lock()
4179 .write_events_blocking(&panic_events, Duration::from_millis(250));
4180 }
4181 worker
4182 .tx
4183 .send(Message::Request(a.clone()))
4184 .await
4185 .expect("Failed sending quit message to HW worker");
4186 worker
4187 .handle
4188 .await
4189 .expect("Failed waiting for HW worker to quit");
4190 }
4191 #[cfg(unix)]
4192 {
4193 self.jack_runtime = None;
4194 }
4195 }
4196 Action::AddTrack {
4197 ref name,
4198 audio_ins,
4199 midi_ins,
4200 audio_outs,
4201 midi_outs,
4202 } => {
4203 let tracks = &mut self.state.lock().tracks;
4204 if tracks.contains_key(name) {
4205 self.notify_clients(Err(format!("Track {} already exists", name)))
4206 .await;
4207 return;
4208 }
4209 let maybe_hw = if let Some(oss) = &self.hw_driver {
4210 let hw = oss.lock();
4211 Some((hw.cycle_samples(), hw.sample_rate() as f64))
4212 } else {
4213 #[cfg(unix)]
4214 if let Some(jack) = &self.jack_runtime {
4215 let j = jack.lock();
4216 Some((j.buffer_size, j.sample_rate as f64))
4217 } else {
4218 None
4219 }
4220 #[cfg(not(unix))]
4221 None
4222 };
4223
4224 if let Some((chsamples, sample_rate)) = maybe_hw {
4225 tracks.insert(
4226 name.clone(),
4227 Arc::new(UnsafeMutex::new(Box::new(Track::new(
4228 name.clone(),
4229 audio_ins,
4230 audio_outs,
4231 midi_ins,
4232 midi_outs,
4233 chsamples,
4234 sample_rate,
4235 )))),
4236 );
4237 if let Some(track) = tracks.get(name) {
4238 track.lock().ensure_default_audio_passthrough();
4239 track.lock().ensure_default_midi_passthrough();
4240 track
4241 .lock()
4242 .set_clip_playback_enabled(self.clip_playback_enabled);
4243 track.lock().set_transport_timing(
4244 self.tempo_bpm,
4245 self.tsig_num,
4246 self.tsig_denom,
4247 );
4248 #[cfg(all(unix, not(target_os = "macos")))]
4249 {
4250 let lv2_dir = self.session_plugins_dir();
4251 track.lock().set_lv2_state_base_dir(lv2_dir);
4252 }
4253 track.lock().set_session_base_dir(self.session_dir.clone());
4254 }
4255 } else {
4256 self.notify_clients(Err(
4257 "Engine needs to open audio device before adding audio track".to_string(),
4258 ))
4259 .await;
4260 }
4261 }
4262 Action::TrackAddAudioInput(ref name) => {
4263 let track = match self.track_handle_or_err(name) {
4264 Ok(track) => track,
4265 Err(e) => {
4266 self.notify_clients(Err(e)).await;
4267 return;
4268 }
4269 };
4270 if let Err(e) = track.lock().add_audio_input() {
4271 self.notify_clients(Err(e)).await;
4272 return;
4273 }
4274 }
4275 Action::TrackAddAudioOutput(ref name) => {
4276 let track = match self.track_handle_or_err(name) {
4277 Ok(track) => track,
4278 Err(e) => {
4279 self.notify_clients(Err(e)).await;
4280 return;
4281 }
4282 };
4283 if let Err(e) = track.lock().add_audio_output() {
4284 self.notify_clients(Err(e)).await;
4285 return;
4286 }
4287 }
4288 Action::TrackRemoveAudioInput(ref name) => {
4289 let track = match self.track_handle_or_err(name) {
4290 Ok(track) => track,
4291 Err(e) => {
4292 self.notify_clients(Err(e)).await;
4293 return;
4294 }
4295 };
4296 if let Err(e) = track.lock().remove_audio_input() {
4297 self.notify_clients(Err(e)).await;
4298 return;
4299 }
4300 }
4301 Action::TrackRemoveAudioOutput(ref name) => {
4302 let track = match self.track_handle_or_err(name) {
4303 Ok(track) => track,
4304 Err(e) => {
4305 self.notify_clients(Err(e)).await;
4306 return;
4307 }
4308 };
4309 let (hw_outputs, track_inputs) = {
4310 let state = self.state.lock();
4311 let hw_outputs = self.all_hw_output_audio_ports();
4312 let track_inputs = state
4313 .tracks
4314 .iter()
4315 .filter(|(track_name, _)| *track_name != name)
4316 .flat_map(|(_, handle)| handle.lock().audio.ins.clone())
4317 .collect::<Vec<_>>();
4318 (hw_outputs, track_inputs)
4319 };
4320 if let Err(e) = track.lock().remove_audio_output(&hw_outputs, &track_inputs) {
4321 self.notify_clients(Err(e)).await;
4322 return;
4323 }
4324 }
4325 Action::RenameTrack {
4326 ref old_name,
4327 ref new_name,
4328 } => {
4329 if self.state.lock().tracks.contains_key(new_name) {
4330 self.notify_clients(Err(format!("Track '{}' already exists", new_name)))
4331 .await;
4332 return;
4333 }
4334
4335 let Some(track) = self.state.lock().tracks.remove(old_name) else {
4336 self.notify_clients(Err(format!("Track '{}' not found", old_name)))
4337 .await;
4338 return;
4339 };
4340
4341 track.lock().name = new_name.clone();
4342 self.state.lock().tracks.insert(new_name.clone(), track);
4343 for other in self.state.lock().tracks.values() {
4344 let other = other.lock();
4345 if other.vca_master.as_deref() == Some(old_name.as_str()) {
4346 other.set_vca_master(Some(new_name.clone()));
4347 }
4348 }
4349
4350 if let Some(recording) = self.audio_recordings.remove(old_name) {
4351 self.audio_recordings.insert(new_name.clone(), recording);
4352 }
4353 if let Some(recording) = self.midi_recordings.remove(old_name) {
4354 self.midi_recordings.insert(new_name.clone(), recording);
4355 }
4356
4357 for route in &mut self.midi_hw_in_routes {
4358 if route.to_track == *old_name {
4359 route.to_track = new_name.clone();
4360 }
4361 }
4362 for route in &mut self.midi_hw_out_routes {
4363 if route.from_track == *old_name {
4364 route.from_track = new_name.clone();
4365 }
4366 }
4367 if let Some((armed_track, target, device)) = self.pending_midi_learn.clone()
4368 && armed_track == *old_name
4369 {
4370 self.pending_midi_learn = Some((new_name.clone(), target, device));
4371 }
4372
4373 self.notify_clients(Ok(Action::RenameTrack {
4374 old_name: old_name.clone(),
4375 new_name: new_name.clone(),
4376 }))
4377 .await;
4378 }
4379 Action::RemoveTrack(ref name) => {
4380 self.state.lock().tracks.remove(name);
4381 self.audio_recordings.remove(name);
4382 self.midi_recordings.remove(name);
4383 self.midi_hw_in_routes.retain(|r| r.to_track != *name);
4384 self.midi_hw_out_routes.retain(|r| r.from_track != *name);
4385 if self
4386 .pending_midi_learn
4387 .as_ref()
4388 .is_some_and(|(track_name, _, _)| track_name == name)
4389 {
4390 self.pending_midi_learn = None;
4391 }
4392 for track in self.state.lock().tracks.values() {
4393 let track = track.lock();
4394 if track.vca_master.as_deref() == Some(name.as_str()) {
4395 track.set_vca_master(None);
4396 }
4397 }
4398 }
4399 Action::TrackLevel(ref name, level) => {
4400 if name == "hw:out" {
4401 self.hw_out_level_db = level;
4402 } else if let Some(track) = self.state.lock().tracks.get(name) {
4403 let previous = track.lock().level();
4404 track.lock().set_level(level);
4405 let delta = level - previous;
4406 if delta.abs() > f32::EPSILON {
4407 for follower_name in self.vca_followers(name) {
4408 if let Some(follower) = self.state.lock().tracks.get(&follower_name) {
4409 let next = (follower.lock().level() + delta).clamp(-90.0, 20.0);
4410 follower.lock().set_level(next);
4411 self.notify_clients(Ok(Action::TrackLevel(
4412 follower_name.clone(),
4413 next,
4414 )))
4415 .await;
4416 }
4417 }
4418 }
4419 }
4420 }
4421 Action::TrackBalance(ref name, balance) => {
4422 if name == "hw:out" {
4423 self.hw_out_balance = balance.clamp(-1.0, 1.0);
4424 } else if let Some(track) = self.state.lock().tracks.get(name) {
4425 track.lock().set_balance(balance);
4426 }
4427 }
4428 Action::TrackAutomationLevel(ref name, level) => {
4429 if let Some(track) = self.state.lock().tracks.get(name) {
4430 let previous = track.lock().level();
4431 track.lock().set_level(level);
4432 let delta = level - previous;
4433 if delta.abs() > f32::EPSILON {
4434 for follower_name in self.vca_followers(name) {
4435 if let Some(follower) = self.state.lock().tracks.get(&follower_name) {
4436 let next = (follower.lock().level() + delta).clamp(-90.0, 20.0);
4437 follower.lock().set_level(next);
4438 self.notify_clients(Ok(Action::TrackAutomationLevel(
4439 follower_name.clone(),
4440 next,
4441 )))
4442 .await;
4443 }
4444 }
4445 }
4446 }
4447 }
4448 Action::TrackAutomationBalance(ref name, balance) => {
4449 if let Some(track) = self.state.lock().tracks.get(name) {
4450 track.lock().set_balance(balance);
4451 }
4452 }
4453 Action::TrackAutomationMute(ref name, muted) => {
4454 if let Some(track) = self.state.lock().tracks.get(name) {
4455 track.lock().set_muted(muted);
4456 for follower_name in self.vca_followers(name) {
4457 if let Some(follower) = self.state.lock().tracks.get(&follower_name) {
4458 follower.lock().set_muted(muted);
4459 self.notify_clients(Ok(Action::TrackAutomationMute(
4460 follower_name.clone(),
4461 muted,
4462 )))
4463 .await;
4464 }
4465 }
4466 }
4467 }
4468 Action::RequestMeterSnapshot => {
4469 self.notify_clients(Ok(Action::MeterSnapshot {
4470 hw_out_db: self.latest_hw_out_meter_db.clone(),
4471 track_meters: self.latest_track_meter_snapshot.clone(),
4472 }))
4473 .await;
4474 return;
4475 }
4476 Action::TrackMeters { .. } => {}
4477 Action::MeterSnapshot { .. } => {}
4478 Action::TrackToggleArm(ref name) => {
4479 if self.reject_if_track_frozen(name, "arming/disarming").await {
4480 return;
4481 }
4482 if let Some(track) = self.state.lock().tracks.get(name).cloned() {
4483 track.lock().arm();
4484 if !track.lock().armed && self.audio_recordings.contains_key(name) {
4485 self.flush_track_recording(name).await;
4486 }
4487 }
4488 }
4489 Action::TrackToggleMute(ref name) => {
4490 if name == "hw:out" {
4491 self.hw_out_muted = !self.hw_out_muted;
4492 } else if let Some(track) = self.state.lock().tracks.get(name) {
4493 track.lock().mute();
4494 let muted = track.lock().muted;
4495 for follower_name in self.vca_followers(name) {
4496 if let Some(follower) = self.state.lock().tracks.get(&follower_name)
4497 && follower.lock().muted != muted
4498 {
4499 follower.lock().set_muted(muted);
4500 self.notify_clients(Ok(Action::TrackToggleMute(follower_name.clone())))
4501 .await;
4502 }
4503 }
4504 }
4505 }
4506 Action::TrackTogglePhase(ref name) => {
4507 if let Some(track) = self.state.lock().tracks.get(name) {
4508 track.lock().invert_phase();
4509 }
4510 }
4511 Action::TrackToggleSolo(ref name) => {
4512 if name == "hw:out" {
4513 return;
4514 }
4515 if let Some(track) = self.state.lock().tracks.get(name) {
4516 track.lock().solo();
4517 let soloed = track.lock().soloed;
4518 for follower_name in self.vca_followers(name) {
4519 if let Some(follower) = self.state.lock().tracks.get(&follower_name)
4520 && follower.lock().soloed != soloed
4521 {
4522 follower.lock().solo();
4523 self.notify_clients(Ok(Action::TrackToggleSolo(follower_name.clone())))
4524 .await;
4525 }
4526 }
4527 }
4528 }
4529 Action::TrackToggleMaster(ref name) => {
4530 if let Some(track) = self.state.lock().tracks.get(name) {
4531 let blocked = {
4532 let t = track.lock();
4533 t.vca_master.is_some() || !self.vca_followers(name).is_empty()
4534 };
4535 if blocked {
4536 self.notify_clients(Err(format!(
4537 "Track '{}' cannot be promoted to Master while part of a VCA group",
4538 name
4539 )))
4540 .await;
4541 return;
4542 }
4543 track.lock().toggle_master();
4544 }
4545 }
4546 Action::TrackToggleInputMonitor(ref name) => {
4547 if let Some(track) = self.state.lock().tracks.get(name) {
4548 track.lock().toggle_input_monitor();
4549 }
4550 }
4551 Action::TrackToggleDiskMonitor(ref name) => {
4552 if let Some(track) = self.state.lock().tracks.get(name) {
4553 track.lock().toggle_disk_monitor();
4554 }
4555 }
4556 Action::TrackSetColor {
4557 ref track_name,
4558 color,
4559 } => {
4560 if let Some(track) = self.state.lock().tracks.get(track_name) {
4561 track.lock().color = color;
4562 }
4563 }
4564 Action::TrackArmMidiLearn {
4565 ref track_name,
4566 target,
4567 } => {
4568 if let Err(e) = self.track_handle_or_err(track_name) {
4569 self.notify_clients(Err(e)).await;
4570 return;
4571 }
4572 self.pending_midi_learn = Some((track_name.clone(), target, None));
4573 }
4574 Action::GlobalArmMidiLearn { target } => {
4575 self.pending_global_midi_learn = Some(target);
4576 }
4577 Action::TrackSetMidiLearnBinding {
4578 ref track_name,
4579 target,
4580 ref binding,
4581 } => {
4582 if let Some(binding) = binding.as_ref() {
4583 let conflicts = self.midi_learn_slot_conflicts(
4584 binding,
4585 Some(MidiLearnSlot::Track(track_name.clone(), target)),
4586 );
4587 if !conflicts.is_empty() {
4588 self.notify_clients(Err(format!(
4589 "MIDI learn conflict for '{}' {:?}: {}",
4590 track_name,
4591 target,
4592 conflicts.join(", ")
4593 )))
4594 .await;
4595 return;
4596 }
4597 }
4598 let track = match self.track_handle_or_err(track_name) {
4599 Ok(track) => track,
4600 Err(e) => {
4601 self.notify_clients(Err(e)).await;
4602 return;
4603 }
4604 };
4605 match target {
4606 crate::message::TrackMidiLearnTarget::Volume => {
4607 track.lock().midi_learn_volume = binding.clone();
4608 }
4609 crate::message::TrackMidiLearnTarget::Balance => {
4610 track.lock().midi_learn_balance = binding.clone();
4611 }
4612 crate::message::TrackMidiLearnTarget::Mute => {
4613 track.lock().midi_learn_mute = binding.clone();
4614 }
4615 crate::message::TrackMidiLearnTarget::Solo => {
4616 track.lock().midi_learn_solo = binding.clone();
4617 }
4618 crate::message::TrackMidiLearnTarget::Arm => {
4619 track.lock().midi_learn_arm = binding.clone();
4620 }
4621 crate::message::TrackMidiLearnTarget::InputMonitor => {
4622 track.lock().midi_learn_input_monitor = binding.clone();
4623 }
4624 crate::message::TrackMidiLearnTarget::DiskMonitor => {
4625 track.lock().midi_learn_disk_monitor = binding.clone();
4626 }
4627 }
4628 }
4629 Action::SetGlobalMidiLearnBinding {
4630 target,
4631 ref binding,
4632 } => {
4633 if let Some(binding) = binding.as_ref() {
4634 let conflicts = self
4635 .midi_learn_slot_conflicts(binding, Some(MidiLearnSlot::Global(target)));
4636 if !conflicts.is_empty() {
4637 self.notify_clients(Err(format!(
4638 "Global MIDI learn conflict for {:?}: {}",
4639 target,
4640 conflicts.join(", ")
4641 )))
4642 .await;
4643 return;
4644 }
4645 }
4646 match target {
4647 crate::message::GlobalMidiLearnTarget::PlayPause => {
4648 self.global_midi_learn_play_pause = binding.clone();
4649 }
4650 crate::message::GlobalMidiLearnTarget::Stop => {
4651 self.global_midi_learn_stop = binding.clone();
4652 }
4653 crate::message::GlobalMidiLearnTarget::RecordToggle => {
4654 self.global_midi_learn_record_toggle = binding.clone();
4655 }
4656 }
4657 }
4658 Action::TrackSetVcaMaster {
4659 ref track_name,
4660 ref master_track,
4661 } => {
4662 let track = match self.track_handle_or_err(track_name) {
4663 Ok(track) => track,
4664 Err(e) => {
4665 self.notify_clients(Err(e)).await;
4666 return;
4667 }
4668 };
4669 if track.lock().is_master {
4670 self.notify_clients(Err(format!(
4671 "Master track '{}' cannot be part of a VCA group",
4672 track_name
4673 )))
4674 .await;
4675 return;
4676 }
4677 if let Some(master_name) = master_track
4678 && master_name == track_name
4679 {
4680 self.notify_clients(Err("Track cannot be its own VCA master".to_string()))
4681 .await;
4682 return;
4683 }
4684 if let Some(master_name) = master_track
4685 && let Some(master) = self.state.lock().tracks.get(master_name)
4686 && master.lock().is_master
4687 {
4688 self.notify_clients(Err(format!(
4689 "Track '{}' cannot be grouped to Master track '{}'",
4690 track_name, master_name
4691 )))
4692 .await;
4693 return;
4694 }
4695 track.lock().set_vca_master(master_track.clone());
4696 }
4697 Action::TrackSetMidiLaneChannel {
4698 ref track_name,
4699 lane,
4700 channel,
4701 } => {
4702 let track = match self.track_handle_or_err(track_name) {
4703 Ok(track) => track,
4704 Err(e) => {
4705 self.notify_clients(Err(e)).await;
4706 return;
4707 }
4708 };
4709 track.lock().set_midi_lane_channel(lane, channel);
4710 }
4711 Action::TrackSetFrozen {
4712 ref track_name,
4713 frozen,
4714 } => {
4715 let track = match self.track_handle_or_err(track_name) {
4716 Ok(track) => track,
4717 Err(e) => {
4718 self.notify_clients(Err(e)).await;
4719 return;
4720 }
4721 };
4722 track.lock().set_frozen(frozen);
4723 }
4724 Action::TrackOfflineBounce {
4725 track_name,
4726 output_path,
4727 start_sample,
4728 length_samples,
4729 automation_lanes,
4730 apply_fader,
4731 } => {
4732 if self.offline_bounce_jobs.contains_key(&track_name) {
4733 self.notify_clients(Err(format!(
4734 "Offline bounce for track '{}' is already in progress",
4735 track_name
4736 )))
4737 .await;
4738 return;
4739 }
4740 if let Err(e) = self.track_handle_or_err(&track_name) {
4741 self.notify_clients(Err(e)).await;
4742 return;
4743 }
4744 if length_samples == 0 {
4745 self.notify_clients(Err(format!(
4746 "Track '{}' has no renderable content for offline bounce",
4747 track_name
4748 )))
4749 .await;
4750 return;
4751 }
4752 let Some(worker_index) = self.take_ready_worker_index() else {
4753 self.pending_requests
4754 .push_front(Action::TrackOfflineBounce {
4755 track_name,
4756 output_path,
4757 start_sample,
4758 length_samples,
4759 automation_lanes,
4760 apply_fader,
4761 });
4762 return;
4763 };
4764 let cancel = Arc::new(AtomicBool::new(false));
4765 self.offline_bounce_jobs.insert(
4766 track_name.clone(),
4767 OfflineBounceJob {
4768 cancel: cancel.clone(),
4769 },
4770 );
4771 let track_name_clone = track_name.clone();
4772 let worker = &self.workers[worker_index];
4773 let job = crate::message::OfflineBounceWork {
4774 state: self.state.clone(),
4775 track_name,
4776 output_path,
4777 start_sample,
4778 length_samples,
4779 tempo_bpm: self.tempo_bpm,
4780 tsig_num: self.tsig_num,
4781 tsig_denom: self.tsig_denom,
4782 automation_lanes,
4783 cancel,
4784 apply_fader,
4785 };
4786 if let Err(e) = worker.tx.send(Message::ProcessOfflineBounce(job)).await {
4787 self.offline_bounce_jobs.remove(&track_name_clone);
4788 self.notify_clients(Err(format!("Failed to schedule offline bounce: {e}")))
4789 .await;
4790 }
4791 return;
4792 }
4793 Action::TrackOfflineBounceCancel { .. } => {}
4794 Action::TrackOfflineBounceCancelAll => {}
4795 Action::TrackOfflineBounceCanceled { .. } => {}
4796 Action::TrackOfflineBounceProgress { .. } => {}
4797 Action::PianoKey {
4798 ref track_name,
4799 note,
4800 velocity,
4801 on,
4802 } => {
4803 if let Some(track) = self.state.lock().tracks.get(track_name) {
4804 let status = if on { 0x90 } else { 0x80 };
4805 let event = MidiEvent::new(0, vec![status, note.min(127), velocity.min(127)]);
4806 track.lock().push_hw_midi_events(&[event]);
4807 }
4808 }
4809 Action::ModifyMidiNotes { .. }
4810 | Action::ModifyMidiControllers { .. }
4811 | Action::DeleteMidiControllers { .. }
4812 | Action::InsertMidiControllers { .. }
4813 | Action::DeleteMidiNotes { .. }
4814 | Action::InsertMidiNotes { .. } => {
4815 if let Err(e) = self.apply_midi_edit_action(&action_to_process) {
4816 self.notify_clients(Err(e)).await;
4817 return;
4818 }
4819 }
4820 Action::SetMidiSysExEvents { .. } => {
4821 if let Err(e) = self.apply_midi_edit_action(&action_to_process) {
4822 self.notify_clients(Err(e)).await;
4823 return;
4824 }
4825 }
4826 #[cfg(all(unix, not(target_os = "macos")))]
4827 Action::TrackLoadLv2Plugin {
4828 ref track_name,
4829 ref plugin_uri,
4830 instance_id,
4831 } => {
4832 if self
4833 .reject_if_track_frozen(track_name, "LV2 plugin loading")
4834 .await
4835 {
4836 return;
4837 }
4838 let track = match self.track_handle_or_err(track_name) {
4839 Ok(track) => track,
4840 Err(e) => {
4841 self.notify_clients(Err(e)).await;
4842 return;
4843 }
4844 };
4845 if let Err(e) = track.lock().load_lv2_plugin(plugin_uri, instance_id) {
4846 self.notify_clients(Err(e)).await;
4847 return;
4848 }
4849 }
4850 Action::TrackClearDefaultPassthrough { ref track_name } => {
4851 if self
4852 .reject_if_track_frozen(track_name, "plugin graph editing")
4853 .await
4854 {
4855 return;
4856 }
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 track.lock().clear_default_passthrough();
4865 }
4866 #[cfg(all(unix, not(target_os = "macos")))]
4867 Action::TrackSetLv2PluginState {
4868 ref track_name,
4869 instance_id,
4870 ref state,
4871 } => {
4872 if self
4873 .reject_if_track_frozen(track_name, "LV2 plugin state changes")
4874 .await
4875 {
4876 return;
4877 }
4878 let track = match self.track_handle_or_err(track_name) {
4879 Ok(track) => track,
4880 Err(e) => {
4881 self.notify_clients(Err(e)).await;
4882 return;
4883 }
4884 };
4885 if let Err(e) = track
4886 .lock()
4887 .set_lv2_plugin_state(instance_id, state.clone())
4888 {
4889 self.notify_clients(Err(e)).await;
4890 return;
4891 }
4892 }
4893 #[cfg(all(unix, not(target_os = "macos")))]
4894 Action::ClipSetLv2PluginState {
4895 ref track_name,
4896 clip_idx,
4897 instance_id,
4898 ref state,
4899 } => {
4900 if self
4901 .reject_if_track_frozen(track_name, "LV2 plugin state changes")
4902 .await
4903 {
4904 return;
4905 }
4906 let track = match self.track_handle_or_err(track_name) {
4907 Ok(track) => track,
4908 Err(e) => {
4909 self.notify_clients(Err(e)).await;
4910 return;
4911 }
4912 };
4913 if let Err(e) =
4914 track
4915 .lock()
4916 .clip_set_lv2_plugin_state(clip_idx, instance_id, state.clone())
4917 {
4918 self.notify_clients(Err(e)).await;
4919 return;
4920 }
4921 self.notify_clients(Ok(Action::ClipLv2StateSnapshot {
4922 track_name: track_name.clone(),
4923 clip_idx,
4924 instance_id,
4925 state: state.clone(),
4926 }))
4927 .await;
4928 }
4929 #[cfg(all(unix, not(target_os = "macos")))]
4930 Action::TrackUnloadLv2PluginInstance {
4931 ref track_name,
4932 instance_id,
4933 } => {
4934 if self
4935 .reject_if_track_frozen(track_name, "LV2 plugin unloading")
4936 .await
4937 {
4938 return;
4939 }
4940 let track = match self.track_handle_or_err(track_name) {
4941 Ok(track) => track,
4942 Err(e) => {
4943 self.notify_clients(Err(e)).await;
4944 return;
4945 }
4946 };
4947 if let Err(e) = track.lock().unload_lv2_plugin_instance(instance_id) {
4948 self.notify_clients(Err(e)).await;
4949 return;
4950 }
4951 }
4952 #[cfg(all(unix, not(target_os = "macos")))]
4953 Action::TrackGetLv2Midnam { ref track_name } => {
4954 let track = match self.track_handle_or_err(track_name) {
4955 Ok(track) => track,
4956 Err(e) => {
4957 self.notify_clients(Err(e)).await;
4958 return;
4959 }
4960 };
4961 let note_names = track.lock().get_lv2_midnam();
4962 self.notify_clients(Ok(Action::TrackLv2Midnam {
4963 track_name: track_name.clone(),
4964 note_names,
4965 }))
4966 .await;
4967 }
4968 Action::TrackGetClapNoteNames { ref track_name } => {
4969 let track = match self.track_handle_or_err(track_name) {
4970 Ok(track) => track,
4971 Err(e) => {
4972 self.notify_clients(Err(e)).await;
4973 return;
4974 }
4975 };
4976 let note_names = track.lock().get_clap_note_names();
4977 self.notify_clients(Ok(Action::TrackClapNoteNames {
4978 track_name: track_name.clone(),
4979 note_names,
4980 }))
4981 .await;
4982 }
4983 #[cfg(all(unix, not(target_os = "macos")))]
4984 Action::TrackGetLv2PluginControls {
4985 ref track_name,
4986 instance_id,
4987 } => {
4988 let track = match self.track_handle_or_err(track_name) {
4989 Ok(track) => track,
4990 Err(e) => {
4991 self.notify_clients(Err(e)).await;
4992 return;
4993 }
4994 };
4995 let (controls, instance_access_handle) =
4996 match track.lock().lv2_plugin_controls(instance_id) {
4997 Ok(result) => result,
4998 Err(e) => {
4999 self.notify_clients(Err(e)).await;
5000 return;
5001 }
5002 };
5003 self.notify_clients(Ok(Action::TrackLv2PluginControls {
5004 track_name: track_name.clone(),
5005 instance_id,
5006 controls,
5007 instance_access_handle,
5008 }))
5009 .await;
5010 return;
5011 }
5012 #[cfg(all(unix, not(target_os = "macos")))]
5013 Action::ClipGetLv2PluginControls {
5014 ref track_name,
5015 clip_idx,
5016 instance_id,
5017 } => {
5018 let track = match self.track_handle_or_err(track_name) {
5019 Ok(track) => track,
5020 Err(e) => {
5021 self.notify_clients(Err(e)).await;
5022 return;
5023 }
5024 };
5025 let (controls, instance_access_handle) =
5026 match track.lock().clip_lv2_plugin_controls(clip_idx, instance_id) {
5027 Ok(result) => result,
5028 Err(e) => {
5029 self.notify_clients(Err(e)).await;
5030 return;
5031 }
5032 };
5033 self.notify_clients(Ok(Action::ClipLv2PluginControls {
5034 track_name: track_name.clone(),
5035 clip_idx,
5036 instance_id,
5037 controls,
5038 instance_access_handle,
5039 }))
5040 .await;
5041 return;
5042 }
5043 #[cfg(all(unix, not(target_os = "macos")))]
5044 Action::TrackSetLv2ControlValue {
5045 ref track_name,
5046 instance_id,
5047 index,
5048 value,
5049 } => {
5050 if self
5051 .reject_if_track_frozen(track_name, "LV2 parameter changes")
5052 .await
5053 {
5054 return;
5055 }
5056 let track = match self.track_handle_or_err(track_name) {
5057 Ok(track) => track,
5058 Err(e) => {
5059 self.notify_clients(Err(e)).await;
5060 return;
5061 }
5062 };
5063 if let Err(e) = track
5064 .lock()
5065 .set_lv2_control_value(instance_id, index, value)
5066 {
5067 self.notify_clients(Err(e)).await;
5068 return;
5069 }
5070 }
5071 #[cfg(all(unix, not(target_os = "macos")))]
5072 Action::ClipSetLv2ControlValue {
5073 ref track_name,
5074 clip_idx,
5075 instance_id,
5076 index,
5077 value,
5078 } => {
5079 if self
5080 .reject_if_track_frozen(track_name, "LV2 parameter changes")
5081 .await
5082 {
5083 return;
5084 }
5085 let track = match self.track_handle_or_err(track_name) {
5086 Ok(track) => track,
5087 Err(e) => {
5088 self.notify_clients(Err(e)).await;
5089 return;
5090 }
5091 };
5092 let state = match track.lock().clip_set_lv2_control_value(
5093 clip_idx,
5094 instance_id,
5095 index,
5096 value,
5097 ) {
5098 Ok(state) => state,
5099 Err(e) => {
5100 self.notify_clients(Err(e)).await;
5101 return;
5102 }
5103 };
5104 self.notify_clients(Ok(Action::ClipLv2StateSnapshot {
5105 track_name: track_name.clone(),
5106 clip_idx,
5107 instance_id,
5108 state,
5109 }))
5110 .await;
5111 }
5112 Action::TrackGetPluginGraph { ref track_name } => {
5113 let track = match self.track_handle_or_err(track_name) {
5114 Ok(track) => track,
5115 Err(e) => {
5116 self.notify_clients(Err(e)).await;
5117 return;
5118 }
5119 };
5120 let (plugins, connections) = {
5121 let track = track.lock();
5122 (
5123 track.plugin_graph_plugins(),
5124 track.plugin_graph_connections(),
5125 )
5126 };
5127 self.notify_clients(Ok(Action::TrackPluginGraph {
5128 track_name: track_name.clone(),
5129 plugins,
5130 connections,
5131 }))
5132 .await;
5133 return;
5134 }
5135 Action::TrackPluginGraph { .. } => {}
5136 Action::TrackConnectPluginAudio {
5137 ref track_name,
5138 ref from_node,
5139 from_port,
5140 ref to_node,
5141 to_port,
5142 } => {
5143 if self
5144 .reject_if_track_frozen(track_name, "plugin routing changes")
5145 .await
5146 {
5147 return;
5148 }
5149 let track = match self.track_handle_or_err(track_name) {
5150 Ok(track) => track,
5151 Err(e) => {
5152 self.notify_clients(Err(e)).await;
5153 return;
5154 }
5155 };
5156 if let Err(e) = track.lock().connect_plugin_audio(
5157 from_node.clone(),
5158 from_port,
5159 to_node.clone(),
5160 to_port,
5161 ) {
5162 self.notify_clients(Err(e)).await;
5163 return;
5164 }
5165 }
5166 Action::TrackConnectPluginMidi {
5167 ref track_name,
5168 ref from_node,
5169 from_port,
5170 ref to_node,
5171 to_port,
5172 } => {
5173 if self
5174 .reject_if_track_frozen(track_name, "plugin routing changes")
5175 .await
5176 {
5177 return;
5178 }
5179 let track = match self.track_handle_or_err(track_name) {
5180 Ok(track) => track,
5181 Err(e) => {
5182 self.notify_clients(Err(e)).await;
5183 return;
5184 }
5185 };
5186 if let Err(e) = track.lock().connect_plugin_midi(
5187 from_node.clone(),
5188 from_port,
5189 to_node.clone(),
5190 to_port,
5191 ) {
5192 self.notify_clients(Err(e)).await;
5193 return;
5194 }
5195 }
5196 Action::TrackDisconnectPluginAudio {
5197 ref track_name,
5198 ref from_node,
5199 from_port,
5200 ref to_node,
5201 to_port,
5202 } => {
5203 if self
5204 .reject_if_track_frozen(track_name, "plugin routing changes")
5205 .await
5206 {
5207 return;
5208 }
5209 let track = match self.track_handle_or_err(track_name) {
5210 Ok(track) => track,
5211 Err(e) => {
5212 self.notify_clients(Err(e)).await;
5213 return;
5214 }
5215 };
5216 if let Err(e) = track.lock().disconnect_plugin_audio(
5217 from_node.clone(),
5218 from_port,
5219 to_node.clone(),
5220 to_port,
5221 ) {
5222 self.notify_clients(Err(e)).await;
5223 return;
5224 }
5225 }
5226 Action::TrackDisconnectPluginMidi {
5227 ref track_name,
5228 ref from_node,
5229 from_port,
5230 ref to_node,
5231 to_port,
5232 } => {
5233 if self
5234 .reject_if_track_frozen(track_name, "plugin routing changes")
5235 .await
5236 {
5237 return;
5238 }
5239 let track = match self.track_handle_or_err(track_name) {
5240 Ok(track) => track,
5241 Err(e) => {
5242 self.notify_clients(Err(e)).await;
5243 return;
5244 }
5245 };
5246 if let Err(e) = track.lock().disconnect_plugin_midi(
5247 from_node.clone(),
5248 from_port,
5249 to_node.clone(),
5250 to_port,
5251 ) {
5252 self.notify_clients(Err(e)).await;
5253 return;
5254 }
5255 }
5256 #[cfg(all(unix, not(target_os = "macos")))]
5257 Action::ListLv2Plugins => {
5258 let plugins = {
5259 let host = crate::lv2::Lv2Host::new(48_000.0);
5260 host.list_plugins()
5261 };
5262 self.notify_clients(Ok(Action::Lv2Plugins(plugins))).await;
5263 return;
5264 }
5265 #[cfg(all(unix, not(target_os = "macos")))]
5266 Action::Lv2Plugins(_) => {}
5267 Action::ListVst3Plugins => {
5268 self.notify_clients(Ok(Action::Vst3Plugins(crate::vst3::list_plugins())))
5269 .await;
5270 return;
5271 }
5272 Action::Vst3Plugins(_) => {}
5273 Action::ListClapPlugins => {
5274 self.notify_clients(Ok(Action::ClapPlugins(crate::clap::list_plugins())))
5275 .await;
5276 return;
5277 }
5278 Action::ListClapPluginsWithCapabilities => {
5279 self.notify_clients(Ok(Action::ClapPlugins(
5280 crate::clap::list_plugins_with_capabilities(true),
5281 )))
5282 .await;
5283 return;
5284 }
5285 Action::ClapPlugins(_) => {}
5286 Action::TrackLoadClapPlugin {
5287 ref track_name,
5288 ref plugin_path,
5289 instance_id,
5290 } => {
5291 if self
5292 .reject_if_track_frozen(track_name, "CLAP plugin loading")
5293 .await
5294 {
5295 return;
5296 }
5297 let track = match self.track_handle_or_err(track_name) {
5298 Ok(track) => track,
5299 Err(e) => {
5300 self.notify_clients(Err(e)).await;
5301 return;
5302 }
5303 };
5304 let track = track.lock();
5305 if track.audio.processing {
5306 self.notify_clients(Err(format!(
5307 "Track '{}' is currently processing audio; stop playback before loading CLAP plugins",
5308 track_name
5309 )))
5310 .await;
5311 return;
5312 }
5313 if let Err(e) = track.load_clap_plugin(plugin_path, instance_id) {
5314 self.notify_clients(Err(e)).await;
5315 return;
5316 }
5317 }
5318 Action::TrackUnloadClapPlugin {
5319 ref track_name,
5320 ref plugin_path,
5321 } => {
5322 if self
5323 .reject_if_track_frozen(track_name, "CLAP plugin unloading")
5324 .await
5325 {
5326 return;
5327 }
5328 let track = match self.track_handle_or_err(track_name) {
5329 Ok(track) => track,
5330 Err(e) => {
5331 self.notify_clients(Err(e)).await;
5332 return;
5333 }
5334 };
5335 let track = track.lock();
5336 if track.audio.processing {
5337 self.notify_clients(Err(format!(
5338 "Track '{}' is currently processing audio; stop playback before unloading CLAP plugins",
5339 track_name
5340 )))
5341 .await;
5342 return;
5343 }
5344 if let Err(e) = track.unload_clap_plugin(plugin_path) {
5345 self.notify_clients(Err(e)).await;
5346 return;
5347 }
5348 }
5349 Action::TrackSetClapParameter {
5350 ref track_name,
5351 instance_id,
5352 param_id,
5353 value,
5354 } => {
5355 if self
5356 .reject_if_track_frozen(track_name, "CLAP parameter changes")
5357 .await
5358 {
5359 return;
5360 }
5361 match self.track_handle_or_err(track_name) {
5362 Ok(track) => {
5363 if let Err(e) =
5364 track
5365 .lock()
5366 .set_clap_parameter(instance_id, param_id, value)
5367 {
5368 self.notify_clients(Err(e)).await;
5369 return;
5370 }
5371 self.notify_clients(Ok(a.clone())).await;
5372 }
5373 Err(e) => {
5374 self.notify_clients(Err(e)).await;
5375 }
5376 }
5377 }
5378 Action::ClipSetClapParameter {
5379 ref track_name,
5380 clip_idx,
5381 instance_id,
5382 param_id,
5383 value,
5384 } => {
5385 if self
5386 .reject_if_track_frozen(track_name, "CLAP parameter changes")
5387 .await
5388 {
5389 return;
5390 }
5391 match self.track_handle_or_err(track_name) {
5392 Ok(track) => {
5393 if let Err(e) = track.lock().clip_set_clap_parameter(
5394 clip_idx,
5395 instance_id,
5396 param_id,
5397 value,
5398 ) {
5399 self.notify_clients(Err(e)).await;
5400 return;
5401 }
5402 self.notify_clients(Ok(a.clone())).await;
5403 }
5404 Err(e) => {
5405 self.notify_clients(Err(e)).await;
5406 }
5407 }
5408 }
5409 Action::TrackSetClapParameterAt {
5410 ref track_name,
5411 instance_id,
5412 param_id,
5413 value,
5414 frame,
5415 } => {
5416 if self
5417 .reject_if_track_frozen(track_name, "CLAP parameter changes")
5418 .await
5419 {
5420 return;
5421 }
5422 match self.track_handle_or_err(track_name) {
5423 Ok(track) => {
5424 if let Err(e) =
5425 track
5426 .lock()
5427 .set_clap_parameter_at(instance_id, param_id, value, frame)
5428 {
5429 self.notify_clients(Err(e)).await;
5430 return;
5431 }
5432 self.notify_clients(Ok(a.clone())).await;
5433 }
5434 Err(e) => {
5435 self.notify_clients(Err(e)).await;
5436 }
5437 }
5438 }
5439 Action::TrackBeginClapParameterEdit {
5440 ref track_name,
5441 instance_id,
5442 param_id,
5443 frame,
5444 } => {
5445 if self
5446 .reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
5447 .await
5448 {
5449 return;
5450 }
5451 match self.track_handle_or_err(track_name) {
5452 Ok(track) => {
5453 if let Err(e) =
5454 track
5455 .lock()
5456 .begin_clap_parameter_edit(instance_id, param_id, frame)
5457 {
5458 self.notify_clients(Err(e)).await;
5459 return;
5460 }
5461 self.notify_clients(Ok(a.clone())).await;
5462 }
5463 Err(e) => {
5464 self.notify_clients(Err(e)).await;
5465 }
5466 }
5467 }
5468 Action::TrackEndClapParameterEdit {
5469 ref track_name,
5470 instance_id,
5471 param_id,
5472 frame,
5473 } => {
5474 if self
5475 .reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
5476 .await
5477 {
5478 return;
5479 }
5480 match self.track_handle_or_err(track_name) {
5481 Ok(track) => {
5482 if let Err(e) =
5483 track
5484 .lock()
5485 .end_clap_parameter_edit(instance_id, param_id, frame)
5486 {
5487 self.notify_clients(Err(e)).await;
5488 return;
5489 }
5490 self.notify_clients(Ok(a.clone())).await;
5491 }
5492 Err(e) => {
5493 self.notify_clients(Err(e)).await;
5494 }
5495 }
5496 }
5497 Action::TrackGetClapParameters {
5498 ref track_name,
5499 instance_id,
5500 } => match self.track_handle_or_err(track_name) {
5501 Ok(track) => match track.lock().get_clap_parameters(instance_id) {
5502 Ok(parameters) => {
5503 self.notify_clients(Ok(Action::TrackClapParameters {
5504 track_name: track_name.clone(),
5505 instance_id,
5506 parameters,
5507 }))
5508 .await;
5509 }
5510 Err(e) => {
5511 self.notify_clients(Err(e)).await;
5512 }
5513 },
5514 Err(e) => {
5515 self.notify_clients(Err(e)).await;
5516 }
5517 },
5518 Action::TrackClapParameters { .. } => {}
5519 Action::TrackClapSnapshotState {
5520 ref track_name,
5521 instance_id,
5522 } => match self.track_handle_or_err(track_name) {
5523 Ok(track) => {
5524 let plugin_path = track
5525 .lock()
5526 .loaded_clap_instances()
5527 .into_iter()
5528 .find(|(id, _, _)| *id == instance_id)
5529 .map(|(_, path, _)| path)
5530 .unwrap_or_default();
5531 match track.lock().clap_snapshot_state(instance_id) {
5532 Ok(state) => {
5533 self.notify_clients(Ok(Action::TrackClapStateSnapshot {
5534 track_name: track_name.clone(),
5535 instance_id,
5536 plugin_path,
5537 state,
5538 }))
5539 .await;
5540 }
5541 Err(e) => {
5542 self.notify_clients(Err(e)).await;
5543 }
5544 }
5545 }
5546 Err(e) => {
5547 self.notify_clients(Err(e)).await;
5548 }
5549 },
5550 Action::ClipClapSnapshotState {
5551 ref track_name,
5552 clip_idx,
5553 instance_id,
5554 } => match self.track_handle_or_err(track_name) {
5555 Ok(track) => match track.lock().clip_clap_snapshot_state(clip_idx, instance_id) {
5556 Ok((plugin_path, state)) => {
5557 self.notify_clients(Ok(Action::ClipClapStateSnapshot {
5558 track_name: track_name.clone(),
5559 clip_idx,
5560 instance_id,
5561 plugin_path,
5562 state,
5563 }))
5564 .await;
5565 }
5566 Err(e) => {
5567 self.notify_clients(Err(e)).await;
5568 }
5569 },
5570 Err(e) => {
5571 self.notify_clients(Err(e)).await;
5572 }
5573 },
5574 Action::TrackGetClapProcessor {
5575 ref track_name,
5576 instance_id,
5577 } => match self.track_handle_or_err(track_name) {
5578 Ok(track) => match track.lock().clap_plugin_processor(instance_id) {
5579 Ok(processor) => {
5580 self.notify_clients(Ok(Action::TrackClapProcessor {
5581 track_name: track_name.clone(),
5582 instance_id,
5583 processor,
5584 }))
5585 .await;
5586 }
5587 Err(e) => {
5588 self.notify_clients(Err(e)).await;
5589 }
5590 },
5591 Err(e) => {
5592 self.notify_clients(Err(e)).await;
5593 }
5594 },
5595 Action::ClipGetClapProcessor {
5596 ref track_name,
5597 clip_idx,
5598 instance_id,
5599 } => match self.track_handle_or_err(track_name) {
5600 Ok(track) => match track
5601 .lock()
5602 .clip_clap_plugin_processor(clip_idx, instance_id)
5603 {
5604 Ok(processor) => {
5605 self.notify_clients(Ok(Action::ClipClapProcessor {
5606 track_name: track_name.clone(),
5607 clip_idx,
5608 instance_id,
5609 processor,
5610 }))
5611 .await;
5612 }
5613 Err(e) => {
5614 self.notify_clients(Err(e)).await;
5615 }
5616 },
5617 Err(e) => {
5618 self.notify_clients(Err(e)).await;
5619 }
5620 },
5621 Action::TrackClapStateSnapshot { .. } => {}
5622 Action::ClipClapStateSnapshot { .. } => {}
5623 Action::TrackClapRestoreState {
5624 ref track_name,
5625 instance_id,
5626 ref state,
5627 } => {
5628 if self
5629 .reject_if_track_frozen(track_name, "CLAP state restore")
5630 .await
5631 {
5632 return;
5633 }
5634 let track = match self.track_handle_or_err(track_name) {
5635 Ok(track) => track,
5636 Err(e) => {
5637 self.notify_clients(Err(e)).await;
5638 return;
5639 }
5640 };
5641 let track = track.lock();
5642 if track.audio.processing {
5643 self.notify_clients(Err(format!(
5644 "Track '{}' is currently processing audio; stop playback before restoring CLAP state",
5645 track_name
5646 )))
5647 .await;
5648 return;
5649 }
5650 if let Err(e) = track.clap_restore_state(instance_id, state) {
5651 self.notify_clients(Err(e)).await;
5652 return;
5653 }
5654 }
5655 Action::ClipClapRestoreState {
5656 ref track_name,
5657 clip_idx,
5658 instance_id,
5659 ref state,
5660 } => {
5661 if self
5662 .reject_if_track_frozen(track_name, "CLAP state restore")
5663 .await
5664 {
5665 return;
5666 }
5667 let track = match self.track_handle_or_err(track_name) {
5668 Ok(track) => track,
5669 Err(e) => {
5670 self.notify_clients(Err(e)).await;
5671 return;
5672 }
5673 };
5674 let track = track.lock();
5675 if track.audio.processing {
5676 self.notify_clients(Err(format!(
5677 "Track '{}' is currently processing audio; stop playback before restoring CLAP state",
5678 track_name
5679 )))
5680 .await;
5681 return;
5682 }
5683 if let Err(e) = track.clip_clap_restore_state(clip_idx, instance_id, state) {
5684 self.notify_clients(Err(e)).await;
5685 return;
5686 }
5687 }
5688 Action::TrackSnapshotAllClapStates { ref track_name } => {
5689 let track = match self.track_handle_or_err(track_name) {
5690 Ok(track) => track,
5691 Err(e) => {
5692 self.notify_clients(Err(e)).await;
5693 return;
5694 }
5695 };
5696 for (instance_id, plugin_path, state) in track.lock().clap_snapshot_all_states() {
5697 self.notify_clients(Ok(Action::TrackClapStateSnapshot {
5698 track_name: track_name.clone(),
5699 instance_id,
5700 plugin_path,
5701 state,
5702 }))
5703 .await;
5704 }
5705 self.notify_clients(Ok(Action::TrackSnapshotAllClapStatesDone {
5706 track_name: track_name.clone(),
5707 }))
5708 .await;
5709 }
5710 Action::TrackSnapshotAllClapStatesDone { .. } => {}
5711 Action::TrackLoadVst3Plugin {
5712 ref track_name,
5713 ref plugin_path,
5714 instance_id,
5715 } => {
5716 if self
5717 .reject_if_track_frozen(track_name, "VST3 plugin loading")
5718 .await
5719 {
5720 return;
5721 }
5722 let track = match self.track_handle_or_err(track_name) {
5723 Ok(track) => track,
5724 Err(e) => {
5725 self.notify_clients(Err(e)).await;
5726 return;
5727 }
5728 };
5729 if let Err(e) = track.lock().load_vst3_plugin(plugin_path, instance_id) {
5730 self.notify_clients(Err(e)).await;
5731 return;
5732 }
5733 }
5734 Action::TrackUnloadVst3PluginInstance {
5735 ref track_name,
5736 instance_id,
5737 } => {
5738 if self
5739 .reject_if_track_frozen(track_name, "VST3 plugin unloading")
5740 .await
5741 {
5742 return;
5743 }
5744 let track = match self.track_handle_or_err(track_name) {
5745 Ok(track) => track,
5746 Err(e) => {
5747 self.notify_clients(Err(e)).await;
5748 return;
5749 }
5750 };
5751 if let Err(e) = track.lock().unload_vst3_plugin_instance(instance_id) {
5752 self.notify_clients(Err(e)).await;
5753 return;
5754 }
5755 }
5756 Action::TrackGetVst3Graph { ref track_name } => {
5757 match self.track_handle_or_err(track_name) {
5758 Ok(track) => {
5759 let t = track.lock();
5760 let plugins = t.vst3_graph_plugins();
5761 let connections = t.vst3_graph_connections();
5762 self.notify_clients(Ok(Action::TrackVst3Graph {
5763 track_name: track_name.clone(),
5764 plugins,
5765 connections,
5766 }))
5767 .await;
5768 }
5769 Err(e) => {
5770 self.notify_clients(Err(e)).await;
5771 }
5772 }
5773 }
5774 Action::TrackVst3Graph { .. } => {}
5775 Action::TrackSetVst3Parameter {
5776 ref track_name,
5777 instance_id,
5778 param_id,
5779 value,
5780 } => {
5781 if self
5782 .reject_if_track_frozen(track_name, "VST3 parameter changes")
5783 .await
5784 {
5785 return;
5786 }
5787 match self.track_handle_or_err(track_name) {
5788 Ok(track) => {
5789 if let Err(e) =
5790 track
5791 .lock()
5792 .set_vst3_parameter(instance_id, param_id, value)
5793 {
5794 self.notify_clients(Err(e)).await;
5795 return;
5796 }
5797 self.notify_clients(Ok(a.clone())).await;
5798 }
5799 Err(e) => {
5800 self.notify_clients(Err(e)).await;
5801 }
5802 }
5803 }
5804 Action::TrackSetPluginBypassed {
5805 ref track_name,
5806 instance_id,
5807 ref format,
5808 bypassed,
5809 } => match self.track_handle_or_err(track_name) {
5810 Ok(track) => {
5811 let result = match format.as_str() {
5812 "CLAP" => track.lock().set_clap_plugin_bypassed(instance_id, bypassed),
5813 "VST3" => track.lock().set_vst3_plugin_bypassed(instance_id, bypassed),
5814 #[cfg(all(unix, not(target_os = "macos")))]
5815 "LV2" => track.lock().set_lv2_plugin_bypassed(instance_id, bypassed),
5816 _ => Err(format!("Unknown plugin format for bypass: {format}")),
5817 };
5818 if let Err(e) = result {
5819 self.notify_clients(Err(e)).await;
5820 return;
5821 }
5822 self.notify_clients(Ok(a.clone())).await;
5823 }
5824 Err(e) => {
5825 self.notify_clients(Err(e)).await;
5826 }
5827 },
5828 Action::TrackGetVst3Parameters {
5829 ref track_name,
5830 instance_id,
5831 } => match self.track_handle_or_err(track_name) {
5832 Ok(track) => match track.lock().get_vst3_parameters(instance_id) {
5833 Ok(parameters) => {
5834 self.notify_clients(Ok(Action::TrackVst3Parameters {
5835 track_name: track_name.clone(),
5836 instance_id,
5837 parameters,
5838 }))
5839 .await;
5840 }
5841 Err(e) => {
5842 self.notify_clients(Err(e)).await;
5843 }
5844 },
5845 Err(e) => {
5846 self.notify_clients(Err(e)).await;
5847 }
5848 },
5849 Action::TrackVst3Parameters { .. } => {}
5850 Action::TrackGetVst3Processor {
5851 ref track_name,
5852 instance_id,
5853 } => match self.track_handle_or_err(track_name) {
5854 Ok(track) => match track.lock().vst3_plugin_processor(instance_id) {
5855 Ok(processor) => {
5856 self.notify_clients(Ok(Action::TrackVst3Processor {
5857 track_name: track_name.clone(),
5858 instance_id,
5859 processor,
5860 }))
5861 .await;
5862 }
5863 Err(e) => {
5864 self.notify_clients(Err(e)).await;
5865 }
5866 },
5867 Err(e) => {
5868 self.notify_clients(Err(e)).await;
5869 }
5870 },
5871 Action::ClipGetVst3Processor {
5872 ref track_name,
5873 clip_idx,
5874 instance_id,
5875 } => match self.track_handle_or_err(track_name) {
5876 Ok(track) => match track
5877 .lock()
5878 .clip_vst3_plugin_processor(clip_idx, instance_id)
5879 {
5880 Ok(processor) => {
5881 self.notify_clients(Ok(Action::ClipVst3Processor {
5882 track_name: track_name.clone(),
5883 clip_idx,
5884 instance_id,
5885 processor,
5886 }))
5887 .await;
5888 }
5889 Err(e) => {
5890 self.notify_clients(Err(e)).await;
5891 }
5892 },
5893 Err(e) => {
5894 self.notify_clients(Err(e)).await;
5895 }
5896 },
5897 Action::TrackVst3Processor { .. } => {}
5898 Action::ClipVst3Processor { .. } => {}
5899 Action::TrackVst3SnapshotState {
5900 ref track_name,
5901 instance_id,
5902 } => match self.track_handle_or_err(track_name) {
5903 Ok(track) => match track.lock().vst3_snapshot_state(instance_id) {
5904 Ok(state) => {
5905 self.notify_clients(Ok(Action::TrackVst3StateSnapshot {
5906 track_name: track_name.clone(),
5907 instance_id,
5908 state,
5909 }))
5910 .await;
5911 }
5912 Err(e) => {
5913 self.notify_clients(Err(e)).await;
5914 }
5915 },
5916 Err(e) => {
5917 self.notify_clients(Err(e)).await;
5918 }
5919 },
5920 Action::ClipVst3SnapshotState {
5921 ref track_name,
5922 clip_idx,
5923 instance_id,
5924 } => match self.track_handle_or_err(track_name) {
5925 Ok(track) => match track.lock().clip_vst3_snapshot_state(clip_idx, instance_id) {
5926 Ok(state) => {
5927 self.notify_clients(Ok(Action::ClipVst3StateSnapshot {
5928 track_name: track_name.clone(),
5929 clip_idx,
5930 instance_id,
5931 state,
5932 }))
5933 .await;
5934 }
5935 Err(e) => {
5936 self.notify_clients(Err(e)).await;
5937 }
5938 },
5939 Err(e) => {
5940 self.notify_clients(Err(e)).await;
5941 }
5942 },
5943 Action::TrackVst3StateSnapshot { .. } => {}
5944 Action::ClipVst3StateSnapshot { .. } => {}
5945 Action::TrackVst3RestoreState {
5946 ref track_name,
5947 instance_id,
5948 ref state,
5949 } => match self.track_handle_or_err(track_name) {
5950 Ok(track) => {
5951 if let Err(e) = track.lock().vst3_restore_state(instance_id, state) {
5952 self.notify_clients(Err(e)).await;
5953 return;
5954 }
5955 self.notify_clients(Ok(a.clone())).await;
5956 }
5957 Err(e) => {
5958 self.notify_clients(Err(e)).await;
5959 }
5960 },
5961 Action::TrackConnectVst3Audio {
5962 ref track_name,
5963 ref from_node,
5964 from_port,
5965 ref to_node,
5966 to_port,
5967 } => {
5968 if self
5969 .reject_if_track_frozen(track_name, "VST3 routing changes")
5970 .await
5971 {
5972 return;
5973 }
5974 match self.track_handle_or_err(track_name) {
5975 Ok(track) => {
5976 if let Err(e) = track
5977 .lock()
5978 .connect_vst3_audio(from_node, from_port, to_node, to_port)
5979 {
5980 self.notify_clients(Err(e)).await;
5981 return;
5982 }
5983 self.notify_clients(Ok(a.clone())).await;
5984 }
5985 Err(e) => {
5986 self.notify_clients(Err(e)).await;
5987 }
5988 }
5989 }
5990 Action::TrackDisconnectVst3Audio {
5991 ref track_name,
5992 ref from_node,
5993 from_port,
5994 ref to_node,
5995 to_port,
5996 } => {
5997 if self
5998 .reject_if_track_frozen(track_name, "VST3 routing changes")
5999 .await
6000 {
6001 return;
6002 }
6003 match self.track_handle_or_err(track_name) {
6004 Ok(track) => {
6005 if let Err(e) = track
6006 .lock()
6007 .disconnect_vst3_audio(from_node, from_port, to_node, to_port)
6008 {
6009 self.notify_clients(Err(e)).await;
6010 return;
6011 }
6012 self.notify_clients(Ok(a.clone())).await;
6013 }
6014 Err(e) => {
6015 self.notify_clients(Err(e)).await;
6016 }
6017 }
6018 }
6019 Action::ClipMove {
6020 ref kind,
6021 ref from,
6022 ref to,
6023 copy,
6024 } => {
6025 if let Some(from_track_handle) = self.state.lock().tracks.get(&from.track_name)
6026 && let Some(to_track_handle) = self.state.lock().tracks.get(&to.track_name)
6027 {
6028 let from_track = from_track_handle.lock();
6029 let to_track = to_track_handle.lock();
6030 match kind {
6031 Kind::Audio => {
6032 if from.clip_index >= from_track.audio.clips.len() {
6033 self.notify_clients(Err(format!(
6034 "Clip index {} is too high, as track {} has only {} clips!",
6035 from.clip_index,
6036 from_track.name.clone(),
6037 from_track.audio.clips.len(),
6038 )))
6039 .await;
6040 return;
6041 }
6042 if from_track.audio.ins.len() != to_track.audio.ins.len() {
6043 self.notify_clients(Err(format!(
6044 "Cannot move/copy audio clip from '{}' ({} inputs) to '{}' ({} inputs)",
6045 from_track.name,
6046 from_track.audio.ins.len(),
6047 to_track.name,
6048 to_track.audio.ins.len()
6049 )))
6050 .await;
6051 return;
6052 }
6053 let clip_copy = from_track.audio.clips[from.clip_index].clone();
6054 if !copy {
6055 from_track.audio.clips.remove(from.clip_index);
6056 }
6057 let mut clip_copy = clip_copy;
6058 clip_copy.start = to.sample_offset;
6059 let max_lane = to_track.audio.ins.len().saturating_sub(1);
6060 clip_copy.input_channel = to.input_channel.min(max_lane);
6061 to_track.audio.clips.push(clip_copy);
6062 }
6063 Kind::MIDI => {
6064 if from.clip_index >= from_track.midi.clips.len() {
6065 self.notify_clients(Err(format!(
6066 "Clip index {} is too high, as track {} has only {} clips!",
6067 from.clip_index,
6068 from_track.name.clone(),
6069 from_track.midi.clips.len(),
6070 )))
6071 .await;
6072 return;
6073 }
6074 let clip_copy = from_track.midi.clips[from.clip_index].clone();
6075 if !copy {
6076 from_track.midi.clips.remove(from.clip_index);
6077 }
6078 let mut clip_copy = clip_copy;
6079 clip_copy.start = to.sample_offset;
6080 let max_lane = to_track.midi.ins.len().saturating_sub(1);
6081 clip_copy.input_channel = to.input_channel.min(max_lane);
6082 to_track.midi.clips.push(clip_copy);
6083 }
6084 }
6085 }
6086 }
6087 Action::AddClip {
6088 ref name,
6089 ref track_name,
6090 start,
6091 length,
6092 offset,
6093 input_channel,
6094 muted,
6095 ref peaks_file,
6096 kind,
6097 fade_enabled,
6098 fade_in_samples,
6099 fade_out_samples,
6100 ref source_name,
6101 source_offset,
6102 source_length,
6103 ref preview_name,
6104 ref pitch_correction_points,
6105 pitch_correction_frame_likeness,
6106 pitch_correction_inertia_ms,
6107 pitch_correction_formant_compensation,
6108 ref plugin_graph_json,
6109 } => {
6110 self.add_clip_to_track(ClipAddRequest {
6111 name,
6112 track_name,
6113 start,
6114 length,
6115 offset,
6116 input_channel,
6117 muted,
6118 peaks_file: peaks_file.clone(),
6119 kind,
6120 fade_enabled,
6121 fade_in_samples,
6122 fade_out_samples,
6123 source_name: source_name.clone(),
6124 source_offset,
6125 source_length,
6126 preview_name: preview_name.clone(),
6127 pitch_correction_points: pitch_correction_points.clone(),
6128 pitch_correction_frame_likeness,
6129 pitch_correction_inertia_ms,
6130 pitch_correction_formant_compensation,
6131 plugin_graph_json: plugin_graph_json.clone(),
6132 });
6133 }
6134 Action::AddGroupedClip {
6135 ref track_name,
6136 kind,
6137 ref audio_clip,
6138 ref midi_clip,
6139 } => {
6140 self.add_grouped_clip_to_track(
6141 track_name,
6142 kind,
6143 audio_clip.clone(),
6144 midi_clip.clone(),
6145 );
6146 }
6147 Action::RemoveClip {
6148 ref track_name,
6149 kind,
6150 ref clip_indices,
6151 } => {
6152 self.remove_clips_from_track(track_name, kind, clip_indices);
6153 }
6154 Action::RenameClip {
6155 ref track_name,
6156 kind,
6157 clip_index,
6158 ref new_name,
6159 } => {
6160 self.rename_clip_references(track_name, kind, clip_index, new_name);
6161 }
6162 Action::SetClipSourceName {
6163 ref track_name,
6164 kind,
6165 clip_index,
6166 ref name,
6167 } => {
6168 self.set_clip_source_name(track_name, clip_index, kind, name.clone());
6169 }
6170 Action::SetClipFade {
6171 ref track_name,
6172 clip_index,
6173 kind,
6174 fade_enabled,
6175 fade_in_samples,
6176 fade_out_samples,
6177 } => {
6178 self.set_clip_fade(
6179 track_name,
6180 clip_index,
6181 kind,
6182 fade_enabled,
6183 fade_in_samples,
6184 fade_out_samples,
6185 );
6186 }
6187 Action::SetClipBounds {
6188 ref track_name,
6189 clip_index,
6190 kind,
6191 start,
6192 length,
6193 offset,
6194 } => {
6195 self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
6196 }
6197 Action::SyncClipBounds {
6198 ref track_name,
6199 clip_index,
6200 kind,
6201 start,
6202 length,
6203 offset,
6204 } => {
6205 self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
6206 }
6207 Action::SetClipMuted {
6208 ref track_name,
6209 clip_index,
6210 kind,
6211 muted,
6212 } => {
6213 self.set_clip_muted(track_name, clip_index, kind, muted);
6214 }
6215 Action::SetClipPluginGraphJson {
6216 ref track_name,
6217 clip_index,
6218 ref plugin_graph_json,
6219 } => {
6220 self.set_clip_plugin_graph_json(track_name, clip_index, plugin_graph_json.clone());
6221 }
6222 Action::SetClipPitchCorrection {
6223 ref track_name,
6224 clip_index,
6225 ref preview_name,
6226 ref source_name,
6227 source_offset,
6228 source_length,
6229 ref pitch_correction_points,
6230 pitch_correction_frame_likeness,
6231 pitch_correction_inertia_ms,
6232 pitch_correction_formant_compensation,
6233 } => {
6234 self.set_clip_pitch_correction(
6235 track_name,
6236 clip_index,
6237 preview_name.clone(),
6238 source_name.clone(),
6239 source_offset,
6240 source_length,
6241 pitch_correction_points.clone(),
6242 pitch_correction_frame_likeness,
6243 pitch_correction_inertia_ms,
6244 pitch_correction_formant_compensation,
6245 );
6246 }
6247 Action::Connect {
6248 ref from_track,
6249 from_port,
6250 ref to_track,
6251 to_port,
6252 kind,
6253 } => {
6254 match kind {
6255 Kind::Audio => {
6256 let from_audio_io = if from_track == "hw:in" {
6257 self.hw_input_audio_port(from_port)
6258 } else {
6259 self.state
6260 .lock()
6261 .tracks
6262 .get(from_track)
6263 .and_then(|t| t.lock().audio.outs.get(from_port).cloned())
6264 };
6265 let to_audio_io = if to_track == "hw:out" {
6266 self.hw_output_audio_port(to_port)
6267 } else {
6268 self.state
6269 .lock()
6270 .tracks
6271 .get(to_track)
6272 .and_then(|t| t.lock().audio.ins.get(to_port).cloned())
6273 };
6274 match (from_audio_io, to_audio_io) {
6275 (Some(source), Some(target)) => {
6276 if from_track != "hw:in"
6277 && to_track != "hw:out"
6278 && self.check_if_leads_to_kind(
6279 Kind::Audio,
6280 to_track,
6281 from_track,
6282 )
6283 {
6284 self.notify_clients(Err(
6285 "Circular routing is not allowed!".into()
6286 ))
6287 .await;
6288 return;
6289 }
6290 crate::audio::io::AudioIO::connect(&source, &target);
6291 }
6292 (None, _) => {
6293 self.notify_clients(Err(format!(
6294 "Source track '{}' not found",
6295 from_track
6296 )))
6297 .await;
6298 return;
6299 }
6300 (_, None) => {
6301 self.notify_clients(Err(format!(
6302 "Destination track '{}' not found",
6303 to_track
6304 )))
6305 .await;
6306 return;
6307 }
6308 }
6309 }
6310 Kind::MIDI => {
6311 let from_hw_in_device = Self::midi_hw_in_device(from_track);
6312 let to_hw_out_device = Self::midi_hw_out_device(to_track);
6313 let from_is_invalid_hw = Self::midi_hw_out_device(from_track).is_some();
6314 let to_is_invalid_hw = Self::midi_hw_in_device(to_track).is_some();
6315
6316 if from_is_invalid_hw || to_is_invalid_hw {
6317 self.notify_clients(Err(
6318 "Invalid MIDI hardware connection direction".to_string()
6319 ))
6320 .await;
6321 return;
6322 }
6323
6324 if from_hw_in_device.is_none()
6325 && to_hw_out_device.is_none()
6326 && self.check_if_leads_to_kind(Kind::MIDI, to_track, from_track)
6327 {
6328 self.notify_clients(Err("Circular routing is not allowed!".into()))
6329 .await;
6330 return;
6331 }
6332
6333 let state = self.state.lock();
6334 let from_track_handle = state.tracks.get(from_track);
6335 let to_track_handle = state.tracks.get(to_track);
6336
6337 if let (Some(from_device), Some(to_device)) =
6338 (from_hw_in_device, to_hw_out_device)
6339 {
6340 let route = MidiHwThruRoute {
6341 from_device: from_device.to_string(),
6342 to_device: to_device.to_string(),
6343 };
6344 if !self.midi_hw_thru_routes.iter().any(|r| r == &route) {
6345 self.midi_hw_thru_routes.push(route);
6346 }
6347 } else if let Some(device) = from_hw_in_device {
6348 if let Some(t_t) = to_track_handle {
6349 if t_t.lock().midi.ins.get(to_port).is_none() {
6350 self.notify_clients(Err(format!(
6351 "MIDI input port {} not found on track '{}'",
6352 to_port, to_track
6353 )))
6354 .await;
6355 return;
6356 }
6357 let route = MidiHwInRoute {
6358 device: device.to_string(),
6359 to_track: to_track.to_string(),
6360 to_port,
6361 };
6362 if !self.midi_hw_in_routes.iter().any(|r| r == &route) {
6363 self.midi_hw_in_routes.push(route);
6364 }
6365 } else {
6366 self.notify_clients(Err(format!(
6367 "MIDI destination track not found: {}",
6368 to_track
6369 )))
6370 .await;
6371 return;
6372 }
6373 } else if let Some(device) = to_hw_out_device {
6374 if let Some(f_t) = from_track_handle {
6375 if f_t.lock().midi.outs.get(from_port).is_none() {
6376 self.notify_clients(Err(format!(
6377 "MIDI output port {} not found on track '{}'",
6378 from_port, from_track
6379 )))
6380 .await;
6381 return;
6382 }
6383 let route = MidiHwOutRoute {
6384 from_track: from_track.to_string(),
6385 from_port,
6386 device: device.to_string(),
6387 };
6388 if !self.midi_hw_out_routes.iter().any(|r| r == &route) {
6389 self.midi_hw_out_routes.push(route);
6390 }
6391 } else {
6392 self.notify_clients(Err(format!(
6393 "MIDI source track not found: {}",
6394 from_track
6395 )))
6396 .await;
6397 return;
6398 }
6399 } else {
6400 match (from_track_handle, to_track_handle) {
6401 (Some(f_t), Some(t_t)) => {
6402 let to_in_res = t_t.lock().midi.ins.get(to_port).cloned();
6403 if let Some(to_in) = to_in_res {
6404 let from_track = f_t.lock();
6405 if let Err(e) =
6406 from_track.midi.connect_out(from_port, to_in)
6407 {
6408 self.notify_clients(Err(e)).await;
6409 return;
6410 }
6411 from_track.invalidate_midi_route_cache();
6412 } else {
6413 self.notify_clients(Err(format!(
6414 "MIDI input port {} not found on track '{}'",
6415 to_port, to_track
6416 )))
6417 .await;
6418 return;
6419 }
6420 }
6421 _ => {
6422 self.notify_clients(Err(format!(
6423 "MIDI tracks not found: {} or {}",
6424 from_track, to_track
6425 )))
6426 .await;
6427 return;
6428 }
6429 }
6430 }
6431 }
6432 };
6433 }
6434 Action::Disconnect {
6435 ref from_track,
6436 from_port,
6437 ref to_track,
6438 to_port,
6439 kind,
6440 } => {
6441 if kind == Kind::Audio {
6442 if let Err(e) = self.disconnect_audio_route_and_notify(a.clone()).await {
6443 self.notify_clients(Err(e)).await;
6444 }
6445 } else if kind == Kind::MIDI {
6446 let from_hw_in_device = Self::midi_hw_in_device(from_track);
6447 let to_hw_out_device = Self::midi_hw_out_device(to_track);
6448
6449 if let (Some(from_device), Some(to_device)) =
6450 (from_hw_in_device, to_hw_out_device)
6451 {
6452 let before = self.midi_hw_thru_routes.len();
6453 self.midi_hw_thru_routes.retain(|r| {
6454 !(r.from_device == from_device && r.to_device == to_device)
6455 });
6456 if self.midi_hw_thru_routes.len() < before {
6457 self.notify_clients(Ok(a.clone())).await;
6458 } else {
6459 self.notify_clients(Err(format!(
6460 "Disconnect failed: MIDI route not found ({} -> {})",
6461 from_track, to_track
6462 )))
6463 .await;
6464 }
6465 return;
6466 }
6467
6468 if let Some(device) = from_hw_in_device {
6469 let before = self.midi_hw_in_routes.len();
6470 self.midi_hw_in_routes.retain(|r| {
6471 !(r.device == device && r.to_track == *to_track && r.to_port == to_port)
6472 });
6473 if self.midi_hw_in_routes.len() < before {
6474 self.notify_clients(Ok(a.clone())).await;
6475 } else {
6476 self.notify_clients(Err(format!(
6477 "Disconnect failed: MIDI route not found ({} -> {})",
6478 from_track, to_track
6479 )))
6480 .await;
6481 }
6482 return;
6483 }
6484
6485 if let Some(device) = to_hw_out_device {
6486 let before = self.midi_hw_out_routes.len();
6487 self.midi_hw_out_routes.retain(|r| {
6488 !(r.from_track == *from_track
6489 && r.from_port == from_port
6490 && r.device == device)
6491 });
6492 if self.midi_hw_out_routes.len() < before {
6493 self.notify_clients(Ok(a.clone())).await;
6494 } else {
6495 self.notify_clients(Err(format!(
6496 "Disconnect failed: MIDI route not found ({} -> {})",
6497 from_track, to_track
6498 )))
6499 .await;
6500 }
6501 return;
6502 }
6503
6504 let state = self.state.lock();
6505 if let (Some(f_t), Some(t_t)) =
6506 (state.tracks.get(from_track), state.tracks.get(to_track))
6507 && let Some(to_in) = t_t.lock().midi.ins.get(to_port).cloned()
6508 {
6509 let from_track = f_t.lock();
6510 if let Err(e) = from_track.midi.disconnect_out(from_port, &to_in) {
6511 self.notify_clients(Err(e)).await;
6512 } else {
6513 from_track.invalidate_midi_route_cache();
6514 self.notify_clients(Ok(a.clone())).await;
6515 }
6516 } else {
6517 self.notify_clients(Err(format!(
6518 "Disconnect failed: MIDI ports not found ({} -> {})",
6519 from_track, to_track
6520 )))
6521 .await;
6522 }
6523 }
6524 }
6525
6526 Action::OpenAudioDevice {
6527 ref device,
6528 ref input_device,
6529 sample_rate_hz,
6530 bits,
6531 exclusive,
6532 period_frames,
6533 nperiods,
6534 sync_mode,
6535 } => {
6536 #[cfg(unix)]
6537 {
6538 let request = AudioOpenRequest {
6539 device,
6540 input_device: input_device.as_deref(),
6541 sample_rate_hz,
6542 bits,
6543 exclusive,
6544 period_frames,
6545 nperiods,
6546 sync_mode,
6547 };
6548 if self.maybe_open_jack_runtime(request).await.is_some() {
6549 return;
6550 }
6551 }
6552 let hw_opts = Self::build_hw_options(exclusive, period_frames, nperiods, sync_mode);
6553 let open_result = self
6554 .open_non_jack_audio_device(
6555 device,
6556 input_device.as_deref(),
6557 sample_rate_hz,
6558 bits,
6559 hw_opts,
6560 )
6561 .await;
6562 match open_result {
6563 Ok(()) => {}
6564 Err(e) => {
6565 self.notify_clients(Err(e)).await;
6566 return;
6567 }
6568 }
6569 self.finalize_open_audio_device().await;
6570 }
6571 Action::JackAddAudioInputPort => {
6572 #[cfg(unix)]
6573 {
6574 if let Some(jack) = self.jack_runtime.clone() {
6575 let (input_channels, output_channels, rate) = {
6576 let jack = jack.lock();
6577 if let Err(e) = jack.add_audio_input_port() {
6578 self.notify_clients(Err(e)).await;
6579 return;
6580 }
6581 (
6582 jack.input_channels(),
6583 jack.output_channels(),
6584 jack.sample_rate,
6585 )
6586 };
6587 self.publish_hw_infos(input_channels, output_channels, rate)
6588 .await;
6589 self.notify_clients(Ok(a.clone())).await;
6590 } else {
6591 self.notify_clients(Err(
6592 "JACK runtime is not active; open the JACK backend first".to_string(),
6593 ))
6594 .await;
6595 }
6596 }
6597 #[cfg(not(unix))]
6598 {
6599 self.notify_clients(Err(
6600 "JACK backend is not available on this platform build".to_string(),
6601 ))
6602 .await;
6603 }
6604 }
6605 Action::JackRemoveAudioInputPort(_removed_port) => {
6606 #[cfg(unix)]
6607 {
6608 let removed_port = _removed_port;
6609 if let Some(jack) = self.jack_runtime.clone() {
6610 let (removed_port, removed_io) = {
6611 let jack = jack.lock();
6612 let removed_port = Some(removed_port);
6613 let removed_io =
6614 removed_port.and_then(|port| jack.input_audio_port(port));
6615 match (removed_port, removed_io) {
6616 (Some(port), Some(io)) => (port, io),
6617 _ => {
6618 self.notify_clients(Err(
6619 "JACK audio input port index is out of range".to_string(),
6620 ))
6621 .await;
6622 return;
6623 }
6624 }
6625 };
6626 let reindex_notifications =
6627 self.reindex_notifications_for_removed_hw_input(removed_port);
6628 for disconnect in
6629 self.disconnect_actions_for_removed_hw_input(removed_port, &removed_io)
6630 {
6631 if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await
6632 {
6633 self.notify_clients(Err(e)).await;
6634 return;
6635 }
6636 }
6637 let (input_channels, output_channels, rate) = {
6638 let jack = jack.lock();
6639 if let Err(e) = jack.remove_audio_input_port(removed_port) {
6640 self.notify_clients(Err(e)).await;
6641 return;
6642 }
6643 (
6644 jack.input_channels(),
6645 jack.output_channels(),
6646 jack.sample_rate,
6647 )
6648 };
6649 for action in reindex_notifications {
6650 self.notify_clients(Ok(action)).await;
6651 }
6652 self.publish_hw_infos(input_channels, output_channels, rate)
6653 .await;
6654 self.notify_clients(Ok(a.clone())).await;
6655 } else {
6656 self.notify_clients(Err(
6657 "JACK runtime is not active; open the JACK backend first".to_string(),
6658 ))
6659 .await;
6660 }
6661 }
6662 #[cfg(not(unix))]
6663 {
6664 self.notify_clients(Err(
6665 "JACK backend is not available on this platform build".to_string(),
6666 ))
6667 .await;
6668 }
6669 }
6670 Action::JackAddAudioOutputPort => {
6671 #[cfg(unix)]
6672 {
6673 if let Some(jack) = self.jack_runtime.clone() {
6674 let (input_channels, output_channels, rate) = {
6675 let jack = jack.lock();
6676 if let Err(e) = jack.add_audio_output_port() {
6677 self.notify_clients(Err(e)).await;
6678 return;
6679 }
6680 (
6681 jack.input_channels(),
6682 jack.output_channels(),
6683 jack.sample_rate,
6684 )
6685 };
6686 self.publish_hw_infos(input_channels, output_channels, rate)
6687 .await;
6688 self.notify_clients(Ok(a.clone())).await;
6689 } else {
6690 self.notify_clients(Err(
6691 "JACK runtime is not active; open the JACK backend first".to_string(),
6692 ))
6693 .await;
6694 }
6695 }
6696 #[cfg(not(unix))]
6697 {
6698 self.notify_clients(Err(
6699 "JACK backend is not available on this platform build".to_string(),
6700 ))
6701 .await;
6702 }
6703 }
6704 Action::JackRemoveAudioOutputPort(_removed_port) => {
6705 #[cfg(unix)]
6706 {
6707 let removed_port = _removed_port;
6708 if let Some(jack) = self.jack_runtime.clone() {
6709 let (removed_port, removed_io) = {
6710 let jack = jack.lock();
6711 let removed_port = Some(removed_port);
6712 let removed_io =
6713 removed_port.and_then(|port| jack.output_audio_port(port));
6714 match (removed_port, removed_io) {
6715 (Some(port), Some(io)) => (port, io),
6716 _ => {
6717 self.notify_clients(Err(
6718 "JACK audio output port index is out of range".to_string(),
6719 ))
6720 .await;
6721 return;
6722 }
6723 }
6724 };
6725 let reindex_notifications =
6726 self.reindex_notifications_for_removed_hw_output(removed_port);
6727 for disconnect in
6728 self.disconnect_actions_for_removed_hw_output(removed_port, &removed_io)
6729 {
6730 if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await
6731 {
6732 self.notify_clients(Err(e)).await;
6733 return;
6734 }
6735 }
6736 let (input_channels, output_channels, rate) = {
6737 let jack = jack.lock();
6738 if let Err(e) = jack.remove_audio_output_port(removed_port) {
6739 self.notify_clients(Err(e)).await;
6740 return;
6741 }
6742 (
6743 jack.input_channels(),
6744 jack.output_channels(),
6745 jack.sample_rate,
6746 )
6747 };
6748 for action in reindex_notifications {
6749 self.notify_clients(Ok(action)).await;
6750 }
6751 self.publish_hw_infos(input_channels, output_channels, rate)
6752 .await;
6753 self.notify_clients(Ok(a.clone())).await;
6754 } else {
6755 self.notify_clients(Err(
6756 "JACK runtime is not active; open the JACK backend first".to_string(),
6757 ))
6758 .await;
6759 }
6760 }
6761 #[cfg(not(unix))]
6762 {
6763 self.notify_clients(Err(
6764 "JACK backend is not available on this platform build".to_string(),
6765 ))
6766 .await;
6767 }
6768 }
6769 Action::OpenMidiInputDevice(ref device) => {
6770 let midi_hub = self.midi_hub.lock();
6771 if let Err(e) = midi_hub.open_input(device) {
6772 self.notify_clients(Err(e)).await;
6773 return;
6774 }
6775 }
6776 Action::OpenMidiOutputDevice(ref device) => {
6777 let midi_hub = self.midi_hub.lock();
6778 if let Err(e) = midi_hub.open_output(device) {
6779 self.notify_clients(Err(e)).await;
6780 return;
6781 }
6782 }
6783 Action::RequestSessionDiagnostics => {
6784 let (
6785 track_count,
6786 frozen_track_count,
6787 audio_clip_count,
6788 midi_clip_count,
6789 lv2_instance_count,
6790 vst3_instance_count,
6791 clap_instance_count,
6792 ) = {
6793 let tracks = &self.state.lock().tracks;
6794 let mut track_count = 0usize;
6795 let mut frozen_track_count = 0usize;
6796 let mut audio_clip_count = 0usize;
6797 let mut midi_clip_count = 0usize;
6798 #[cfg(all(unix, not(target_os = "macos")))]
6799 let mut lv2_instance_count = 0usize;
6800 #[cfg(not(all(unix, not(target_os = "macos"))))]
6801 let lv2_instance_count = 0usize;
6802 let mut vst3_instance_count = 0usize;
6803 let mut clap_instance_count = 0usize;
6804 for track in tracks.values() {
6805 let t = track.lock();
6806 track_count += 1;
6807 if t.frozen {
6808 frozen_track_count += 1;
6809 }
6810 audio_clip_count += t.audio.clips.len();
6811 midi_clip_count += t.midi.clips.len();
6812 #[cfg(all(unix, not(target_os = "macos")))]
6813 {
6814 lv2_instance_count += t.lv2_processors.len();
6815 }
6816 vst3_instance_count += t.vst3_processors.len();
6817 clap_instance_count += t.clap_plugins.len();
6818 }
6819 (
6820 track_count,
6821 frozen_track_count,
6822 audio_clip_count,
6823 midi_clip_count,
6824 lv2_instance_count,
6825 vst3_instance_count,
6826 clap_instance_count,
6827 )
6828 };
6829 #[cfg(not(all(unix, not(target_os = "macos"))))]
6830 let _lv2_instance_count = lv2_instance_count;
6831 let pending_hw_midi_events = self.pending_hw_midi_events.len()
6832 + self
6833 .pending_hw_midi_events_by_device
6834 .values()
6835 .map(std::vec::Vec::len)
6836 .sum::<usize>();
6837 let sample_rate_hz = if let Some(hw) = &self.hw_driver {
6838 hw.lock().sample_rate() as usize
6839 } else {
6840 #[cfg(unix)]
6841 {
6842 self.jack_runtime
6843 .as_ref()
6844 .map(|j| j.lock().sample_rate)
6845 .unwrap_or(0)
6846 }
6847 #[cfg(not(unix))]
6848 0
6849 };
6850 let cycle_samples = self.current_cycle_samples();
6851 self.notify_clients(Ok(Action::SessionDiagnosticsReport {
6852 track_count,
6853 frozen_track_count,
6854 audio_clip_count,
6855 midi_clip_count,
6856 #[cfg(all(unix, not(target_os = "macos")))]
6857 lv2_instance_count,
6858 vst3_instance_count,
6859 clap_instance_count,
6860 pending_requests: self.pending_requests.len(),
6861 workers_total: self.workers.len(),
6862 workers_ready: self.ready_workers.len(),
6863 pending_hw_midi_events,
6864 playing: self.playing,
6865 transport_sample: self.transport_sample,
6866 tempo_bpm: self.tempo_bpm,
6867 sample_rate_hz,
6868 cycle_samples,
6869 }))
6870 .await;
6871 }
6872 Action::RequestMidiLearnMappingsReport => {
6873 let mut lines = Vec::<String>::new();
6874 let fmt_binding = |b: &crate::message::MidiLearnBinding| {
6875 let device = b.device.as_deref().unwrap_or("*");
6876 format!("{device} CH{} CC{}", b.channel + 1, b.cc)
6877 };
6878 if let Some(b) = self.global_midi_learn_play_pause.as_ref() {
6879 lines.push(format!("Global PlayPause: {}", fmt_binding(b)));
6880 }
6881 if let Some(b) = self.global_midi_learn_stop.as_ref() {
6882 lines.push(format!("Global Stop: {}", fmt_binding(b)));
6883 }
6884 if let Some(b) = self.global_midi_learn_record_toggle.as_ref() {
6885 lines.push(format!("Global RecordToggle: {}", fmt_binding(b)));
6886 }
6887 for (track_name, track) in self.state.lock().tracks.iter() {
6888 let t = track.lock();
6889 if let Some(b) = t.midi_learn_volume.as_ref() {
6890 lines.push(format!("{} Volume: {}", track_name, fmt_binding(b)));
6891 }
6892 if let Some(b) = t.midi_learn_balance.as_ref() {
6893 lines.push(format!("{} Balance: {}", track_name, fmt_binding(b)));
6894 }
6895 if let Some(b) = t.midi_learn_mute.as_ref() {
6896 lines.push(format!("{} Mute: {}", track_name, fmt_binding(b)));
6897 }
6898 if let Some(b) = t.midi_learn_solo.as_ref() {
6899 lines.push(format!("{} Solo: {}", track_name, fmt_binding(b)));
6900 }
6901 if let Some(b) = t.midi_learn_arm.as_ref() {
6902 lines.push(format!("{} Arm: {}", track_name, fmt_binding(b)));
6903 }
6904 if let Some(b) = t.midi_learn_input_monitor.as_ref() {
6905 lines.push(format!("{} InputMonitor: {}", track_name, fmt_binding(b)));
6906 }
6907 if let Some(b) = t.midi_learn_disk_monitor.as_ref() {
6908 lines.push(format!("{} DiskMonitor: {}", track_name, fmt_binding(b)));
6909 }
6910 }
6911 if lines.is_empty() {
6912 lines.push("No MIDI learn mappings configured".to_string());
6913 }
6914 self.notify_clients(Ok(Action::MidiLearnMappingsReport { lines }))
6915 .await;
6916 }
6917 Action::ClearAllMidiLearnBindings => {
6918 self.pending_midi_learn = None;
6919 self.pending_global_midi_learn = None;
6920 self.global_midi_learn_play_pause = None;
6921 self.global_midi_learn_stop = None;
6922 self.global_midi_learn_record_toggle = None;
6923 self.midi_cc_gate.clear();
6924 for track in self.state.lock().tracks.values() {
6925 let t = track.lock();
6926 t.midi_learn_volume = None;
6927 t.midi_learn_balance = None;
6928 t.midi_learn_mute = None;
6929 t.midi_learn_solo = None;
6930 t.midi_learn_arm = None;
6931 t.midi_learn_input_monitor = None;
6932 t.midi_learn_disk_monitor = None;
6933 }
6934 }
6935 #[cfg(all(unix, not(target_os = "macos")))]
6936 Action::TrackLv2PluginControls { .. } => {}
6937 #[cfg(all(unix, not(target_os = "macos")))]
6938 Action::ClipLv2PluginControls { .. } => {}
6939 #[cfg(all(unix, not(target_os = "macos")))]
6940 Action::ClipLv2StateSnapshot { .. } => {}
6941 #[cfg(all(unix, not(target_os = "macos")))]
6942 Action::TrackLv2Midnam { .. } => {}
6943 Action::TrackClapNoteNames { .. } => {}
6944 Action::SessionDiagnosticsReport { .. } => {}
6945 Action::MidiLearnMappingsReport { .. } => {}
6946 Action::HWInfo { .. } => {}
6947 Action::HistoryState { .. } => {}
6948 Action::Undo => {}
6949 Action::Redo => {}
6950 Action::ApplyGroupedActions(_) => {}
6951 Action::TrackClapProcessor { .. } => {}
6952 Action::ClipClapProcessor { .. } => {}
6953 }
6954
6955 if let Some(inverse) = inverse_actions {
6956 if let Some(group) = self.history_group.as_mut() {
6957 group.forward_actions.push(action_to_process.clone());
6958 group.inverse_actions.splice(0..0, inverse);
6959 } else {
6960 self.history.record(UndoEntry {
6961 forward_actions: vec![action_to_process.clone()],
6962 inverse_actions: inverse,
6963 });
6964 }
6965 }
6966
6967 self.notify_clients(Ok(action_to_process)).await;
6968 }
6969
6970 pub async fn work(&mut self) {
6971 while let Some(message) = self.rx.recv().await {
6972 match message {
6973 Message::Ready(id) => {
6974 self.ready_workers.push(id);
6975 }
6976 Message::Finished {
6977 worker_id,
6978 track_name,
6979 output_linear,
6980 process_epoch,
6981 } => {
6982 self.ready_workers.push(worker_id);
6983 self.track_processing_started_at.remove(&track_name);
6984 if process_epoch != self.track_process_epoch {
6985 if let Some(track) = self.state.lock().tracks.get(&track_name).cloned() {
6986 let t = track.lock();
6987 t.audio.finished = false;
6988 t.audio.processing = false;
6989 }
6990 continue;
6991 }
6992 self.track_meter_linear_by_track
6993 .insert(track_name, output_linear);
6994 self.force_stalled_track_completions();
6995 let all_finished = self.send_tracks().await;
6996 if all_finished {
6997 self.on_all_tracks_finished().await;
6998 }
6999 }
7000 Message::Channel(s) => {
7001 self.clients.push(s);
7002 }
7003
7004 Message::Request(a) => match a {
7005 Action::TrackOfflineBounceCancel { track_name } => {
7006 if let Some(job) = self.offline_bounce_jobs.get(&track_name) {
7007 job.cancel.store(true, Ordering::Relaxed);
7008 }
7009 }
7010 Action::TrackOfflineBounceCancelAll => {
7011 for job in self.offline_bounce_jobs.values() {
7012 job.cancel.store(true, Ordering::Relaxed);
7013 }
7014 }
7015 _ if !self.offline_bounce_jobs.is_empty() => {
7016 self.pending_requests.push_back(a);
7017 }
7018 Action::OpenAudioDevice { .. }
7019 | Action::OpenMidiInputDevice(_)
7020 | Action::OpenMidiOutputDevice(_)
7021 | Action::RequestMeterSnapshot
7022 | Action::Quit
7023 | Action::Play
7024 | Action::Pause
7025 | Action::Stop
7026 | Action::TransportPosition(_)
7027 | Action::JumpToEnd
7028 | Action::SetLoopEnabled(_)
7029 | Action::SetLoopRange(_)
7030 | Action::SetPunchEnabled(_)
7031 | Action::SetPunchRange(_)
7032 | Action::SetMetronomeEnabled(_)
7033 | Action::SetTempo(_)
7034 | Action::SetTimeSignature { .. }
7035 | Action::SetOscEnabled(_)
7036 | Action::SetClipPlaybackEnabled(_)
7037 | Action::SetRecordEnabled(_)
7038 | Action::SetSessionPath(_)
7039 | Action::ClearHistory
7040 | Action::BeginSessionRestore
7041 | Action::PianoKey { .. }
7042 | Action::ModifyMidiNotes { .. }
7043 | Action::ModifyMidiControllers { .. }
7044 | Action::DeleteMidiControllers { .. }
7045 | Action::InsertMidiControllers { .. }
7046 | Action::DeleteMidiNotes { .. }
7047 | Action::InsertMidiNotes { .. }
7048 | Action::SetMidiSysExEvents { .. } => {
7049 self.handle_request(a).await;
7050 }
7051 #[cfg(all(unix, not(target_os = "macos")))]
7052 Action::ListLv2Plugins => {
7053 self.handle_request(a).await;
7054 }
7055 Action::ListVst3Plugins => {
7056 self.handle_request(a).await;
7057 }
7058 Action::ListClapPlugins => {
7059 self.handle_request(a).await;
7060 }
7061 Action::ListClapPluginsWithCapabilities => {
7062 self.handle_request(a).await;
7063 }
7064 _ => {
7065 self.pending_requests.push_back(a);
7066 if self.can_schedule_hw_cycle() {
7067 self.request_hw_cycle().await;
7068 } else {
7069 while let Some(next) = self.pending_requests.pop_front() {
7070 self.handle_request(next).await;
7071 }
7072 }
7073 }
7074 },
7075 Message::OfflineBounceFinished { result } => {
7076 if let Ok(Action::TrackOfflineBounce { track_name, .. }) = &result {
7077 self.offline_bounce_jobs.remove(track_name);
7078 }
7079 self.notify_clients(result).await;
7080 if self.offline_bounce_jobs.is_empty() {
7081 while let Some(next) = self.pending_requests.pop_front() {
7082 self.handle_request(next).await;
7083 }
7084 }
7085 }
7086 Message::HWFinished => {
7087 if !self.awaiting_hwfinished {
7088 continue;
7089 }
7090 self.handling_hwfinished = true;
7091 self.awaiting_hwfinished = false;
7092 #[cfg(unix)]
7093 {
7094 if let Some(jack) = &self.jack_runtime {
7095 if !self.pending_hw_midi_out_events.is_empty() {
7096 let out_events =
7097 std::mem::take(&mut self.pending_hw_midi_out_events);
7098 jack.lock().write_events(&out_events);
7099 }
7100 let mut in_events = vec![];
7101 jack.lock().read_events_into(&mut in_events);
7102 if !in_events.is_empty() {
7103 self.pending_hw_midi_events.extend(in_events);
7104 }
7105 }
7106 }
7107 #[cfg(unix)]
7108 if self.jack_runtime.is_some() {
7109 self.sync_from_jack_transport().await;
7110 }
7111 while let Some(a) = self.pending_requests.pop_front() {
7112 self.handle_request(a).await;
7113 }
7114 self.apply_mute_solo_policy();
7115 self.append_recorded_cycle();
7116 self.flush_completed_recordings().await;
7117 let hw_in_routes = self.midi_hw_in_routes.clone();
7118 let pending_hw_in_by_device = self.pending_hw_midi_events_by_device.clone();
7119 let mut reconfigured_tracks = Vec::new();
7120 for (track_name, track) in self.state.lock().tracks.iter() {
7121 let track_lock = track.lock();
7122 if self.jack_runtime_is_some() {
7123 if !self.pending_hw_midi_events.is_empty() {
7124 track_lock.push_hw_midi_events(&self.pending_hw_midi_events);
7125 }
7126 } else {
7127 for route in hw_in_routes.iter().filter(|r| &r.to_track == track_name) {
7128 if let Some(events) = pending_hw_in_by_device.get(&route.device) {
7129 track_lock.push_hw_midi_events_to_port(route.to_port, events);
7130 }
7131 }
7132 }
7133 if track_lock.setup() {
7134 reconfigured_tracks.push(track_name.clone());
7135 }
7136 }
7137 self.publish_track_meters().await;
7138 for track_name in reconfigured_tracks {
7139 let track = self.state.lock().tracks.get(&track_name).cloned();
7140 if let Some(track) = track {
7141 let (plugins, connections) = {
7142 let track_lock = track.lock();
7143 (
7144 track_lock.plugin_graph_plugins(),
7145 track_lock.plugin_graph_connections(),
7146 )
7147 };
7148 self.notify_clients(Ok(Action::TrackPluginGraph {
7149 track_name: track_name.clone(),
7150 plugins,
7151 connections,
7152 }))
7153 .await;
7154 }
7155 }
7156 self.pending_hw_midi_events.clear();
7157 self.pending_hw_midi_events_by_device.clear();
7158 if self.playing {
7159 if self.transport_panic_flush_pending {
7160 self.transport_panic_flush_pending = false;
7161 } else if self.transport_restart_pending {
7162 self.transport_restart_pending = false;
7163 } else {
7164 let next = self
7165 .transport_sample
7166 .saturating_add(self.current_cycle_samples());
7167 let normalized = self.normalize_transport_sample(next);
7168 let wrapped = normalized != next;
7169 self.transport_sample = normalized;
7170 if wrapped {
7171 self.notify_clients(Ok(Action::TransportPosition(
7172 self.transport_sample,
7173 )))
7174 .await;
7175 }
7176 }
7177 }
7178 if self.send_tracks().await && self.hw_worker.is_some() {
7179 self.request_hw_cycle().await;
7180 }
7181 #[cfg(unix)]
7182 {
7183 if self.jack_runtime.is_some() {
7184 self.awaiting_hwfinished = true;
7185 }
7186 }
7187 self.handling_hwfinished = false;
7188 }
7189 Message::HWMidiEvents(events) => {
7190 for hw_event in events {
7191 let thru_targets: Vec<String> = self
7192 .midi_hw_thru_routes
7193 .iter()
7194 .filter(|route| route.from_device == hw_event.device)
7195 .map(|route| route.to_device.clone())
7196 .collect();
7197 for device in thru_targets {
7198 self.pending_hw_midi_out_events_by_device.push(HwMidiEvent {
7199 device,
7200 event: hw_event.event.clone(),
7201 });
7202 }
7203 if hw_event.event.data.len() >= 3 {
7204 let status = hw_event.event.data[0];
7205 if status & 0xF0 == 0xB0 {
7206 let channel = status & 0x0F;
7207 let cc = hw_event.event.data[1];
7208 let value = hw_event.event.data[2];
7209 self.handle_incoming_hw_cc(&hw_event.device, channel, cc, value)
7210 .await;
7211 }
7212 }
7213 self.pending_hw_midi_events_by_device
7214 .entry(hw_event.device)
7215 .or_default()
7216 .push(hw_event.event);
7217 }
7218 }
7219 _ => {}
7220 }
7221 }
7222 }
7223
7224 fn collect_hw_midi_output_events(&self) -> Vec<MidiEvent> {
7225 let mut events = vec![];
7226 for track in self.state.lock().tracks.values() {
7227 events.extend(
7228 track
7229 .lock()
7230 .take_hw_midi_out_events()
7231 .into_iter()
7232 .map(|evt| evt.event),
7233 );
7234 }
7235 events.sort_by_key(|a| a.frame);
7236 events
7237 }
7238
7239 fn collect_hw_midi_output_events_by_device(&mut self) -> Vec<HwMidiEvent> {
7240 let mut events = Vec::<HwMidiEvent>::new();
7241 let routes = self.midi_hw_out_routes.clone();
7242 let mut events_by_track = HashMap::<String, Vec<crate::track::HwMidiOutEvent>>::new();
7243 {
7244 let state = self.state.lock();
7245 for route in &routes {
7246 if events_by_track.contains_key(&route.from_track) {
7247 continue;
7248 }
7249 let Some(track) = state.tracks.get(&route.from_track) else {
7250 continue;
7251 };
7252 events_by_track.insert(
7253 route.from_track.clone(),
7254 track.lock().take_hw_midi_out_events(),
7255 );
7256 }
7257 }
7258
7259 for route in routes {
7260 let Some(track_events) = events_by_track.get(&route.from_track) else {
7261 continue;
7262 };
7263 for hw_event in track_events
7264 .iter()
7265 .filter(|evt| evt.port == route.from_port)
7266 {
7267 self.update_active_hw_notes_for_track(
7268 &route.from_track,
7269 &route.device,
7270 &hw_event.event.data,
7271 );
7272 events.push(HwMidiEvent {
7273 device: route.device.clone(),
7274 event: hw_event.event.clone(),
7275 });
7276 }
7277 }
7278 events.sort_by(|a, b| {
7279 a.event
7280 .frame
7281 .cmp(&b.event.frame)
7282 .then_with(|| a.device.cmp(&b.device))
7283 });
7284 events
7285 }
7286}
7287
7288#[cfg(test)]
7289mod tests {
7290 use super::*;
7291 use crate::mutex::UnsafeMutex;
7292 use tokio::sync::mpsc::channel;
7293 use tokio::time::{Duration as TokioDuration, timeout};
7294
7295 #[test]
7296 #[cfg(unix)]
7297 fn jack_transport_sync_decision_starts_and_syncs_position_on_external_play() {
7298 let decision = Engine::jack_transport_sync_decision(false, 128, true, 256, 64);
7299
7300 assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Start));
7301 assert_eq!(decision.position_sync, Some(256));
7302 }
7303
7304 #[test]
7305 #[cfg(unix)]
7306 fn jack_transport_sync_decision_stops_and_syncs_position_on_external_stop() {
7307 let decision = Engine::jack_transport_sync_decision(true, 512, false, 96, 64);
7308
7309 assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Stop));
7310 assert_eq!(decision.position_sync, Some(96));
7311 }
7312
7313 #[test]
7314 #[cfg(unix)]
7315 fn jack_transport_sync_decision_ignores_small_rolling_drift() {
7316 let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1040, 64);
7317
7318 assert_eq!(decision.play_sync, None);
7319 assert_eq!(decision.position_sync, None);
7320 }
7321
7322 #[test]
7323 #[cfg(unix)]
7324 fn jack_transport_sync_decision_syncs_large_rolling_jump() {
7325 let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1200, 64);
7326
7327 assert_eq!(decision.play_sync, None);
7328 assert_eq!(decision.position_sync, Some(1200));
7329 }
7330
7331 #[test]
7332 #[cfg(unix)]
7333 fn jack_transport_sync_decision_syncs_locate_while_stopped() {
7334 let decision = Engine::jack_transport_sync_decision(false, 400, false, 900, 64);
7335
7336 assert_eq!(decision.play_sync, None);
7337 assert_eq!(decision.position_sync, Some(900));
7338 }
7339
7340 fn make_engine_with_client() -> (Engine, tokio::sync::mpsc::Receiver<Message>) {
7341 let (engine_tx, engine_rx) = channel(16);
7342 let mut engine = Engine::new(engine_rx, engine_tx);
7343 let (client_tx, client_rx) = channel(16);
7344 engine.clients.push(client_tx);
7345 (engine, client_rx)
7346 }
7347
7348 fn insert_track(engine: &mut Engine, track: Track) {
7349 engine.state.lock().tracks.insert(
7350 track.name.clone(),
7351 Arc::new(UnsafeMutex::new(Box::new(track))),
7352 );
7353 }
7354
7355 fn osc_packet(address: &str) -> Vec<u8> {
7356 fn push_padded_osc_string(packet: &mut Vec<u8>, value: &str) {
7357 packet.extend_from_slice(value.as_bytes());
7358 packet.push(0);
7359 while !packet.len().is_multiple_of(4) {
7360 packet.push(0);
7361 }
7362 }
7363
7364 let mut packet = Vec::new();
7365 push_padded_osc_string(&mut packet, address);
7366 push_padded_osc_string(&mut packet, ",");
7367 packet
7368 }
7369
7370 #[tokio::test]
7371 async fn set_osc_enabled_starts_and_stops_server() {
7372 let (mut engine, _client_rx) = make_engine_with_client();
7373
7374 engine
7375 .set_osc_enabled_with(true, |tx| OscServer::start_on_addr(tx, "127.0.0.1:0"))
7376 .expect("start osc server on ephemeral port");
7377 assert!(engine.osc_server.is_some());
7378
7379 engine
7380 .set_osc_enabled_with(false, OscServer::start)
7381 .expect("stop osc server");
7382 assert!(engine.osc_server.is_none());
7383 }
7384
7385 #[tokio::test]
7386 async fn osc_server_forwards_transport_packets_to_engine_channel() {
7387 let (tx, mut rx) = channel(4);
7388 let mut server =
7389 OscServer::start_on_addr(tx, "127.0.0.1:0").expect("start osc test server");
7390 let socket = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind sender socket");
7391 let packet = osc_packet("/transport/play");
7392 socket
7393 .send_to(&packet, server.listen_addr())
7394 .expect("send osc packet");
7395
7396 let message = timeout(TokioDuration::from_secs(1), rx.recv())
7397 .await
7398 .expect("packet delivery timeout")
7399 .expect("osc message");
7400 match message {
7401 Message::Request(Action::Play) => {}
7402 other => panic!("unexpected osc message: {other:?}"),
7403 }
7404
7405 server.stop();
7406 }
7407
7408 #[tokio::test]
7409 async fn track_offline_bounce_rejects_zero_length_requests() {
7410 let (mut engine, mut client_rx) = make_engine_with_client();
7411 insert_track(
7412 &mut engine,
7413 Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
7414 );
7415
7416 engine
7417 .handle_request(Action::TrackOfflineBounce {
7418 track_name: "track".to_string(),
7419 output_path: "/tmp/out.wav".to_string(),
7420 start_sample: 0,
7421 length_samples: 0,
7422 automation_lanes: vec![],
7423 apply_fader: false,
7424 })
7425 .await;
7426
7427 match client_rx.recv().await.expect("response") {
7428 Message::Response(Err(err)) => {
7429 assert!(err.contains("has no renderable content for offline bounce"));
7430 }
7431 other => panic!("unexpected message: {other:?}"),
7432 }
7433 }
7434
7435 #[tokio::test]
7436 async fn track_offline_bounce_rejects_when_same_track_is_active() {
7437 let (mut engine, mut client_rx) = make_engine_with_client();
7438 engine.offline_bounce_jobs.insert(
7439 "other".to_string(),
7440 OfflineBounceJob {
7441 cancel: Arc::new(AtomicBool::new(false)),
7442 },
7443 );
7444
7445 engine
7446 .handle_request(Action::TrackOfflineBounce {
7447 track_name: "other".to_string(),
7448 output_path: "/tmp/out.wav".to_string(),
7449 start_sample: 0,
7450 length_samples: 128,
7451 automation_lanes: vec![],
7452 apply_fader: false,
7453 })
7454 .await;
7455
7456 match client_rx.recv().await.expect("response") {
7457 Message::Response(Err(err)) => {
7458 assert!(err.contains("already in progress"));
7459 }
7460 other => panic!("unexpected message: {other:?}"),
7461 }
7462 }
7463
7464 #[tokio::test]
7465 async fn track_offline_bounce_allows_different_track_concurrently() {
7466 let (mut engine, mut 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 engine.offline_bounce_jobs.insert(
7472 "other".to_string(),
7473 OfflineBounceJob {
7474 cancel: Arc::new(AtomicBool::new(false)),
7475 },
7476 );
7477
7478 engine
7479 .handle_request(Action::TrackOfflineBounce {
7480 track_name: "track".to_string(),
7481 output_path: "/tmp/out.wav".to_string(),
7482 start_sample: 0,
7483 length_samples: 128,
7484 automation_lanes: vec![],
7485 apply_fader: false,
7486 })
7487 .await;
7488
7489 assert!(engine.offline_bounce_jobs.contains_key("other"));
7490 assert_eq!(engine.pending_requests.len(), 1);
7491 }
7492
7493 #[tokio::test]
7494 async fn reject_if_track_frozen_sends_error_and_blocks_operation() {
7495 let (mut engine, mut client_rx) = make_engine_with_client();
7496 let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
7497 track.set_frozen(true);
7498 insert_track(&mut engine, track);
7499
7500 let rejected = engine
7501 .reject_if_track_frozen("track", "arming/disarming")
7502 .await;
7503
7504 assert!(rejected);
7505 match client_rx.recv().await.expect("response") {
7506 Message::Response(Err(err)) => {
7507 assert_eq!(err, "Track 'track' is frozen; arming/disarming is blocked");
7508 }
7509 other => panic!("unexpected message: {other:?}"),
7510 }
7511 }
7512
7513 #[tokio::test]
7514 async fn undo_restores_original_clip_bounds_after_stretch_style_group() {
7515 let (mut engine, _client_rx) = make_engine_with_client();
7516 let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
7517 let mut clip = AudioClip::new("audio/original.wav".to_string(), 100, 220);
7518 clip.offset = 12;
7519 clip.fade_in_samples = 20;
7520 clip.fade_out_samples = 30;
7521 track.audio.clips.push(clip);
7522 insert_track(&mut engine, track);
7523
7524 engine.handle_request(Action::BeginHistoryGroup).await;
7525 engine
7526 .handle_request(Action::SetClipBounds {
7527 track_name: "track".to_string(),
7528 clip_index: 0,
7529 kind: Kind::Audio,
7530 start: 120,
7531 length: 180,
7532 offset: 0,
7533 })
7534 .await;
7535 engine
7536 .handle_request(Action::SetClipSourceName {
7537 track_name: "track".to_string(),
7538 clip_index: 0,
7539 kind: Kind::Audio,
7540 name: "audio/stretched.wav".to_string(),
7541 })
7542 .await;
7543 engine
7544 .handle_request(Action::SetClipFade {
7545 track_name: "track".to_string(),
7546 clip_index: 0,
7547 kind: Kind::Audio,
7548 fade_enabled: true,
7549 fade_in_samples: 12,
7550 fade_out_samples: 12,
7551 })
7552 .await;
7553 engine.handle_request(Action::EndHistoryGroup).await;
7554
7555 engine.handle_request(Action::Undo).await;
7556
7557 let state = engine.state.lock();
7558 let track = state.tracks.get("track").expect("track exists").lock();
7559 let clip = track.audio.clips.first().expect("clip exists");
7560 assert_eq!(clip.name, "audio/original.wav");
7561 assert_eq!(clip.start, 100);
7562 assert_eq!(clip.end, 220);
7563 assert_eq!(clip.end.saturating_sub(clip.start), 120);
7564 assert_eq!(clip.offset, 12);
7565 }
7566
7567 #[tokio::test]
7568 async fn track_offline_bounce_queues_when_no_worker_is_ready() {
7569 let (mut engine, _client_rx) = make_engine_with_client();
7570 insert_track(
7571 &mut engine,
7572 Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
7573 );
7574
7575 engine
7576 .handle_request(Action::TrackOfflineBounce {
7577 track_name: "track".to_string(),
7578 output_path: "/tmp/out.wav".to_string(),
7579 start_sample: 0,
7580 length_samples: 128,
7581 automation_lanes: vec![],
7582 apply_fader: false,
7583 })
7584 .await;
7585
7586 assert!(engine.offline_bounce_jobs.is_empty());
7587 assert_eq!(engine.pending_requests.len(), 1);
7588 assert!(matches!(
7589 engine.pending_requests.front(),
7590 Some(Action::TrackOfflineBounce { track_name, length_samples, .. })
7591 if track_name == "track" && *length_samples == 128
7592 ));
7593 }
7594
7595 #[tokio::test]
7596 async fn track_offline_bounce_returns_missing_track_error() {
7597 let (mut engine, mut client_rx) = make_engine_with_client();
7598
7599 engine
7600 .handle_request(Action::TrackOfflineBounce {
7601 track_name: "missing".to_string(),
7602 output_path: "/tmp/out.wav".to_string(),
7603 start_sample: 0,
7604 length_samples: 128,
7605 automation_lanes: vec![],
7606 apply_fader: false,
7607 })
7608 .await;
7609
7610 match client_rx.recv().await.expect("response") {
7611 Message::Response(Err(err)) => {
7612 assert_eq!(err, "Track not found: missing");
7613 }
7614 other => panic!("unexpected message: {other:?}"),
7615 }
7616 }
7617
7618 #[tokio::test]
7619 async fn track_offline_bounce_clears_job_when_worker_send_fails() {
7620 let (mut engine, mut client_rx) = make_engine_with_client();
7621 insert_track(
7622 &mut engine,
7623 Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
7624 );
7625 let (worker_tx, worker_rx) = channel(1);
7626 drop(worker_rx);
7627 engine
7628 .workers
7629 .push(WorkerData::new(worker_tx, tokio::spawn(async {})));
7630 engine.ready_workers.push(0);
7631
7632 engine
7633 .handle_request(Action::TrackOfflineBounce {
7634 track_name: "track".to_string(),
7635 output_path: "/tmp/out.wav".to_string(),
7636 start_sample: 0,
7637 length_samples: 128,
7638 automation_lanes: vec![],
7639 apply_fader: false,
7640 })
7641 .await;
7642
7643 assert!(engine.offline_bounce_jobs.is_empty());
7644 match client_rx.recv().await.expect("response") {
7645 Message::Response(Err(err)) => {
7646 assert!(err.contains("Failed to schedule offline bounce"));
7647 }
7648 other => panic!("unexpected message: {other:?}"),
7649 }
7650 }
7651}