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