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