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.notify_clients(Ok(Action::Quit)).await;
4707 self.ready_realtime_workers.clear();
4708 self.ready_refill_workers.clear();
4709 while !self.workers.is_empty() {
4710 let worker = self.workers.remove(0);
4711 if let Err(e) = worker.tx.send(Message::Request(a.clone())).await {
4712 error!("Error sending quit message to worker: {e}");
4713 }
4714 worker
4715 .handle
4716 .await
4717 .unwrap_or_else(|e| error!("Error waiting for worker to quit: {e}"));
4718 }
4719
4720 if let Some(worker) = self.hw_worker.take() {
4721 if let Some(hw) = &self.hw_driver {
4722 hw.lock().request_stop();
4723 }
4724 let mut panic_events = self.note_off_events_for_all_active_tracks();
4725 panic_events.extend(self.panic_events_for_all_hw_midi_outputs());
4726 if !panic_events.is_empty() {
4727 if let Err(e) = worker.tx.send(Message::ClearHWMidiOutEvents).await {
4728 error!("Error clearing HW MIDI queue during quit {e}");
4729 }
4730 self.midi_hub
4731 .lock()
4732 .write_events_blocking(&panic_events, Duration::from_millis(250));
4733 }
4734 if let Err(e) = worker.tx.send(Message::Request(a.clone())).await {
4735 error!("Error sending quit message to HW worker: {e}");
4736 }
4737 worker
4738 .handle
4739 .await
4740 .unwrap_or_else(|e| error!("Error waiting for HW worker to quit: {e}"));
4741 }
4742 #[cfg(unix)]
4743 {
4744 self.jack_runtime = None;
4745 }
4746 self.osc_server = None;
4747 return;
4748 }
4749 Action::AddTrack {
4750 ref name,
4751 audio_ins,
4752 midi_ins,
4753 audio_outs,
4754 midi_outs,
4755 } => {
4756 let tracks = &mut self.state.lock().tracks;
4757 if tracks.contains_key(name) {
4758 self.notify_clients(Err(format!("Track {} already exists", name)))
4759 .await;
4760 return;
4761 }
4762 let maybe_hw = if let Some(oss) = &self.hw_driver {
4763 let hw = oss.lock();
4764 Some((hw.cycle_samples(), hw.sample_rate() as f64))
4765 } else {
4766 #[cfg(unix)]
4767 if let Some(jack) = &self.jack_runtime {
4768 let j = jack.lock();
4769 Some((j.buffer_size, j.sample_rate as f64))
4770 } else {
4771 None
4772 }
4773 #[cfg(not(unix))]
4774 None
4775 };
4776
4777 if let Some((chsamples, sample_rate)) = maybe_hw {
4778 tracks.insert(
4779 name.clone(),
4780 Arc::new(UnsafeMutex::new(Box::new(Track::new(
4781 name.clone(),
4782 audio_ins,
4783 audio_outs,
4784 midi_ins,
4785 midi_outs,
4786 chsamples,
4787 sample_rate,
4788 )))),
4789 );
4790 if let Some(track) = tracks.get(name) {
4791 let t = track.lock();
4792 t.ensure_default_audio_passthrough();
4793 t.ensure_default_midi_passthrough();
4794 t.set_clip_playback_enabled(self.clip_playback_enabled);
4795 t.set_transport_timing(self.tempo_bpm, self.tsig_num, self.tsig_denom);
4796 t.set_session_base_dir(self.session_dir.clone());
4797 t.set_hybrid_enabled(self.hybrid_enabled);
4798 }
4799 } else {
4800 self.notify_clients(Err(
4801 "Engine needs to open audio device before adding audio track".to_string(),
4802 ))
4803 .await;
4804 }
4805 }
4806 Action::TrackAddAudioInput(ref name) => {
4807 let track = match self.track_handle_or_err(name) {
4808 Ok(track) => track,
4809 Err(e) => {
4810 self.notify_clients(Err(e)).await;
4811 return;
4812 }
4813 };
4814 if let Err(e) = track.lock().add_audio_input() {
4815 self.notify_clients(Err(e)).await;
4816 return;
4817 }
4818 }
4819 Action::TrackAddAudioOutput(ref name) => {
4820 let track = match self.track_handle_or_err(name) {
4821 Ok(track) => track,
4822 Err(e) => {
4823 self.notify_clients(Err(e)).await;
4824 return;
4825 }
4826 };
4827 if let Err(e) = track.lock().add_audio_output() {
4828 self.notify_clients(Err(e)).await;
4829 return;
4830 }
4831 }
4832 Action::TrackRemoveAudioInput(ref name) => {
4833 let track = match self.track_handle_or_err(name) {
4834 Ok(track) => track,
4835 Err(e) => {
4836 self.notify_clients(Err(e)).await;
4837 return;
4838 }
4839 };
4840 if let Err(e) = track.lock().remove_audio_input() {
4841 self.notify_clients(Err(e)).await;
4842 return;
4843 }
4844 }
4845 Action::TrackRemoveAudioOutput(ref name) => {
4846 let track = match self.track_handle_or_err(name) {
4847 Ok(track) => track,
4848 Err(e) => {
4849 self.notify_clients(Err(e)).await;
4850 return;
4851 }
4852 };
4853 let (hw_outputs, track_inputs) = {
4854 let state = self.state.lock();
4855 let hw_outputs = self.all_hw_output_audio_ports();
4856 let track_inputs = state
4857 .tracks
4858 .iter()
4859 .filter(|(track_name, _)| *track_name != name)
4860 .flat_map(|(_, handle)| handle.lock().audio.ins.clone())
4861 .collect::<Vec<_>>();
4862 (hw_outputs, track_inputs)
4863 };
4864 if let Err(e) = track.lock().remove_audio_output(&hw_outputs, &track_inputs) {
4865 self.notify_clients(Err(e)).await;
4866 return;
4867 }
4868 }
4869 Action::RenameTrack {
4870 ref old_name,
4871 ref new_name,
4872 } => {
4873 if self.state.lock().tracks.contains_key(new_name) {
4874 self.notify_clients(Err(format!("Track '{}' already exists", new_name)))
4875 .await;
4876 return;
4877 }
4878
4879 let Some(track) = self.state.lock().tracks.remove(old_name) else {
4880 self.notify_clients(Err(format!("Track '{}' not found", old_name)))
4881 .await;
4882 return;
4883 };
4884
4885 track.lock().name = new_name.clone();
4886 self.state.lock().tracks.insert(new_name.clone(), track);
4887 for other in self.state.lock().tracks.values() {
4888 let other = other.lock();
4889 if other.vca_master.as_deref() == Some(old_name.as_str()) {
4890 other.set_vca_master(Some(new_name.clone()));
4891 }
4892 if other.parent_track.as_deref() == Some(old_name.as_str()) {
4893 other.parent_track = Some(new_name.clone());
4894 }
4895 }
4896
4897 if let Some(recording) = self.audio_recordings.remove(old_name) {
4898 self.audio_recordings.insert(new_name.clone(), recording);
4899 }
4900 if let Some(recording) = self.midi_recordings.remove(old_name) {
4901 self.midi_recordings.insert(new_name.clone(), recording);
4902 }
4903
4904 for route in &mut self.midi_hw_in_routes {
4905 if route.to_track == *old_name {
4906 route.to_track = new_name.clone();
4907 }
4908 }
4909 for route in &mut self.midi_hw_out_routes {
4910 if route.from_track == *old_name {
4911 route.from_track = new_name.clone();
4912 }
4913 }
4914 if let Some((armed_track, target, device)) = self.pending_midi_learn.clone()
4915 && armed_track == *old_name
4916 {
4917 self.pending_midi_learn = Some((new_name.clone(), target, device));
4918 }
4919
4920 self.notify_clients(Ok(Action::RenameTrack {
4921 old_name: old_name.clone(),
4922 new_name: new_name.clone(),
4923 }))
4924 .await;
4925 }
4926 Action::RemoveTrack(ref name) => {
4927 let children: Vec<String> = {
4928 let state = self.state.lock();
4929 state
4930 .tracks
4931 .iter()
4932 .filter_map(|(n, t)| {
4933 if t.lock().parent_track.as_deref() == Some(name.as_str()) {
4934 Some(n.clone())
4935 } else {
4936 None
4937 }
4938 })
4939 .collect()
4940 };
4941 if let Some(removed_track) = self.state.lock().tracks.get(name).cloned() {
4942 for child_name in children {
4943 if let Some(child) = self.state.lock().tracks.get(&child_name).cloned() {
4944 let removed = removed_track.lock();
4945 child.lock().disconnect_outputs_from_parent(removed);
4946 child.lock().parent_track = None;
4947 }
4948 }
4949 }
4950 self.state.lock().tracks.remove(name);
4951 self.audio_recordings.remove(name);
4952 self.midi_recordings.remove(name);
4953 self.midi_hw_in_routes.retain(|r| r.to_track != *name);
4954 self.midi_hw_out_routes.retain(|r| r.from_track != *name);
4955 if self
4956 .pending_midi_learn
4957 .as_ref()
4958 .is_some_and(|(track_name, _, _)| track_name == name)
4959 {
4960 self.pending_midi_learn = None;
4961 }
4962 for track in self.state.lock().tracks.values() {
4963 let track = track.lock();
4964 if track.vca_master.as_deref() == Some(name.as_str()) {
4965 track.set_vca_master(None);
4966 }
4967 }
4968 }
4969 Action::TrackLevel(ref name, level) => {
4970 if name == "hw:out" {
4971 self.hw_out_level_db = level;
4972 } else if let Some(track) = self.state.lock().tracks.get(name) {
4973 let previous = track.lock().level();
4974 track.lock().set_level(level);
4975 let delta = level - previous;
4976 if delta.abs() > f32::EPSILON {
4977 for follower_name in self.vca_followers(name) {
4978 if let Some(follower) = self.state.lock().tracks.get(&follower_name) {
4979 let next = (follower.lock().level() + delta).clamp(-90.0, 20.0);
4980 follower.lock().set_level(next);
4981 self.notify_clients(Ok(Action::TrackLevel(
4982 follower_name.clone(),
4983 next,
4984 )))
4985 .await;
4986 }
4987 }
4988 }
4989 }
4990 }
4991 Action::TrackBalance(ref name, balance) => {
4992 if name == "hw:out" {
4993 self.hw_out_balance = balance.clamp(-1.0, 1.0);
4994 } else if let Some(track) = self.state.lock().tracks.get(name) {
4995 track.lock().set_balance(balance);
4996 }
4997 }
4998 Action::TrackAutomationLevel(ref name, level) => {
4999 if let Some(track) = self.state.lock().tracks.get(name) {
5000 let previous = track.lock().level();
5001 track.lock().set_level(level);
5002 let delta = level - previous;
5003 if delta.abs() > f32::EPSILON {
5004 for follower_name in self.vca_followers(name) {
5005 if let Some(follower) = self.state.lock().tracks.get(&follower_name) {
5006 let next = (follower.lock().level() + delta).clamp(-90.0, 20.0);
5007 follower.lock().set_level(next);
5008 self.notify_clients(Ok(Action::TrackAutomationLevel(
5009 follower_name.clone(),
5010 next,
5011 )))
5012 .await;
5013 }
5014 }
5015 }
5016 }
5017 }
5018 Action::TrackAutomationBalance(ref name, balance) => {
5019 if let Some(track) = self.state.lock().tracks.get(name) {
5020 track.lock().set_balance(balance);
5021 }
5022 }
5023 Action::TrackAutomationMute(ref name, muted) => {
5024 if let Some(track) = self.state.lock().tracks.get(name) {
5025 track.lock().set_muted(muted);
5026 for follower_name in self.vca_followers(name) {
5027 if let Some(follower) = self.state.lock().tracks.get(&follower_name) {
5028 follower.lock().set_muted(muted);
5029 self.notify_clients(Ok(Action::TrackAutomationMute(
5030 follower_name.clone(),
5031 muted,
5032 )))
5033 .await;
5034 }
5035 }
5036 }
5037 }
5038 Action::RequestMeterSnapshot => {
5039 self.notify_clients(Ok(Action::MeterSnapshot {
5040 hw_out_db: self.latest_hw_out_meter_db.clone(),
5041 track_meters: self.latest_track_meter_snapshot.clone(),
5042 }))
5043 .await;
5044 return;
5045 }
5046 Action::TrackMeters { .. } => {}
5047 Action::MeterSnapshot { .. } => {}
5048 Action::TrackToggleArm(ref name) => {
5049 if self.reject_if_track_frozen(name, "arming/disarming").await {
5050 return;
5051 }
5052 if let Some(track) = self.state.lock().tracks.get(name).cloned() {
5053 track.lock().arm();
5054 let armed = track.lock().armed;
5055 if !armed && self.audio_recordings.contains_key(name) {
5056 self.flush_track_recording(name).await;
5057 }
5058 } else {
5059 tracing::warn!(
5060 "TrackToggleArm for '{}' but track not found in engine",
5061 name
5062 );
5063 }
5064 }
5065 Action::TrackToggleMute(ref name) => {
5066 if name == "hw:out" {
5067 self.hw_out_muted = !self.hw_out_muted;
5068 } else if let Some(track) = self.state.lock().tracks.get(name) {
5069 track.lock().mute();
5070 let muted = track.lock().muted;
5071 for follower_name in self.vca_followers(name) {
5072 if let Some(follower) = self.state.lock().tracks.get(&follower_name)
5073 && follower.lock().muted != muted
5074 {
5075 follower.lock().set_muted(muted);
5076 self.notify_clients(Ok(Action::TrackToggleMute(follower_name.clone())))
5077 .await;
5078 }
5079 }
5080 }
5081 }
5082 Action::TrackTogglePhase(ref name) => {
5083 if let Some(track) = self.state.lock().tracks.get(name) {
5084 track.lock().invert_phase();
5085 }
5086 }
5087 Action::TrackToggleSolo(ref name) => {
5088 if name == "hw:out" {
5089 return;
5090 }
5091 if let Some(track) = self.state.lock().tracks.get(name) {
5092 track.lock().solo();
5093 let soloed = track.lock().soloed;
5094 for follower_name in self.vca_followers(name) {
5095 if let Some(follower) = self.state.lock().tracks.get(&follower_name)
5096 && follower.lock().soloed != soloed
5097 {
5098 follower.lock().solo();
5099 self.notify_clients(Ok(Action::TrackToggleSolo(follower_name.clone())))
5100 .await;
5101 }
5102 }
5103 }
5104 }
5105 Action::TrackToggleMaster(ref name) => {
5106 if let Some(track) = self.state.lock().tracks.get(name) {
5107 let blocked = {
5108 let t = track.lock();
5109 t.vca_master.is_some() || !self.vca_followers(name).is_empty()
5110 };
5111 if blocked {
5112 self.notify_clients(Err(format!(
5113 "Track '{}' cannot be promoted to Master while part of a VCA group",
5114 name
5115 )))
5116 .await;
5117 return;
5118 }
5119 track.lock().toggle_master();
5120 }
5121 }
5122 Action::TrackToggleInputMonitor(ref name) => {
5123 if let Some(track) = self.state.lock().tracks.get(name) {
5124 track.lock().toggle_input_monitor();
5125 }
5126 }
5127 Action::TrackToggleDiskMonitor(ref name) => {
5128 if let Some(track) = self.state.lock().tracks.get(name) {
5129 track.lock().toggle_disk_monitor();
5130 }
5131 }
5132 Action::TrackSetColor {
5133 ref track_name,
5134 color,
5135 } => {
5136 if let Some(track) = self.state.lock().tracks.get(track_name) {
5137 track.lock().color = color;
5138 }
5139 }
5140 Action::TrackArmMidiLearn {
5141 ref track_name,
5142 target,
5143 } => {
5144 if let Err(e) = self.track_handle_or_err(track_name) {
5145 self.notify_clients(Err(e)).await;
5146 return;
5147 }
5148 self.pending_midi_learn = Some((track_name.clone(), target, None));
5149 }
5150 Action::GlobalArmMidiLearn { target } => {
5151 self.pending_global_midi_learn = Some(target);
5152 }
5153 Action::TrackSetMidiLearnBinding {
5154 ref track_name,
5155 target,
5156 ref binding,
5157 } => {
5158 if let Some(binding) = binding.as_ref() {
5159 let conflicts = self.midi_learn_slot_conflicts(
5160 binding,
5161 Some(MidiLearnSlot::Track(track_name.clone(), target)),
5162 );
5163 if !conflicts.is_empty() {
5164 self.notify_clients(Err(format!(
5165 "MIDI learn conflict for '{}' {:?}: {}",
5166 track_name,
5167 target,
5168 conflicts.join(", ")
5169 )))
5170 .await;
5171 return;
5172 }
5173 }
5174 let track = match self.track_handle_or_err(track_name) {
5175 Ok(track) => track,
5176 Err(e) => {
5177 self.notify_clients(Err(e)).await;
5178 return;
5179 }
5180 };
5181 match target {
5182 crate::message::TrackMidiLearnTarget::Volume => {
5183 track.lock().midi_learn_volume = binding.clone();
5184 }
5185 crate::message::TrackMidiLearnTarget::Balance => {
5186 track.lock().midi_learn_balance = binding.clone();
5187 }
5188 crate::message::TrackMidiLearnTarget::Mute => {
5189 track.lock().midi_learn_mute = binding.clone();
5190 }
5191 crate::message::TrackMidiLearnTarget::Solo => {
5192 track.lock().midi_learn_solo = binding.clone();
5193 }
5194 crate::message::TrackMidiLearnTarget::Arm => {
5195 track.lock().midi_learn_arm = binding.clone();
5196 }
5197 crate::message::TrackMidiLearnTarget::InputMonitor => {
5198 track.lock().midi_learn_input_monitor = binding.clone();
5199 }
5200 crate::message::TrackMidiLearnTarget::DiskMonitor => {
5201 track.lock().midi_learn_disk_monitor = binding.clone();
5202 }
5203 }
5204 }
5205 Action::SetGlobalMidiLearnBinding {
5206 target,
5207 ref binding,
5208 } => {
5209 if let Some(binding) = binding.as_ref() {
5210 let conflicts = self
5211 .midi_learn_slot_conflicts(binding, Some(MidiLearnSlot::Global(target)));
5212 if !conflicts.is_empty() {
5213 self.notify_clients(Err(format!(
5214 "Global MIDI learn conflict for {:?}: {}",
5215 target,
5216 conflicts.join(", ")
5217 )))
5218 .await;
5219 return;
5220 }
5221 }
5222 match target {
5223 crate::message::GlobalMidiLearnTarget::PlayPause => {
5224 self.global_midi_learn_play_pause = binding.clone();
5225 }
5226 crate::message::GlobalMidiLearnTarget::Stop => {
5227 self.global_midi_learn_stop = binding.clone();
5228 }
5229 crate::message::GlobalMidiLearnTarget::RecordToggle => {
5230 self.global_midi_learn_record_toggle = binding.clone();
5231 }
5232 }
5233 }
5234 Action::TrackSetVcaMaster {
5235 ref track_name,
5236 ref master_track,
5237 } => {
5238 let track = match self.track_handle_or_err(track_name) {
5239 Ok(track) => track,
5240 Err(e) => {
5241 self.notify_clients(Err(e)).await;
5242 return;
5243 }
5244 };
5245 if track.lock().is_master {
5246 self.notify_clients(Err(format!(
5247 "Master track '{}' cannot be part of a VCA group",
5248 track_name
5249 )))
5250 .await;
5251 return;
5252 }
5253 if let Some(master_name) = master_track
5254 && master_name == track_name
5255 {
5256 self.notify_clients(Err("Track cannot be its own VCA master".to_string()))
5257 .await;
5258 return;
5259 }
5260 if let Some(master_name) = master_track
5261 && let Some(master) = self.state.lock().tracks.get(master_name)
5262 && master.lock().is_master
5263 {
5264 self.notify_clients(Err(format!(
5265 "Track '{}' cannot be grouped to Master track '{}'",
5266 track_name, master_name
5267 )))
5268 .await;
5269 return;
5270 }
5271 track.lock().set_vca_master(master_track.clone());
5272 }
5273 Action::TrackSetFolder {
5274 ref track_name,
5275 is_folder,
5276 } => {
5277 let track = match self.track_handle_or_err(track_name) {
5278 Ok(track) => track,
5279 Err(e) => {
5280 self.notify_clients(Err(e)).await;
5281 return;
5282 }
5283 };
5284 track.lock().is_folder = is_folder;
5285 self.notify_clients(Ok(Action::TrackSetFolder {
5286 track_name: track_name.clone(),
5287 is_folder,
5288 }))
5289 .await;
5290 }
5291 Action::TrackSetParent {
5292 ref track_name,
5293 ref parent_name,
5294 } => {
5295 let track = match self.track_handle_or_err(track_name) {
5296 Ok(track) => track,
5297 Err(e) => {
5298 self.notify_clients(Err(e)).await;
5299 return;
5300 }
5301 };
5302 if parent_name.as_deref() == Some(track_name.as_str()) {
5303 self.notify_clients(Err("Track cannot be its own parent".to_string()))
5304 .await;
5305 return;
5306 }
5307
5308 let old_parent = {
5309 let t = track.lock();
5310 t.parent_track.clone()
5311 };
5312 if let Some(ref old) = old_parent
5313 && let Some(old_track_arc) = self.state.lock().tracks.get(old).cloned()
5314 {
5315 let old_track = old_track_arc.lock();
5316 track.lock().disconnect_outputs_from_parent(old_track);
5317 }
5318
5319 if let Some(new_parent) = parent_name
5320 && let Some(parent_track_arc) =
5321 self.state.lock().tracks.get(new_parent).cloned()
5322 {
5323 let parent_track = parent_track_arc.lock();
5324 track.lock().connect_outputs_to_parent(parent_track);
5325 }
5326 track.lock().parent_track = parent_name.clone();
5327 self.notify_clients(Ok(Action::TrackSetParent {
5328 track_name: track_name.clone(),
5329 parent_name: parent_name.clone(),
5330 }))
5331 .await;
5332 }
5333 Action::TrackToggleFolder { ref track_name } => {
5334 let track = match self.track_handle_or_err(track_name) {
5335 Ok(track) => track,
5336 Err(e) => {
5337 self.notify_clients(Err(e)).await;
5338 return;
5339 }
5340 };
5341 {
5342 let t = track.lock();
5343 t.folder_open = !t.folder_open;
5344 }
5345 self.notify_clients(Ok(Action::TrackToggleFolder {
5346 track_name: track_name.clone(),
5347 }))
5348 .await;
5349
5350 self.notify_clients(Ok(Action::TrackSetFolder {
5351 track_name: track_name.clone(),
5352 is_folder: track.lock().is_folder,
5353 }))
5354 .await;
5355 }
5356 Action::TrackSetMidiLaneChannel {
5357 ref track_name,
5358 lane,
5359 channel,
5360 } => {
5361 let track = match self.track_handle_or_err(track_name) {
5362 Ok(track) => track,
5363 Err(e) => {
5364 self.notify_clients(Err(e)).await;
5365 return;
5366 }
5367 };
5368 track.lock().set_midi_lane_channel(lane, channel);
5369 }
5370 Action::TrackSetFrozen {
5371 ref track_name,
5372 frozen,
5373 } => {
5374 let track = match self.track_handle_or_err(track_name) {
5375 Ok(track) => track,
5376 Err(e) => {
5377 self.notify_clients(Err(e)).await;
5378 return;
5379 }
5380 };
5381 track.lock().set_frozen(frozen);
5382 }
5383 Action::TrackOfflineBounce {
5384 track_name,
5385 output_path,
5386 start_sample,
5387 length_samples,
5388 automation_lanes,
5389 apply_fader,
5390 } => {
5391 if self.offline_bounce_jobs.contains_key(&track_name) {
5392 self.notify_clients(Err(format!(
5393 "Offline bounce for track '{}' is already in progress",
5394 track_name
5395 )))
5396 .await;
5397 return;
5398 }
5399 if let Err(e) = self.track_handle_or_err(&track_name) {
5400 self.notify_clients(Err(e)).await;
5401 return;
5402 }
5403 if length_samples == 0 {
5404 self.notify_clients(Err(format!(
5405 "Track '{}' has no renderable content for offline bounce",
5406 track_name
5407 )))
5408 .await;
5409 return;
5410 }
5411 let Some(worker_index) = self.take_ready_worker_index(WorkerClass::Refill) else {
5412 self.pending_requests
5413 .push_front(Action::TrackOfflineBounce {
5414 track_name,
5415 output_path,
5416 start_sample,
5417 length_samples,
5418 automation_lanes,
5419 apply_fader,
5420 });
5421 return;
5422 };
5423 let cancel = Arc::new(AtomicBool::new(false));
5424 self.offline_bounce_jobs.insert(
5425 track_name.clone(),
5426 OfflineBounceJob {
5427 cancel: cancel.clone(),
5428 },
5429 );
5430 let track_name_clone = track_name.clone();
5431 let worker = &self.workers[worker_index];
5432 let job = crate::message::OfflineBounceWork {
5433 state: self.state.clone(),
5434 track_name,
5435 output_path,
5436 start_sample,
5437 length_samples,
5438 tempo_bpm: self.tempo_bpm,
5439 tsig_num: self.tsig_num,
5440 tsig_denom: self.tsig_denom,
5441 automation_lanes,
5442 cancel,
5443 apply_fader,
5444 };
5445 if let Err(e) = worker.tx.send(Message::ProcessOfflineBounce(job)).await {
5446 self.offline_bounce_jobs.remove(&track_name_clone);
5447 self.notify_clients(Err(format!("Failed to schedule offline bounce: {e}")))
5448 .await;
5449 }
5450 return;
5451 }
5452 Action::TrackOfflineBounceCancel { .. } => {}
5453 Action::TrackOfflineBounceCancelAll => {}
5454 Action::TrackOfflineBounceCanceled { .. } => {}
5455 Action::TrackOfflineBounceProgress { .. } => {}
5456 Action::PianoKey {
5457 ref track_name,
5458 note,
5459 velocity,
5460 on,
5461 } => {
5462 if let Some(track) = self.state.lock().tracks.get(track_name) {
5463 let status = if on { 0x90 } else { 0x80 };
5464 let event = MidiEvent::new(0, vec![status, note.min(127), velocity.min(127)]);
5465 track.lock().push_hw_midi_events(&[event]);
5466 }
5467 }
5468 Action::ModifyMidiNotes { .. }
5469 | Action::ModifyMidiControllers { .. }
5470 | Action::DeleteMidiControllers { .. }
5471 | Action::InsertMidiControllers { .. }
5472 | Action::DeleteMidiNotes { .. }
5473 | Action::InsertMidiNotes { .. } => {
5474 if let Err(e) = self.apply_midi_edit_action(&action_to_process) {
5475 self.notify_clients(Err(e)).await;
5476 return;
5477 }
5478 }
5479 Action::SetMidiSysExEvents { .. } => {
5480 if let Err(e) = self.apply_midi_edit_action(&action_to_process) {
5481 self.notify_clients(Err(e)).await;
5482 return;
5483 }
5484 }
5485 Action::TrackClearDefaultPassthrough { ref track_name } => {
5486 if self
5487 .reject_if_track_frozen(track_name, "plugin graph editing")
5488 .await
5489 {
5490 return;
5491 }
5492 let track = match self.track_handle_or_err(track_name) {
5493 Ok(track) => track,
5494 Err(e) => {
5495 self.notify_clients(Err(e)).await;
5496 return;
5497 }
5498 };
5499 track.lock().clear_default_passthrough();
5500 }
5501 #[cfg(all(unix, not(target_os = "macos")))]
5502 Action::ClipSetLv2PluginState { ref track_name, .. } => {
5503 self.notify_clients(Err(format!(
5504 "Track '{}': clip LV2 plugin state changes are not supported",
5505 track_name
5506 )))
5507 .await;
5508 }
5509 Action::TrackGetClapNoteNames { ref track_name } => {
5510 let track = match self.track_handle_or_err(track_name) {
5511 Ok(track) => track,
5512 Err(e) => {
5513 self.notify_clients(Err(e)).await;
5514 return;
5515 }
5516 };
5517 let note_names = track.lock().get_clap_note_names();
5518 self.notify_clients(Ok(Action::TrackClapNoteNames {
5519 track_name: track_name.clone(),
5520 note_names,
5521 }))
5522 .await;
5523 }
5524 Action::TrackGetPluginGraph { ref track_name } => {
5525 let track = match self.track_handle_or_err(track_name) {
5526 Ok(track) => track,
5527 Err(e) => {
5528 self.notify_clients(Err(e)).await;
5529 return;
5530 }
5531 };
5532 let (plugins, connections) = {
5533 let track = track.lock();
5534 (
5535 track.plugin_graph_plugins(),
5536 track.plugin_graph_connections(),
5537 )
5538 };
5539 self.notify_clients(Ok(Action::TrackPluginGraph {
5540 track_name: track_name.clone(),
5541 plugins,
5542 connections,
5543 }))
5544 .await;
5545 return;
5546 }
5547 Action::TrackPluginGraph { .. } => {}
5548 Action::TrackConnectPluginAudio {
5549 ref track_name,
5550 ref from_node,
5551 from_port,
5552 ref to_node,
5553 to_port,
5554 } => {
5555 if self
5556 .reject_if_track_frozen(track_name, "plugin routing changes")
5557 .await
5558 {
5559 return;
5560 }
5561 let track = match self.track_handle_or_err(track_name) {
5562 Ok(track) => track,
5563 Err(e) => {
5564 self.notify_clients(Err(e)).await;
5565 return;
5566 }
5567 };
5568 if let Err(e) = track.lock().connect_plugin_audio(
5569 from_node.clone(),
5570 from_port,
5571 to_node.clone(),
5572 to_port,
5573 ) {
5574 self.notify_clients(Err(e)).await;
5575 return;
5576 }
5577 }
5578 Action::TrackConnectPluginMidi {
5579 ref track_name,
5580 ref from_node,
5581 from_port,
5582 ref to_node,
5583 to_port,
5584 } => {
5585 if self
5586 .reject_if_track_frozen(track_name, "plugin routing changes")
5587 .await
5588 {
5589 return;
5590 }
5591 let track = match self.track_handle_or_err(track_name) {
5592 Ok(track) => track,
5593 Err(e) => {
5594 self.notify_clients(Err(e)).await;
5595 return;
5596 }
5597 };
5598 if let Err(e) = track.lock().connect_plugin_midi(
5599 from_node.clone(),
5600 from_port,
5601 to_node.clone(),
5602 to_port,
5603 ) {
5604 self.notify_clients(Err(e)).await;
5605 return;
5606 }
5607 }
5608 Action::TrackDisconnectPluginAudio {
5609 ref track_name,
5610 ref from_node,
5611 from_port,
5612 ref to_node,
5613 to_port,
5614 } => {
5615 if self
5616 .reject_if_track_frozen(track_name, "plugin routing changes")
5617 .await
5618 {
5619 return;
5620 }
5621 let track = match self.track_handle_or_err(track_name) {
5622 Ok(track) => track,
5623 Err(e) => {
5624 self.notify_clients(Err(e)).await;
5625 return;
5626 }
5627 };
5628 if let Err(e) = track.lock().disconnect_plugin_audio(
5629 from_node.clone(),
5630 from_port,
5631 to_node.clone(),
5632 to_port,
5633 ) {
5634 self.notify_clients(Err(e)).await;
5635 return;
5636 }
5637 }
5638 Action::TrackDisconnectPluginMidi {
5639 ref track_name,
5640 ref from_node,
5641 from_port,
5642 ref to_node,
5643 to_port,
5644 } => {
5645 if self
5646 .reject_if_track_frozen(track_name, "plugin routing changes")
5647 .await
5648 {
5649 return;
5650 }
5651 let track = match self.track_handle_or_err(track_name) {
5652 Ok(track) => track,
5653 Err(e) => {
5654 self.notify_clients(Err(e)).await;
5655 return;
5656 }
5657 };
5658 if let Err(e) = track.lock().disconnect_plugin_midi(
5659 from_node.clone(),
5660 from_port,
5661 to_node.clone(),
5662 to_port,
5663 ) {
5664 self.notify_clients(Err(e)).await;
5665 return;
5666 }
5667 }
5668 #[cfg(all(unix, not(target_os = "macos")))]
5669 Action::ListLv2Plugins => {
5670 match crate::plugins::scan_plugins::<crate::plugins::types::Lv2PluginInfo>("lv2") {
5671 Ok(plugins) => {
5672 self.notify_clients(Ok(Action::Lv2Plugins(plugins))).await;
5673 }
5674 Err(e) => {
5675 self.notify_clients(Err(e)).await;
5676 }
5677 }
5678 return;
5679 }
5680 #[cfg(all(unix, not(target_os = "macos")))]
5681 Action::Lv2Plugins(_) => {}
5682 Action::ListVst3Plugins => {
5683 match crate::plugins::scan_plugins::<crate::plugins::types::Vst3PluginInfo>("vst3")
5684 {
5685 Ok(plugins) => {
5686 self.notify_clients(Ok(Action::Vst3Plugins(plugins))).await;
5687 }
5688 Err(e) => {
5689 self.notify_clients(Err(e)).await;
5690 }
5691 }
5692 return;
5693 }
5694 Action::Vst3Plugins(_) => {}
5695 Action::ListClapPlugins => {
5696 match crate::plugins::scan_plugins::<crate::plugins::types::ClapPluginInfo>("clap")
5697 {
5698 Ok(plugins) => {
5699 self.notify_clients(Ok(Action::ClapPlugins(plugins))).await;
5700 }
5701 Err(e) => {
5702 self.notify_clients(Err(e)).await;
5703 }
5704 }
5705 return;
5706 }
5707 Action::ListClapPluginsWithCapabilities => {
5708 match crate::plugins::scan_plugins::<crate::plugins::types::ClapPluginInfo>("clap")
5709 {
5710 Ok(plugins) => {
5711 self.notify_clients(Ok(Action::ClapPlugins(plugins))).await;
5712 }
5713 Err(e) => {
5714 self.notify_clients(Err(e)).await;
5715 }
5716 }
5717 return;
5718 }
5719 Action::ClapPlugins(_) => {}
5720 Action::TrackLoadClapPlugin {
5721 ref track_name,
5722 ref plugin_path,
5723 instance_id,
5724 } => {
5725 if self
5726 .reject_if_track_frozen(track_name, "CLAP plugin loading")
5727 .await
5728 {
5729 return;
5730 }
5731 let track = match self.track_handle_or_err(track_name) {
5732 Ok(track) => track,
5733 Err(e) => {
5734 self.notify_clients(Err(e)).await;
5735 return;
5736 }
5737 };
5738 let track = track.lock();
5739 if track.audio.processing {
5740 self.notify_clients(Err(format!(
5741 "Track '{}' is currently processing audio; stop playback before loading CLAP plugins",
5742 track_name
5743 )))
5744 .await;
5745 return;
5746 }
5747 if let Err(e) = track.load_clap_plugin(plugin_path, instance_id) {
5748 self.notify_clients(Err(e)).await;
5749 return;
5750 }
5751 self.notify_clients(Ok(Action::Log {
5752 source: "engine".to_string(),
5753 message: format!("CLAP plugin loaded on track '{track_name}': {plugin_path}"),
5754 }))
5755 .await;
5756 if let Some(instance) = track.clap_plugins.last()
5757 && let Some(stderr) = instance.processor.lock().take_stderr()
5758 {
5759 let source = format!("clap:{plugin_path}");
5760 self.spawn_plugin_host_stderr_reader(stderr, source);
5761 self.notify_clients(Ok(Action::Log {
5762 source: "engine".to_string(),
5763 message: format!(
5764 "Attached stderr reader for CLAP plugin on track '{track_name}'"
5765 ),
5766 }))
5767 .await;
5768 }
5769 }
5770 Action::TrackUnloadClapPlugin {
5771 ref track_name,
5772 ref plugin_path,
5773 } => {
5774 if self
5775 .reject_if_track_frozen(track_name, "CLAP plugin unloading")
5776 .await
5777 {
5778 return;
5779 }
5780 let track = match self.track_handle_or_err(track_name) {
5781 Ok(track) => track,
5782 Err(e) => {
5783 self.notify_clients(Err(e)).await;
5784 return;
5785 }
5786 };
5787 let track = track.lock();
5788 if track.audio.processing {
5789 self.notify_clients(Err(format!(
5790 "Track '{}' is currently processing audio; stop playback before unloading CLAP plugins",
5791 track_name
5792 )))
5793 .await;
5794 return;
5795 }
5796 if let Err(e) = track.unload_clap_plugin(plugin_path) {
5797 self.notify_clients(Err(e)).await;
5798 return;
5799 }
5800 }
5801 Action::TrackUnloadClapPluginInstance {
5802 ref track_name,
5803 instance_id,
5804 } => {
5805 if self
5806 .reject_if_track_frozen(track_name, "CLAP plugin unloading")
5807 .await
5808 {
5809 return;
5810 }
5811 let track = match self.track_handle_or_err(track_name) {
5812 Ok(track) => track,
5813 Err(e) => {
5814 self.notify_clients(Err(e)).await;
5815 return;
5816 }
5817 };
5818 let track = track.lock();
5819 if track.audio.processing {
5820 self.notify_clients(Err(format!(
5821 "Track '{}' is currently processing audio; stop playback before unloading CLAP plugins",
5822 track_name
5823 )))
5824 .await;
5825 return;
5826 }
5827 if let Err(e) = track.unload_clap_plugin_instance(instance_id) {
5828 self.notify_clients(Err(e)).await;
5829 return;
5830 }
5831 }
5832 Action::TrackShowClapGui {
5833 ref track_name,
5834 instance_id,
5835 } => {
5836 let track = match self.track_handle_or_err(track_name) {
5837 Ok(track) => track,
5838 Err(e) => {
5839 self.notify_clients(Err(e)).await;
5840 return;
5841 }
5842 };
5843 if let Err(e) = track.lock().show_clap_gui(instance_id) {
5844 self.notify_clients(Err(e)).await;
5845 return;
5846 }
5847 }
5848 Action::TrackLoadVst3Plugin {
5849 ref track_name,
5850 ref plugin_path,
5851 instance_id,
5852 } => {
5853 if self
5854 .reject_if_track_frozen(track_name, "VST3 plugin loading")
5855 .await
5856 {
5857 return;
5858 }
5859 let track = match self.track_handle_or_err(track_name) {
5860 Ok(track) => track,
5861 Err(e) => {
5862 self.notify_clients(Err(e)).await;
5863 return;
5864 }
5865 };
5866 let track = track.lock();
5867 if track.audio.processing {
5868 self.notify_clients(Err(format!(
5869 "Track '{}' is currently processing audio; stop playback before loading VST3 plugins",
5870 track_name
5871 )))
5872 .await;
5873 return;
5874 }
5875 if let Err(e) = track.load_vst3_plugin(plugin_path, instance_id) {
5876 self.notify_clients(Err(e)).await;
5877 return;
5878 }
5879 if let Some(instance) = track.vst3_plugins.last()
5880 && let Some(stderr) = instance.processor.lock().take_stderr()
5881 {
5882 let source = format!("vst3:{plugin_path}");
5883 self.spawn_plugin_host_stderr_reader(stderr, source);
5884 }
5885 }
5886 Action::TrackUnloadVst3Plugin {
5887 ref track_name,
5888 ref plugin_path,
5889 } => {
5890 if self
5891 .reject_if_track_frozen(track_name, "VST3 plugin unloading")
5892 .await
5893 {
5894 return;
5895 }
5896 let track = match self.track_handle_or_err(track_name) {
5897 Ok(track) => track,
5898 Err(e) => {
5899 self.notify_clients(Err(e)).await;
5900 return;
5901 }
5902 };
5903 let track = track.lock();
5904 if track.audio.processing {
5905 self.notify_clients(Err(format!(
5906 "Track '{}' is currently processing audio; stop playback before unloading VST3 plugins",
5907 track_name
5908 )))
5909 .await;
5910 return;
5911 }
5912 if let Err(e) = track.unload_vst3_plugin(plugin_path) {
5913 self.notify_clients(Err(e)).await;
5914 return;
5915 }
5916 }
5917 Action::TrackUnloadVst3PluginInstance {
5918 ref track_name,
5919 instance_id,
5920 } => {
5921 if self
5922 .reject_if_track_frozen(track_name, "VST3 plugin unloading")
5923 .await
5924 {
5925 return;
5926 }
5927 let track = match self.track_handle_or_err(track_name) {
5928 Ok(track) => track,
5929 Err(e) => {
5930 self.notify_clients(Err(e)).await;
5931 return;
5932 }
5933 };
5934 let track = track.lock();
5935 if track.audio.processing {
5936 self.notify_clients(Err(format!(
5937 "Track '{}' is currently processing audio; stop playback before unloading VST3 plugins",
5938 track_name
5939 )))
5940 .await;
5941 return;
5942 }
5943 if let Err(e) = track.unload_vst3_plugin_instance(instance_id) {
5944 self.notify_clients(Err(e)).await;
5945 return;
5946 }
5947 }
5948 Action::TrackShowVst3Gui {
5949 ref track_name,
5950 instance_id,
5951 } => {
5952 let track = match self.track_handle_or_err(track_name) {
5953 Ok(track) => track,
5954 Err(e) => {
5955 self.notify_clients(Err(e)).await;
5956 return;
5957 }
5958 };
5959 if let Err(e) = track.lock().show_vst3_gui(instance_id) {
5960 self.notify_clients(Err(e)).await;
5961 return;
5962 }
5963 }
5964 #[cfg(all(unix, not(target_os = "macos")))]
5965 Action::TrackLoadLv2Plugin {
5966 ref track_name,
5967 ref plugin_uri,
5968 instance_id,
5969 } => {
5970 if self
5971 .reject_if_track_frozen(track_name, "LV2 plugin loading")
5972 .await
5973 {
5974 return;
5975 }
5976 let track = match self.track_handle_or_err(track_name) {
5977 Ok(track) => track,
5978 Err(e) => {
5979 self.notify_clients(Err(e)).await;
5980 return;
5981 }
5982 };
5983 let track = track.lock();
5984 if track.audio.processing {
5985 self.notify_clients(Err(format!(
5986 "Track '{}' is currently processing audio; stop playback before loading LV2 plugins",
5987 track_name
5988 )))
5989 .await;
5990 return;
5991 }
5992 if let Err(e) = track.load_lv2_plugin(plugin_uri, instance_id) {
5993 self.notify_clients(Err(e)).await;
5994 return;
5995 }
5996 if let Some(instance) = track.lv2_plugins.last()
5997 && let Some(stderr) = instance.processor.lock().take_stderr()
5998 {
5999 let source = format!("lv2:{plugin_uri}");
6000 self.spawn_plugin_host_stderr_reader(stderr, source);
6001 }
6002 }
6003 #[cfg(all(unix, not(target_os = "macos")))]
6004 Action::TrackUnloadLv2Plugin {
6005 ref track_name,
6006 ref plugin_uri,
6007 } => {
6008 if self
6009 .reject_if_track_frozen(track_name, "LV2 plugin unloading")
6010 .await
6011 {
6012 return;
6013 }
6014 let track = match self.track_handle_or_err(track_name) {
6015 Ok(track) => track,
6016 Err(e) => {
6017 self.notify_clients(Err(e)).await;
6018 return;
6019 }
6020 };
6021 let track = track.lock();
6022 if track.audio.processing {
6023 self.notify_clients(Err(format!(
6024 "Track '{}' is currently processing audio; stop playback before unloading LV2 plugins",
6025 track_name
6026 )))
6027 .await;
6028 return;
6029 }
6030 if let Err(e) = track.unload_lv2_plugin(plugin_uri) {
6031 self.notify_clients(Err(e)).await;
6032 return;
6033 }
6034 }
6035 #[cfg(all(unix, not(target_os = "macos")))]
6036 Action::TrackUnloadLv2PluginInstance {
6037 ref track_name,
6038 instance_id,
6039 } => {
6040 if self
6041 .reject_if_track_frozen(track_name, "LV2 plugin unloading")
6042 .await
6043 {
6044 return;
6045 }
6046 let track = match self.track_handle_or_err(track_name) {
6047 Ok(track) => track,
6048 Err(e) => {
6049 self.notify_clients(Err(e)).await;
6050 return;
6051 }
6052 };
6053 let track = track.lock();
6054 if track.audio.processing {
6055 self.notify_clients(Err(format!(
6056 "Track '{}' is currently processing audio; stop playback before unloading LV2 plugins",
6057 track_name
6058 )))
6059 .await;
6060 return;
6061 }
6062 if let Err(e) = track.unload_lv2_plugin_instance(instance_id) {
6063 self.notify_clients(Err(e)).await;
6064 return;
6065 }
6066 }
6067 #[cfg(all(unix, not(target_os = "macos")))]
6068 Action::TrackShowLv2Gui {
6069 ref track_name,
6070 instance_id,
6071 } => {
6072 let track = match self.track_handle_or_err(track_name) {
6073 Ok(track) => track,
6074 Err(e) => {
6075 self.notify_clients(Err(e)).await;
6076 return;
6077 }
6078 };
6079 if let Err(e) = track.lock().show_lv2_gui(instance_id) {
6080 self.notify_clients(Err(e)).await;
6081 return;
6082 }
6083 }
6084 Action::TrackSetClapParameter {
6085 ref track_name,
6086 instance_id,
6087 param_id,
6088 value,
6089 } => {
6090 if self
6091 .reject_if_track_frozen(track_name, "CLAP parameter changes")
6092 .await
6093 {
6094 return;
6095 }
6096 match self.track_handle_or_err(track_name) {
6097 Ok(track) => {
6098 if let Err(e) =
6099 track
6100 .lock()
6101 .set_clap_parameter(instance_id, param_id, value)
6102 {
6103 self.notify_clients(Err(e)).await;
6104 return;
6105 }
6106 self.notify_clients(Ok(a.clone())).await;
6107 }
6108 Err(e) => {
6109 self.notify_clients(Err(e)).await;
6110 }
6111 }
6112 }
6113 Action::ClipSetClapParameter {
6114 ref track_name,
6115 clip_idx,
6116 instance_id,
6117 param_id,
6118 value,
6119 } => {
6120 if self
6121 .reject_if_track_frozen(track_name, "CLAP parameter changes")
6122 .await
6123 {
6124 return;
6125 }
6126 match self.track_handle_or_err(track_name) {
6127 Ok(track) => {
6128 if let Err(e) = track.lock().clip_set_clap_parameter(
6129 clip_idx,
6130 instance_id,
6131 param_id,
6132 value,
6133 ) {
6134 self.notify_clients(Err(e)).await;
6135 return;
6136 }
6137 self.notify_clients(Ok(a.clone())).await;
6138 }
6139 Err(e) => {
6140 self.notify_clients(Err(e)).await;
6141 }
6142 }
6143 }
6144 Action::TrackSetClapParameterAt {
6145 ref track_name,
6146 instance_id,
6147 param_id,
6148 value,
6149 frame,
6150 } => {
6151 if self
6152 .reject_if_track_frozen(track_name, "CLAP parameter changes")
6153 .await
6154 {
6155 return;
6156 }
6157 match self.track_handle_or_err(track_name) {
6158 Ok(track) => {
6159 if let Err(e) =
6160 track
6161 .lock()
6162 .set_clap_parameter_at(instance_id, param_id, value, frame)
6163 {
6164 self.notify_clients(Err(e)).await;
6165 return;
6166 }
6167 self.notify_clients(Ok(a.clone())).await;
6168 }
6169 Err(e) => {
6170 self.notify_clients(Err(e)).await;
6171 }
6172 }
6173 }
6174 Action::TrackBeginClapParameterEdit {
6175 ref track_name,
6176 instance_id,
6177 param_id,
6178 frame,
6179 } => {
6180 if self
6181 .reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
6182 .await
6183 {
6184 return;
6185 }
6186 match self.track_handle_or_err(track_name) {
6187 Ok(track) => {
6188 if let Err(e) =
6189 track
6190 .lock()
6191 .begin_clap_parameter_edit(instance_id, param_id, frame)
6192 {
6193 self.notify_clients(Err(e)).await;
6194 return;
6195 }
6196 self.notify_clients(Ok(a.clone())).await;
6197 }
6198 Err(e) => {
6199 self.notify_clients(Err(e)).await;
6200 }
6201 }
6202 }
6203 Action::TrackEndClapParameterEdit {
6204 ref track_name,
6205 instance_id,
6206 param_id,
6207 frame,
6208 } => {
6209 if self
6210 .reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
6211 .await
6212 {
6213 return;
6214 }
6215 match self.track_handle_or_err(track_name) {
6216 Ok(track) => {
6217 if let Err(e) =
6218 track
6219 .lock()
6220 .end_clap_parameter_edit(instance_id, param_id, frame)
6221 {
6222 self.notify_clients(Err(e)).await;
6223 return;
6224 }
6225 self.notify_clients(Ok(a.clone())).await;
6226 }
6227 Err(e) => {
6228 self.notify_clients(Err(e)).await;
6229 }
6230 }
6231 }
6232 Action::TrackGetClapParameters {
6233 ref track_name,
6234 instance_id,
6235 } => match self.track_handle_or_err(track_name) {
6236 Ok(track) => match track.lock().get_clap_parameters(instance_id) {
6237 Ok(parameters) => {
6238 self.notify_clients(Ok(Action::TrackClapParameters {
6239 track_name: track_name.clone(),
6240 instance_id,
6241 parameters,
6242 }))
6243 .await;
6244 }
6245 Err(e) => {
6246 self.notify_clients(Err(e)).await;
6247 }
6248 },
6249 Err(e) => {
6250 self.notify_clients(Err(e)).await;
6251 }
6252 },
6253 Action::TrackClapParameters { .. } => {}
6254 Action::TrackClapSnapshotState {
6255 ref track_name,
6256 instance_id,
6257 } => match self.track_handle_or_err(track_name) {
6258 Ok(track) => {
6259 let plugin_path = track
6260 .lock()
6261 .clap_plugins
6262 .iter()
6263 .find(|instance| instance.id == instance_id)
6264 .map(|instance| instance.processor.lock().path().to_string())
6265 .unwrap_or_default();
6266 match track.lock().clap_snapshot_state(instance_id) {
6267 Ok(state) => {
6268 self.notify_clients(Ok(Action::TrackClapStateSnapshot {
6269 track_name: track_name.clone(),
6270 instance_id,
6271 plugin_path,
6272 state,
6273 }))
6274 .await;
6275 }
6276 Err(e) => {
6277 self.notify_clients(Err(e)).await;
6278 }
6279 }
6280 }
6281 Err(e) => {
6282 self.notify_clients(Err(e)).await;
6283 }
6284 },
6285 Action::ClipClapSnapshotState {
6286 ref track_name,
6287 clip_idx,
6288 instance_id,
6289 } => match self.track_handle_or_err(track_name) {
6290 Ok(track) => match track.lock().clip_clap_snapshot_state(clip_idx, instance_id) {
6291 Ok((plugin_path, state)) => {
6292 self.notify_clients(Ok(Action::ClipClapStateSnapshot {
6293 track_name: track_name.clone(),
6294 clip_idx,
6295 instance_id,
6296 plugin_path,
6297 state,
6298 }))
6299 .await;
6300 }
6301 Err(e) => {
6302 self.notify_clients(Err(e)).await;
6303 }
6304 },
6305 Err(e) => {
6306 self.notify_clients(Err(e)).await;
6307 }
6308 },
6309 Action::TrackClapStateSnapshot { .. } => {}
6310 Action::ClipClapStateSnapshot { .. } => {}
6311 Action::TrackClapRestoreState {
6312 ref track_name,
6313 instance_id,
6314 ref state,
6315 } => {
6316 if self
6317 .reject_if_track_frozen(track_name, "CLAP state restore")
6318 .await
6319 {
6320 return;
6321 }
6322 let track = match self.track_handle_or_err(track_name) {
6323 Ok(track) => track,
6324 Err(e) => {
6325 self.notify_clients(Err(e)).await;
6326 return;
6327 }
6328 };
6329 let track = track.lock();
6330 if track.audio.processing {
6331 self.notify_clients(Err(format!(
6332 "Track '{}' is currently processing audio; stop playback before restoring CLAP state",
6333 track_name
6334 )))
6335 .await;
6336 return;
6337 }
6338 if let Err(e) = track.clap_restore_state(instance_id, state) {
6339 self.notify_clients(Err(e)).await;
6340 return;
6341 }
6342 }
6343 Action::ClipClapRestoreState {
6344 ref track_name,
6345 clip_idx,
6346 instance_id,
6347 ref state,
6348 } => {
6349 if self
6350 .reject_if_track_frozen(track_name, "CLAP state restore")
6351 .await
6352 {
6353 return;
6354 }
6355 let track = match self.track_handle_or_err(track_name) {
6356 Ok(track) => track,
6357 Err(e) => {
6358 self.notify_clients(Err(e)).await;
6359 return;
6360 }
6361 };
6362 let track = track.lock();
6363 if track.audio.processing {
6364 self.notify_clients(Err(format!(
6365 "Track '{}' is currently processing audio; stop playback before restoring CLAP state",
6366 track_name
6367 )))
6368 .await;
6369 return;
6370 }
6371 if let Err(e) = track.clip_clap_restore_state(clip_idx, instance_id, state) {
6372 self.notify_clients(Err(e)).await;
6373 return;
6374 }
6375 }
6376 Action::TrackSnapshotAllClapStates { ref track_name } => {
6377 let track = match self.track_handle_or_err(track_name) {
6378 Ok(track) => track,
6379 Err(e) => {
6380 self.notify_clients(Err(e)).await;
6381 return;
6382 }
6383 };
6384 for (instance_id, plugin_path, state) in track.lock().clap_snapshot_all_states() {
6385 self.notify_clients(Ok(Action::TrackClapStateSnapshot {
6386 track_name: track_name.clone(),
6387 instance_id,
6388 plugin_path,
6389 state,
6390 }))
6391 .await;
6392 }
6393 self.notify_clients(Ok(Action::TrackSnapshotAllClapStatesDone {
6394 track_name: track_name.clone(),
6395 }))
6396 .await;
6397 }
6398 Action::TrackSnapshotAllClapStatesDone { .. } => {}
6399 Action::TrackGetVst3Graph { ref track_name } => {
6400 match self.track_handle_or_err(track_name) {
6401 Ok(track) => {
6402 let t = track.lock();
6403 let plugins = t.vst3_graph_plugins();
6404 let connections = t.vst3_graph_connections();
6405 self.notify_clients(Ok(Action::TrackVst3Graph {
6406 track_name: track_name.clone(),
6407 plugins,
6408 connections,
6409 }))
6410 .await;
6411 }
6412 Err(e) => {
6413 self.notify_clients(Err(e)).await;
6414 }
6415 }
6416 }
6417 Action::TrackVst3Graph { .. } => {}
6418 Action::TrackSetVst3Parameter {
6419 ref track_name,
6420 instance_id,
6421 param_id,
6422 value,
6423 } => {
6424 if self
6425 .reject_if_track_frozen(track_name, "VST3 parameter changes")
6426 .await
6427 {
6428 return;
6429 }
6430 match self.track_handle_or_err(track_name) {
6431 Ok(track) => {
6432 if let Err(e) =
6433 track
6434 .lock()
6435 .set_vst3_parameter(instance_id, param_id, value)
6436 {
6437 self.notify_clients(Err(e)).await;
6438 return;
6439 }
6440 self.notify_clients(Ok(a.clone())).await;
6441 }
6442 Err(e) => {
6443 self.notify_clients(Err(e)).await;
6444 }
6445 }
6446 }
6447 Action::TrackSetPluginBypassed {
6448 ref track_name,
6449 instance_id,
6450 ref format,
6451 bypassed,
6452 } => match self.track_handle_or_err(track_name) {
6453 Ok(track) => {
6454 let result = match format.as_str() {
6455 "CLAP" => track.lock().set_clap_plugin_bypassed(instance_id, bypassed),
6456 "VST3" => track.lock().set_vst3_plugin_bypassed(instance_id, bypassed),
6457 #[cfg(all(unix, not(target_os = "macos")))]
6458 "LV2" => track.lock().set_lv2_plugin_bypassed(instance_id, bypassed),
6459 _ => Err(format!("Unknown plugin format for bypass: {format}")),
6460 };
6461 if let Err(e) = result {
6462 self.notify_clients(Err(e)).await;
6463 return;
6464 }
6465 self.notify_clients(Ok(a.clone())).await;
6466 }
6467 Err(e) => {
6468 self.notify_clients(Err(e)).await;
6469 }
6470 },
6471 Action::TrackGetVst3Parameters {
6472 ref track_name,
6473 instance_id,
6474 } => match self.track_handle_or_err(track_name) {
6475 Ok(track) => match track.lock().get_vst3_parameters(instance_id) {
6476 Ok(parameters) => {
6477 self.notify_clients(Ok(Action::TrackVst3Parameters {
6478 track_name: track_name.clone(),
6479 instance_id,
6480 parameters,
6481 }))
6482 .await;
6483 }
6484 Err(e) => {
6485 self.notify_clients(Err(e)).await;
6486 }
6487 },
6488 Err(e) => {
6489 self.notify_clients(Err(e)).await;
6490 }
6491 },
6492 Action::TrackVst3Parameters { .. } => {}
6493 Action::TrackVst3SnapshotState {
6494 ref track_name,
6495 instance_id,
6496 } => match self.track_handle_or_err(track_name) {
6497 Ok(track) => match track.lock().vst3_snapshot_state(instance_id) {
6498 Ok(state) => {
6499 self.notify_clients(Ok(Action::TrackVst3StateSnapshot {
6500 track_name: track_name.clone(),
6501 instance_id,
6502 state,
6503 }))
6504 .await;
6505 }
6506 Err(e) => {
6507 self.notify_clients(Err(e)).await;
6508 }
6509 },
6510 Err(e) => {
6511 self.notify_clients(Err(e)).await;
6512 }
6513 },
6514 Action::ClipVst3SnapshotState {
6515 ref track_name,
6516 clip_idx,
6517 instance_id,
6518 } => match self.track_handle_or_err(track_name) {
6519 Ok(track) => match track.lock().clip_vst3_snapshot_state(clip_idx, instance_id) {
6520 Ok(state) => {
6521 self.notify_clients(Ok(Action::ClipVst3StateSnapshot {
6522 track_name: track_name.clone(),
6523 clip_idx,
6524 instance_id,
6525 state,
6526 }))
6527 .await;
6528 }
6529 Err(e) => {
6530 self.notify_clients(Err(e)).await;
6531 }
6532 },
6533 Err(e) => {
6534 self.notify_clients(Err(e)).await;
6535 }
6536 },
6537 Action::TrackVst3StateSnapshot { .. } => {}
6538 Action::ClipVst3StateSnapshot { .. } => {}
6539 Action::TrackVst3RestoreState {
6540 ref track_name,
6541 instance_id,
6542 ref state,
6543 } => match self.track_handle_or_err(track_name) {
6544 Ok(track) => {
6545 if let Err(e) = track.lock().vst3_restore_state(instance_id, state) {
6546 self.notify_clients(Err(e)).await;
6547 return;
6548 }
6549 self.notify_clients(Ok(a.clone())).await;
6550 }
6551 Err(e) => {
6552 self.notify_clients(Err(e)).await;
6553 }
6554 },
6555 Action::TrackConnectVst3Audio {
6556 ref track_name,
6557 ref from_node,
6558 from_port,
6559 ref to_node,
6560 to_port,
6561 } => {
6562 if self
6563 .reject_if_track_frozen(track_name, "VST3 routing changes")
6564 .await
6565 {
6566 return;
6567 }
6568 match self.track_handle_or_err(track_name) {
6569 Ok(track) => {
6570 if let Err(e) = track
6571 .lock()
6572 .connect_vst3_audio(from_node, from_port, to_node, to_port)
6573 {
6574 self.notify_clients(Err(e)).await;
6575 return;
6576 }
6577 self.notify_clients(Ok(a.clone())).await;
6578 }
6579 Err(e) => {
6580 self.notify_clients(Err(e)).await;
6581 }
6582 }
6583 }
6584 Action::TrackDisconnectVst3Audio {
6585 ref track_name,
6586 ref from_node,
6587 from_port,
6588 ref to_node,
6589 to_port,
6590 } => {
6591 if self
6592 .reject_if_track_frozen(track_name, "VST3 routing changes")
6593 .await
6594 {
6595 return;
6596 }
6597 match self.track_handle_or_err(track_name) {
6598 Ok(track) => {
6599 if let Err(e) = track
6600 .lock()
6601 .disconnect_vst3_audio(from_node, from_port, to_node, to_port)
6602 {
6603 self.notify_clients(Err(e)).await;
6604 return;
6605 }
6606 self.notify_clients(Ok(a.clone())).await;
6607 }
6608 Err(e) => {
6609 self.notify_clients(Err(e)).await;
6610 }
6611 }
6612 }
6613 Action::ClipMove {
6614 ref kind,
6615 ref from,
6616 ref to,
6617 copy,
6618 } => {
6619 if let Some(from_track_handle) = self.state.lock().tracks.get(&from.track_name)
6620 && let Some(to_track_handle) = self.state.lock().tracks.get(&to.track_name)
6621 {
6622 let from_track = from_track_handle.lock();
6623 let to_track = to_track_handle.lock();
6624 match kind {
6625 Kind::Audio => {
6626 if from.clip_index >= from_track.audio.clips.len() {
6627 self.notify_clients(Err(format!(
6628 "Clip index {} is too high, as track {} has only {} clips!",
6629 from.clip_index,
6630 from_track.name.clone(),
6631 from_track.audio.clips.len(),
6632 )))
6633 .await;
6634 return;
6635 }
6636 if from_track.audio.ins.len() != to_track.audio.ins.len() {
6637 self.notify_clients(Err(format!(
6638 "Cannot move/copy audio clip from '{}' ({} inputs) to '{}' ({} inputs)",
6639 from_track.name,
6640 from_track.audio.ins.len(),
6641 to_track.name,
6642 to_track.audio.ins.len()
6643 )))
6644 .await;
6645 return;
6646 }
6647 let clip_copy = from_track.audio.clips[from.clip_index].clone();
6648 if !copy {
6649 from_track.audio.clips.remove(from.clip_index);
6650 }
6651 let mut clip_copy = clip_copy;
6652 clip_copy.start = to.sample_offset;
6653 let max_lane = to_track.audio.ins.len().saturating_sub(1);
6654 clip_copy.input_channel = to.input_channel.min(max_lane);
6655 to_track.audio.clips.push(clip_copy);
6656 }
6657 Kind::MIDI => {
6658 if from.clip_index >= from_track.midi.clips.len() {
6659 self.notify_clients(Err(format!(
6660 "Clip index {} is too high, as track {} has only {} clips!",
6661 from.clip_index,
6662 from_track.name.clone(),
6663 from_track.midi.clips.len(),
6664 )))
6665 .await;
6666 return;
6667 }
6668 let clip_copy = from_track.midi.clips[from.clip_index].clone();
6669 if !copy {
6670 from_track.midi.clips.remove(from.clip_index);
6671 }
6672 let mut clip_copy = clip_copy;
6673 clip_copy.start = to.sample_offset;
6674 let max_lane = to_track.midi.ins.len().saturating_sub(1);
6675 clip_copy.input_channel = to.input_channel.min(max_lane);
6676 to_track.midi.clips.push(clip_copy);
6677 }
6678 }
6679 }
6680 }
6681 Action::AddClip {
6682 ref name,
6683 ref track_name,
6684 start,
6685 length,
6686 offset,
6687 input_channel,
6688 muted,
6689 ref peaks_file,
6690 kind,
6691 fade_enabled,
6692 fade_in_samples,
6693 fade_out_samples,
6694 ref source_name,
6695 source_offset,
6696 source_length,
6697 ref preview_name,
6698 ref pitch_correction_points,
6699 pitch_correction_frame_likeness,
6700 pitch_correction_inertia_ms,
6701 pitch_correction_formant_compensation,
6702 ref plugin_graph_json,
6703 } => {
6704 self.add_clip_to_track(ClipAddRequest {
6705 name,
6706 track_name,
6707 start,
6708 length,
6709 offset,
6710 input_channel,
6711 muted,
6712 peaks_file: peaks_file.clone(),
6713 kind,
6714 fade_enabled,
6715 fade_in_samples,
6716 fade_out_samples,
6717 source_name: source_name.clone(),
6718 source_offset,
6719 source_length,
6720 preview_name: preview_name.clone(),
6721 pitch_correction_points: pitch_correction_points.clone(),
6722 pitch_correction_frame_likeness,
6723 pitch_correction_inertia_ms,
6724 pitch_correction_formant_compensation,
6725 plugin_graph_json: plugin_graph_json.clone(),
6726 });
6727 if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
6728 let track_name = track_name.clone();
6729 tokio::task::spawn_blocking(move || {
6730 track.lock().preload_clips();
6731 tracing::debug!("Preloaded clips for track '{}' after AddClip", track_name);
6732 });
6733 }
6734 }
6735 Action::AddGroupedClip {
6736 ref track_name,
6737 kind,
6738 ref audio_clip,
6739 ref midi_clip,
6740 } => {
6741 self.add_grouped_clip_to_track(
6742 track_name,
6743 kind,
6744 audio_clip.clone(),
6745 midi_clip.clone(),
6746 );
6747 if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
6748 let track_name = track_name.clone();
6749 tokio::task::spawn_blocking(move || {
6750 track.lock().preload_clips();
6751 tracing::debug!(
6752 "Preloaded clips for track '{}' after AddGroupedClip",
6753 track_name
6754 );
6755 });
6756 }
6757 }
6758 Action::RemoveClip {
6759 ref track_name,
6760 kind,
6761 ref clip_indices,
6762 } => {
6763 self.remove_clips_from_track(track_name, kind, clip_indices);
6764 }
6765 Action::RenameClip {
6766 ref track_name,
6767 kind,
6768 clip_index,
6769 ref new_name,
6770 } => {
6771 self.rename_clip_references(track_name, kind, clip_index, new_name);
6772 }
6773 Action::SetClipSourceName {
6774 ref track_name,
6775 kind,
6776 clip_index,
6777 ref name,
6778 } => {
6779 self.set_clip_source_name(track_name, clip_index, kind, name.clone());
6780 }
6781 Action::SetClipFade {
6782 ref track_name,
6783 clip_index,
6784 kind,
6785 fade_enabled,
6786 fade_in_samples,
6787 fade_out_samples,
6788 } => {
6789 self.set_clip_fade(
6790 track_name,
6791 clip_index,
6792 kind,
6793 fade_enabled,
6794 fade_in_samples,
6795 fade_out_samples,
6796 );
6797 }
6798 Action::SetClipBounds {
6799 ref track_name,
6800 clip_index,
6801 kind,
6802 start,
6803 length,
6804 offset,
6805 } => {
6806 self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
6807 }
6808 Action::SyncClipBounds {
6809 ref track_name,
6810 clip_index,
6811 kind,
6812 start,
6813 length,
6814 offset,
6815 } => {
6816 self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
6817 }
6818 Action::SetClipMuted {
6819 ref track_name,
6820 clip_index,
6821 kind,
6822 muted,
6823 } => {
6824 self.set_clip_muted(track_name, clip_index, kind, muted);
6825 }
6826 Action::SetClipPluginGraphJson {
6827 ref track_name,
6828 clip_index,
6829 ref plugin_graph_json,
6830 } => {
6831 self.set_clip_plugin_graph_json(track_name, clip_index, plugin_graph_json.clone());
6832 }
6833 Action::SetClipPitchCorrection {
6834 ref track_name,
6835 clip_index,
6836 ref preview_name,
6837 ref source_name,
6838 source_offset,
6839 source_length,
6840 ref pitch_correction_points,
6841 pitch_correction_frame_likeness,
6842 pitch_correction_inertia_ms,
6843 pitch_correction_formant_compensation,
6844 } => {
6845 self.set_clip_pitch_correction(
6846 track_name,
6847 clip_index,
6848 preview_name.clone(),
6849 source_name.clone(),
6850 source_offset,
6851 source_length,
6852 pitch_correction_points.clone(),
6853 pitch_correction_frame_likeness,
6854 pitch_correction_inertia_ms,
6855 pitch_correction_formant_compensation,
6856 );
6857 }
6858 Action::Connect {
6859 ref from_track,
6860 from_port,
6861 ref to_track,
6862 to_port,
6863 kind,
6864 } => {
6865 match kind {
6866 Kind::Audio => {
6867 let from_audio_io = if from_track == "hw:in" {
6868 self.hw_input_audio_port(from_port)
6869 } else {
6870 self.state
6871 .lock()
6872 .tracks
6873 .get(from_track)
6874 .and_then(|t| t.lock().audio.outs.get(from_port).cloned())
6875 };
6876 let to_audio_io = if to_track == "hw:out" {
6877 self.hw_output_audio_port(to_port)
6878 } else {
6879 self.state
6880 .lock()
6881 .tracks
6882 .get(to_track)
6883 .and_then(|t| t.lock().audio.ins.get(to_port).cloned())
6884 };
6885 match (from_audio_io, to_audio_io) {
6886 (Some(source), Some(target)) => {
6887 if from_track != "hw:in"
6888 && to_track != "hw:out"
6889 && self.check_if_leads_to_kind(
6890 Kind::Audio,
6891 to_track,
6892 from_track,
6893 )
6894 {
6895 self.notify_clients(Err(
6896 "Circular routing is not allowed!".into()
6897 ))
6898 .await;
6899 return;
6900 }
6901 crate::audio::io::AudioIO::connect(&source, &target);
6902 }
6903 (None, _) => {
6904 self.notify_clients(Err(format!(
6905 "Source track '{}' not found",
6906 from_track
6907 )))
6908 .await;
6909 return;
6910 }
6911 (_, None) => {
6912 self.notify_clients(Err(format!(
6913 "Destination track '{}' not found",
6914 to_track
6915 )))
6916 .await;
6917 return;
6918 }
6919 }
6920 }
6921 Kind::MIDI => {
6922 let from_hw_in_device = Self::midi_hw_in_device(from_track);
6923 let to_hw_out_device = Self::midi_hw_out_device(to_track);
6924 let from_is_invalid_hw = Self::midi_hw_out_device(from_track).is_some();
6925 let to_is_invalid_hw = Self::midi_hw_in_device(to_track).is_some();
6926
6927 if from_is_invalid_hw || to_is_invalid_hw {
6928 self.notify_clients(Err(
6929 "Invalid MIDI hardware connection direction".to_string()
6930 ))
6931 .await;
6932 return;
6933 }
6934
6935 if from_hw_in_device.is_none()
6936 && to_hw_out_device.is_none()
6937 && self.check_if_leads_to_kind(Kind::MIDI, to_track, from_track)
6938 {
6939 self.notify_clients(Err("Circular routing is not allowed!".into()))
6940 .await;
6941 return;
6942 }
6943
6944 let state = self.state.lock();
6945 let from_track_handle = state.tracks.get(from_track);
6946 let to_track_handle = state.tracks.get(to_track);
6947
6948 if let (Some(from_device), Some(to_device)) =
6949 (from_hw_in_device, to_hw_out_device)
6950 {
6951 let route = MidiHwThruRoute {
6952 from_device: from_device.to_string(),
6953 to_device: to_device.to_string(),
6954 };
6955 if !self.midi_hw_thru_routes.iter().any(|r| r == &route) {
6956 self.midi_hw_thru_routes.push(route);
6957 }
6958 } else if let Some(device) = from_hw_in_device {
6959 if let Some(t_t) = to_track_handle {
6960 if t_t.lock().midi.ins.get(to_port).is_none() {
6961 self.notify_clients(Err(format!(
6962 "MIDI input port {} not found on track '{}'",
6963 to_port, to_track
6964 )))
6965 .await;
6966 return;
6967 }
6968 let route = MidiHwInRoute {
6969 device: device.to_string(),
6970 to_track: to_track.to_string(),
6971 to_port,
6972 };
6973 if !self.midi_hw_in_routes.iter().any(|r| r == &route) {
6974 self.midi_hw_in_routes.push(route);
6975 }
6976 } else {
6977 self.notify_clients(Err(format!(
6978 "MIDI destination track not found: {}",
6979 to_track
6980 )))
6981 .await;
6982 return;
6983 }
6984 } else if let Some(device) = to_hw_out_device {
6985 if let Some(f_t) = from_track_handle {
6986 if f_t.lock().midi.outs.get(from_port).is_none() {
6987 self.notify_clients(Err(format!(
6988 "MIDI output port {} not found on track '{}'",
6989 from_port, from_track
6990 )))
6991 .await;
6992 return;
6993 }
6994 let route = MidiHwOutRoute {
6995 from_track: from_track.to_string(),
6996 from_port,
6997 device: device.to_string(),
6998 };
6999 if !self.midi_hw_out_routes.iter().any(|r| r == &route) {
7000 self.midi_hw_out_routes.push(route);
7001 }
7002 } else {
7003 self.notify_clients(Err(format!(
7004 "MIDI source track not found: {}",
7005 from_track
7006 )))
7007 .await;
7008 return;
7009 }
7010 } else {
7011 match (from_track_handle, to_track_handle) {
7012 (Some(f_t), Some(t_t)) => {
7013 let to_in_res = t_t.lock().midi.ins.get(to_port).cloned();
7014 if let Some(to_in) = to_in_res {
7015 let from_track = f_t.lock();
7016 if let Err(e) =
7017 from_track.midi.connect_out(from_port, to_in)
7018 {
7019 self.notify_clients(Err(e)).await;
7020 return;
7021 }
7022 from_track.invalidate_midi_route_cache();
7023 } else {
7024 self.notify_clients(Err(format!(
7025 "MIDI input port {} not found on track '{}'",
7026 to_port, to_track
7027 )))
7028 .await;
7029 return;
7030 }
7031 }
7032 _ => {
7033 self.notify_clients(Err(format!(
7034 "MIDI tracks not found: {} or {}",
7035 from_track, to_track
7036 )))
7037 .await;
7038 return;
7039 }
7040 }
7041 }
7042 }
7043 };
7044 }
7045 Action::Disconnect {
7046 ref from_track,
7047 from_port,
7048 ref to_track,
7049 to_port,
7050 kind,
7051 } => {
7052 if kind == Kind::Audio {
7053 if let Err(e) = self.disconnect_audio_route_and_notify(a.clone()).await {
7054 self.notify_clients(Err(e)).await;
7055 }
7056 } else if kind == Kind::MIDI {
7057 let from_hw_in_device = Self::midi_hw_in_device(from_track);
7058 let to_hw_out_device = Self::midi_hw_out_device(to_track);
7059
7060 if let (Some(from_device), Some(to_device)) =
7061 (from_hw_in_device, to_hw_out_device)
7062 {
7063 let before = self.midi_hw_thru_routes.len();
7064 self.midi_hw_thru_routes.retain(|r| {
7065 !(r.from_device == from_device && r.to_device == to_device)
7066 });
7067 if self.midi_hw_thru_routes.len() < before {
7068 self.notify_clients(Ok(a.clone())).await;
7069 } else {
7070 self.notify_clients(Err(format!(
7071 "Disconnect failed: MIDI route not found ({} -> {})",
7072 from_track, to_track
7073 )))
7074 .await;
7075 }
7076 return;
7077 }
7078
7079 if let Some(device) = from_hw_in_device {
7080 let before = self.midi_hw_in_routes.len();
7081 self.midi_hw_in_routes.retain(|r| {
7082 !(r.device == device && r.to_track == *to_track && r.to_port == to_port)
7083 });
7084 if self.midi_hw_in_routes.len() < before {
7085 self.notify_clients(Ok(a.clone())).await;
7086 } else {
7087 self.notify_clients(Err(format!(
7088 "Disconnect failed: MIDI route not found ({} -> {})",
7089 from_track, to_track
7090 )))
7091 .await;
7092 }
7093 return;
7094 }
7095
7096 if let Some(device) = to_hw_out_device {
7097 let before = self.midi_hw_out_routes.len();
7098 self.midi_hw_out_routes.retain(|r| {
7099 !(r.from_track == *from_track
7100 && r.from_port == from_port
7101 && r.device == device)
7102 });
7103 if self.midi_hw_out_routes.len() < before {
7104 self.notify_clients(Ok(a.clone())).await;
7105 } else {
7106 self.notify_clients(Err(format!(
7107 "Disconnect failed: MIDI route not found ({} -> {})",
7108 from_track, to_track
7109 )))
7110 .await;
7111 }
7112 return;
7113 }
7114
7115 let state = self.state.lock();
7116 if let (Some(f_t), Some(t_t)) =
7117 (state.tracks.get(from_track), state.tracks.get(to_track))
7118 && let Some(to_in) = t_t.lock().midi.ins.get(to_port).cloned()
7119 {
7120 let from_track = f_t.lock();
7121 if let Err(e) = from_track.midi.disconnect_out(from_port, &to_in) {
7122 self.notify_clients(Err(e)).await;
7123 } else {
7124 from_track.invalidate_midi_route_cache();
7125 self.notify_clients(Ok(a.clone())).await;
7126 }
7127 } else {
7128 self.notify_clients(Err(format!(
7129 "Disconnect failed: MIDI ports not found ({} -> {})",
7130 from_track, to_track
7131 )))
7132 .await;
7133 }
7134 }
7135 }
7136
7137 Action::OpenAudioDevice {
7138 ref device,
7139 ref input_device,
7140 sample_rate_hz,
7141 bits,
7142 exclusive,
7143 period_frames,
7144 realtime_frames,
7145 low_watermark_frames,
7146 nperiods,
7147 sync_mode,
7148 hybrid_enabled,
7149 actual_period_frames: _,
7150 input_channels: _,
7151 output_channels: _,
7152 bytes_per_frame: _,
7153 } => {
7154 #[cfg(unix)]
7155 {
7156 let request = AudioOpenRequest {
7157 device,
7158 input_device: input_device.as_deref(),
7159 sample_rate_hz,
7160 bits,
7161 exclusive,
7162 period_frames,
7163 realtime_frames,
7164 low_watermark_frames,
7165 nperiods,
7166 sync_mode,
7167 hybrid_enabled,
7168 };
7169 if self.maybe_open_jack_runtime(request).await.is_some() {
7170 return;
7171 }
7172 }
7173 let hw_period = if hybrid_enabled {
7174 realtime_frames
7175 } else {
7176 period_frames
7177 };
7178 let hw_opts = Self::build_hw_options(exclusive, hw_period, nperiods, sync_mode);
7179 self.hybrid_playback_frames = period_frames.max(1);
7180 self.hybrid_realtime_frames = realtime_frames.max(1);
7181 self.hybrid_low_watermark_frames = low_watermark_frames.max(1);
7182 self.hybrid_enabled = hybrid_enabled;
7183 let open_result = self
7184 .open_non_jack_audio_device(
7185 device,
7186 input_device.as_deref(),
7187 sample_rate_hz,
7188 bits,
7189 hw_opts,
7190 )
7191 .await;
7192 match open_result {
7193 Ok(()) => {}
7194 Err(e) => {
7195 error!("Failed to open audio device: {e}");
7196 self.notify_clients(Err(e)).await;
7197 return;
7198 }
7199 }
7200 {
7201 let state = self.state.lock();
7202 for track in state.tracks.values() {
7203 track.lock().set_hybrid_enabled(hybrid_enabled);
7204 }
7205 }
7206 self.finalize_open_audio_device().await;
7207 if let Some(hw) = &self.hw_driver {
7208 let effective_action = {
7209 let hw = hw.lock();
7210 Action::OpenAudioDevice {
7211 device: device.clone(),
7212 input_device: input_device.clone(),
7213 sample_rate_hz: hw.sample_rate(),
7214 bits: hw.sample_bits(),
7215 exclusive,
7216 period_frames,
7217 realtime_frames,
7218 low_watermark_frames: low_watermark_frames.max(1),
7219 nperiods,
7220 sync_mode,
7221 hybrid_enabled,
7222 actual_period_frames: hw.cycle_samples(),
7223 input_channels: hw.input_channels(),
7224 output_channels: hw.output_channels(),
7225 bytes_per_frame: hw.frame_size_bytes(),
7226 }
7227 };
7228 action_to_process = effective_action;
7229 }
7230 }
7231 Action::JackAddAudioInputPort => {
7232 #[cfg(unix)]
7233 {
7234 if let Some(jack) = self.jack_runtime.clone() {
7235 let (input_channels, output_channels, rate) = {
7236 let jack = jack.lock();
7237 if let Err(e) = jack.add_audio_input_port() {
7238 self.notify_clients(Err(e)).await;
7239 return;
7240 }
7241 (
7242 jack.input_channels(),
7243 jack.output_channels(),
7244 jack.sample_rate,
7245 )
7246 };
7247 self.publish_hw_infos(input_channels, output_channels, rate)
7248 .await;
7249 self.notify_clients(Ok(a.clone())).await;
7250 } else {
7251 self.notify_clients(Err(
7252 "JACK runtime is not active; open the JACK backend first".to_string(),
7253 ))
7254 .await;
7255 }
7256 }
7257 #[cfg(not(unix))]
7258 {
7259 self.notify_clients(Err(
7260 "JACK backend is not available on this platform build".to_string(),
7261 ))
7262 .await;
7263 }
7264 }
7265 Action::JackRemoveAudioInputPort(_removed_port) => {
7266 #[cfg(unix)]
7267 {
7268 let removed_port = _removed_port;
7269 if let Some(jack) = self.jack_runtime.clone() {
7270 let (removed_port, removed_io) = {
7271 let jack = jack.lock();
7272 let removed_port = Some(removed_port);
7273 let removed_io =
7274 removed_port.and_then(|port| jack.input_audio_port(port));
7275 match (removed_port, removed_io) {
7276 (Some(port), Some(io)) => (port, io),
7277 _ => {
7278 self.notify_clients(Err(
7279 "JACK audio input port index is out of range".to_string(),
7280 ))
7281 .await;
7282 return;
7283 }
7284 }
7285 };
7286 let reindex_notifications =
7287 self.reindex_notifications_for_removed_hw_input(removed_port);
7288 for disconnect in
7289 self.disconnect_actions_for_removed_hw_input(removed_port, &removed_io)
7290 {
7291 if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await
7292 {
7293 self.notify_clients(Err(e)).await;
7294 return;
7295 }
7296 }
7297 let (input_channels, output_channels, rate) = {
7298 let jack = jack.lock();
7299 if let Err(e) = jack.remove_audio_input_port(removed_port) {
7300 self.notify_clients(Err(e)).await;
7301 return;
7302 }
7303 (
7304 jack.input_channels(),
7305 jack.output_channels(),
7306 jack.sample_rate,
7307 )
7308 };
7309 for action in reindex_notifications {
7310 self.notify_clients(Ok(action)).await;
7311 }
7312 self.publish_hw_infos(input_channels, output_channels, rate)
7313 .await;
7314 self.notify_clients(Ok(a.clone())).await;
7315 } else {
7316 self.notify_clients(Err(
7317 "JACK runtime is not active; open the JACK backend first".to_string(),
7318 ))
7319 .await;
7320 }
7321 }
7322 #[cfg(not(unix))]
7323 {
7324 self.notify_clients(Err(
7325 "JACK backend is not available on this platform build".to_string(),
7326 ))
7327 .await;
7328 }
7329 }
7330 Action::JackAddAudioOutputPort => {
7331 #[cfg(unix)]
7332 {
7333 if let Some(jack) = self.jack_runtime.clone() {
7334 let (input_channels, output_channels, rate) = {
7335 let jack = jack.lock();
7336 if let Err(e) = jack.add_audio_output_port() {
7337 self.notify_clients(Err(e)).await;
7338 return;
7339 }
7340 (
7341 jack.input_channels(),
7342 jack.output_channels(),
7343 jack.sample_rate,
7344 )
7345 };
7346 self.publish_hw_infos(input_channels, output_channels, rate)
7347 .await;
7348 self.notify_clients(Ok(a.clone())).await;
7349 } else {
7350 self.notify_clients(Err(
7351 "JACK runtime is not active; open the JACK backend first".to_string(),
7352 ))
7353 .await;
7354 }
7355 }
7356 #[cfg(not(unix))]
7357 {
7358 self.notify_clients(Err(
7359 "JACK backend is not available on this platform build".to_string(),
7360 ))
7361 .await;
7362 }
7363 }
7364 Action::JackRemoveAudioOutputPort(_removed_port) => {
7365 #[cfg(unix)]
7366 {
7367 let removed_port = _removed_port;
7368 if let Some(jack) = self.jack_runtime.clone() {
7369 let (removed_port, removed_io) = {
7370 let jack = jack.lock();
7371 let removed_port = Some(removed_port);
7372 let removed_io =
7373 removed_port.and_then(|port| jack.output_audio_port(port));
7374 match (removed_port, removed_io) {
7375 (Some(port), Some(io)) => (port, io),
7376 _ => {
7377 self.notify_clients(Err(
7378 "JACK audio output port index is out of range".to_string(),
7379 ))
7380 .await;
7381 return;
7382 }
7383 }
7384 };
7385 let reindex_notifications =
7386 self.reindex_notifications_for_removed_hw_output(removed_port);
7387 for disconnect in
7388 self.disconnect_actions_for_removed_hw_output(removed_port, &removed_io)
7389 {
7390 if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await
7391 {
7392 self.notify_clients(Err(e)).await;
7393 return;
7394 }
7395 }
7396 let (input_channels, output_channels, rate) = {
7397 let jack = jack.lock();
7398 if let Err(e) = jack.remove_audio_output_port(removed_port) {
7399 self.notify_clients(Err(e)).await;
7400 return;
7401 }
7402 (
7403 jack.input_channels(),
7404 jack.output_channels(),
7405 jack.sample_rate,
7406 )
7407 };
7408 for action in reindex_notifications {
7409 self.notify_clients(Ok(action)).await;
7410 }
7411 self.publish_hw_infos(input_channels, output_channels, rate)
7412 .await;
7413 self.notify_clients(Ok(a.clone())).await;
7414 } else {
7415 self.notify_clients(Err(
7416 "JACK runtime is not active; open the JACK backend first".to_string(),
7417 ))
7418 .await;
7419 }
7420 }
7421 #[cfg(not(unix))]
7422 {
7423 self.notify_clients(Err(
7424 "JACK backend is not available on this platform build".to_string(),
7425 ))
7426 .await;
7427 }
7428 }
7429 Action::OpenMidiInputDevice(ref device) => {
7430 let midi_hub = self.midi_hub.lock();
7431 if let Err(e) = midi_hub.open_input(device) {
7432 self.notify_clients(Err(e)).await;
7433 return;
7434 }
7435 }
7436 Action::OpenMidiOutputDevice(ref device) => {
7437 let midi_hub = self.midi_hub.lock();
7438 if let Err(e) = midi_hub.open_output(device) {
7439 self.notify_clients(Err(e)).await;
7440 return;
7441 }
7442 }
7443 Action::RequestSessionDiagnostics => {
7444 let (
7445 track_count,
7446 frozen_track_count,
7447 audio_clip_count,
7448 midi_clip_count,
7449 lv2_instance_count,
7450 vst3_instance_count,
7451 clap_instance_count,
7452 ) = {
7453 let tracks = &self.state.lock().tracks;
7454 let mut track_count = 0usize;
7455 let mut frozen_track_count = 0usize;
7456 let mut audio_clip_count = 0usize;
7457 let mut midi_clip_count = 0usize;
7458 #[cfg(all(unix, not(target_os = "macos")))]
7459 let mut lv2_instance_count = 0usize;
7460 #[cfg(not(all(unix, not(target_os = "macos"))))]
7461 let lv2_instance_count = 0usize;
7462 let mut vst3_instance_count = 0usize;
7463 let mut clap_instance_count = 0usize;
7464 for track in tracks.values() {
7465 let t = track.lock();
7466 track_count += 1;
7467 if t.frozen {
7468 frozen_track_count += 1;
7469 }
7470 audio_clip_count += t.audio.clips.len();
7471 midi_clip_count += t.midi.clips.len();
7472 #[cfg(all(unix, not(target_os = "macos")))]
7473 {
7474 lv2_instance_count += t.lv2_plugins.len();
7475 }
7476 vst3_instance_count += t.vst3_plugins.len();
7477 clap_instance_count += t.clap_plugins.len();
7478 }
7479 (
7480 track_count,
7481 frozen_track_count,
7482 audio_clip_count,
7483 midi_clip_count,
7484 lv2_instance_count,
7485 vst3_instance_count,
7486 clap_instance_count,
7487 )
7488 };
7489 #[cfg(not(all(unix, not(target_os = "macos"))))]
7490 let _lv2_instance_count = lv2_instance_count;
7491 let pending_hw_midi_events = self.pending_hw_midi_events.len()
7492 + self
7493 .pending_hw_midi_events_by_device
7494 .values()
7495 .map(std::vec::Vec::len)
7496 .sum::<usize>();
7497 let sample_rate_hz = if let Some(hw) = &self.hw_driver {
7498 hw.lock().sample_rate() as usize
7499 } else {
7500 #[cfg(unix)]
7501 {
7502 self.jack_runtime
7503 .as_ref()
7504 .map(|j| j.lock().sample_rate)
7505 .unwrap_or(0)
7506 }
7507 #[cfg(not(unix))]
7508 0
7509 };
7510 let cycle_samples = self.current_cycle_samples();
7511 tracing::info!(
7512 "Hybrid diagnostics: refill_budget_per_pass={}, refill_budget_throttle_count={}, realtime_fallback_dispatch_count={}, realtime_ready={}, refill_ready={}",
7513 self.refill_budget_per_pass,
7514 self.refill_budget_throttle_count,
7515 self.realtime_fallback_dispatch_count,
7516 self.ready_realtime_workers.len(),
7517 self.ready_refill_workers.len()
7518 );
7519 self.notify_clients(Ok(Action::SessionDiagnosticsReport {
7520 track_count,
7521 frozen_track_count,
7522 audio_clip_count,
7523 midi_clip_count,
7524 #[cfg(all(unix, not(target_os = "macos")))]
7525 lv2_instance_count,
7526 vst3_instance_count,
7527 clap_instance_count,
7528 pending_requests: self.pending_requests.len(),
7529 workers_total: self.workers.len(),
7530 workers_ready: self.ready_realtime_workers.len()
7531 + self.ready_refill_workers.len(),
7532 pending_hw_midi_events,
7533 playing: self.playing,
7534 transport_sample: self.transport_sample,
7535 tempo_bpm: self.tempo_bpm,
7536 sample_rate_hz,
7537 cycle_samples,
7538 }))
7539 .await;
7540 }
7541 Action::RequestMidiLearnMappingsReport => {
7542 let mut lines = Vec::<String>::new();
7543 let fmt_binding = |b: &crate::message::MidiLearnBinding| {
7544 let device = b.device.as_deref().unwrap_or("*");
7545 format!("{device} CH{} CC{}", b.channel + 1, b.cc)
7546 };
7547 if let Some(b) = self.global_midi_learn_play_pause.as_ref() {
7548 lines.push(format!("Global PlayPause: {}", fmt_binding(b)));
7549 }
7550 if let Some(b) = self.global_midi_learn_stop.as_ref() {
7551 lines.push(format!("Global Stop: {}", fmt_binding(b)));
7552 }
7553 if let Some(b) = self.global_midi_learn_record_toggle.as_ref() {
7554 lines.push(format!("Global RecordToggle: {}", fmt_binding(b)));
7555 }
7556 for (track_name, track) in self.state.lock().tracks.iter() {
7557 let t = track.lock();
7558 if let Some(b) = t.midi_learn_volume.as_ref() {
7559 lines.push(format!("{} Volume: {}", track_name, fmt_binding(b)));
7560 }
7561 if let Some(b) = t.midi_learn_balance.as_ref() {
7562 lines.push(format!("{} Balance: {}", track_name, fmt_binding(b)));
7563 }
7564 if let Some(b) = t.midi_learn_mute.as_ref() {
7565 lines.push(format!("{} Mute: {}", track_name, fmt_binding(b)));
7566 }
7567 if let Some(b) = t.midi_learn_solo.as_ref() {
7568 lines.push(format!("{} Solo: {}", track_name, fmt_binding(b)));
7569 }
7570 if let Some(b) = t.midi_learn_arm.as_ref() {
7571 lines.push(format!("{} Arm: {}", track_name, fmt_binding(b)));
7572 }
7573 if let Some(b) = t.midi_learn_input_monitor.as_ref() {
7574 lines.push(format!("{} InputMonitor: {}", track_name, fmt_binding(b)));
7575 }
7576 if let Some(b) = t.midi_learn_disk_monitor.as_ref() {
7577 lines.push(format!("{} DiskMonitor: {}", track_name, fmt_binding(b)));
7578 }
7579 }
7580 if lines.is_empty() {
7581 lines.push("No MIDI learn mappings configured".to_string());
7582 }
7583 self.notify_clients(Ok(Action::MidiLearnMappingsReport { lines }))
7584 .await;
7585 }
7586 Action::ClearAllMidiLearnBindings => {
7587 self.pending_midi_learn = None;
7588 self.pending_global_midi_learn = None;
7589 self.global_midi_learn_play_pause = None;
7590 self.global_midi_learn_stop = None;
7591 self.global_midi_learn_record_toggle = None;
7592 self.midi_cc_gate.clear();
7593 for track in self.state.lock().tracks.values() {
7594 let t = track.lock();
7595 t.midi_learn_volume = None;
7596 t.midi_learn_balance = None;
7597 t.midi_learn_mute = None;
7598 t.midi_learn_solo = None;
7599 t.midi_learn_arm = None;
7600 t.midi_learn_input_monitor = None;
7601 t.midi_learn_disk_monitor = None;
7602 }
7603 }
7604 #[cfg(all(unix, not(target_os = "macos")))]
7605 Action::TrackLv2PluginControls { .. } => {}
7606 #[cfg(all(unix, not(target_os = "macos")))]
7607 Action::ClipLv2PluginControls { .. } => {}
7608 #[cfg(all(unix, not(target_os = "macos")))]
7609 Action::TrackLv2Midnam { .. } => {}
7610 Action::TrackClapNoteNames { .. } => {}
7611 Action::SessionDiagnosticsReport { .. } => {}
7612 Action::MidiLearnMappingsReport { .. } => {}
7613 Action::HWInfo { .. } => {}
7614 Action::HistoryState { .. } => {}
7615 Action::Undo => {}
7616 Action::Redo => {}
7617 Action::ApplyGroupedActions(_) => {}
7618 _ => {}
7619 }
7620
7621 if let Some(inverse) = inverse_actions {
7622 if let Some(group) = self.history_group.as_mut() {
7623 group.forward_actions.push(action_to_process.clone());
7624 group.inverse_actions.splice(0..0, inverse);
7625 } else {
7626 self.history.record(UndoEntry {
7627 forward_actions: vec![action_to_process.clone()],
7628 inverse_actions: inverse,
7629 });
7630 }
7631 }
7632
7633 self.notify_clients(Ok(action_to_process)).await;
7634 }
7635
7636 pub async fn work(&mut self) {
7637 while let Some(message) = self.rx.recv().await {
7638 match message {
7639 Message::Ready(id) => self.push_ready_worker(id),
7640 Message::Finished {
7641 worker_id,
7642 track_name,
7643 output_linear,
7644 process_epoch,
7645 parameter_updates,
7646 } => {
7647 self.push_ready_worker(worker_id);
7648 self.track_processing_started_at.remove(&track_name);
7649 if process_epoch != self.track_process_epoch {
7650 if let Some(track) = self.state.lock().tracks.get(&track_name).cloned() {
7651 let t = track.lock();
7652 t.audio.finished = false;
7653 t.audio.processing = false;
7654 }
7655 continue;
7656 }
7657 self.track_meter_linear_by_track
7658 .insert(track_name, output_linear);
7659 for action in parameter_updates {
7660 self.notify_clients(Ok(action)).await;
7661 }
7662 self.force_stalled_track_completions();
7663 let all_finished = self.send_tracks().await;
7664 if all_finished {
7665 self.on_all_tracks_finished().await;
7666 }
7667 }
7668 Message::Channel(s) => {
7669 self.clients.push(s);
7670 }
7671
7672 Message::Request(a) => match a {
7673 Action::TrackOfflineBounceCancel { track_name } => {
7674 if let Some(job) = self.offline_bounce_jobs.get(&track_name) {
7675 job.cancel.store(true, Ordering::Relaxed);
7676 }
7677 }
7678 Action::TrackOfflineBounceCancelAll => {
7679 for job in self.offline_bounce_jobs.values() {
7680 job.cancel.store(true, Ordering::Relaxed);
7681 }
7682 }
7683 _ if !self.offline_bounce_jobs.is_empty() => {
7684 self.pending_requests.push_back(a);
7685 }
7686 Action::OpenAudioDevice { .. }
7687 | Action::OpenMidiInputDevice(_)
7688 | Action::OpenMidiOutputDevice(_)
7689 | Action::RequestMeterSnapshot
7690 | Action::Quit
7691 | Action::Log { .. }
7692 | Action::Play
7693 | Action::Pause
7694 | Action::Stop
7695 | Action::TransportPosition(_)
7696 | Action::JumpToEnd
7697 | Action::SetLoopEnabled(_)
7698 | Action::SetLoopRange(_)
7699 | Action::SetPunchEnabled(_)
7700 | Action::SetPunchRange(_)
7701 | Action::SetMetronomeEnabled(_)
7702 | Action::SetTempo(_)
7703 | Action::SetTimeSignature { .. }
7704 | Action::SetOscEnabled(_)
7705 | Action::SetClipPlaybackEnabled(_)
7706 | Action::SetRecordEnabled(_)
7707 | Action::SetSessionPath(_)
7708 | Action::ClearHistory
7709 | Action::BeginSessionRestore
7710 | Action::PianoKey { .. }
7711 | Action::ModifyMidiNotes { .. }
7712 | Action::ModifyMidiControllers { .. }
7713 | Action::DeleteMidiControllers { .. }
7714 | Action::InsertMidiControllers { .. }
7715 | Action::DeleteMidiNotes { .. }
7716 | Action::InsertMidiNotes { .. }
7717 | Action::SetMidiSysExEvents { .. } => {
7718 self.handle_request(a).await;
7719 }
7720 #[cfg(all(unix, not(target_os = "macos")))]
7721 Action::ListLv2Plugins => {
7722 self.handle_request(a).await;
7723 }
7724 Action::ListVst3Plugins => {
7725 self.handle_request(a).await;
7726 }
7727 Action::ListClapPlugins => {
7728 self.handle_request(a).await;
7729 }
7730 Action::ListClapPluginsWithCapabilities => {
7731 self.handle_request(a).await;
7732 }
7733 _ => {
7734 self.pending_requests.push_back(a);
7735 if self.can_schedule_hw_cycle() {
7736 self.request_hw_cycle().await;
7737 } else {
7738 while let Some(next) = self.pending_requests.pop_front() {
7739 self.handle_request(next).await;
7740 }
7741 }
7742 }
7743 },
7744 Message::OfflineBounceFinished { result } => {
7745 if let Ok(Action::TrackOfflineBounce { track_name, .. }) = &result {
7746 self.offline_bounce_jobs.remove(track_name);
7747 }
7748 self.notify_clients(result).await;
7749 if self.offline_bounce_jobs.is_empty() {
7750 while let Some(next) = self.pending_requests.pop_front() {
7751 self.handle_request(next).await;
7752 }
7753 }
7754 }
7755 Message::HWFinished => {
7756 if !self.awaiting_hwfinished {
7757 continue;
7758 }
7759 self.handling_hwfinished = true;
7760 self.awaiting_hwfinished = false;
7761 #[cfg(unix)]
7762 {
7763 if let Some(jack) = &self.jack_runtime {
7764 if !self.pending_hw_midi_out_events.is_empty() {
7765 let out_events =
7766 std::mem::take(&mut self.pending_hw_midi_out_events);
7767 jack.lock().write_events(&out_events);
7768 }
7769 let mut in_events = vec![];
7770 jack.lock().read_events_into(&mut in_events);
7771 if !in_events.is_empty() {
7772 self.pending_hw_midi_events.extend(in_events);
7773 }
7774 }
7775 }
7776 #[cfg(unix)]
7777 if self.jack_runtime.is_some() {
7778 self.sync_from_jack_transport().await;
7779 }
7780 while let Some(a) = self.pending_requests.pop_front() {
7781 self.handle_request(a).await;
7782 }
7783 self.apply_mute_solo_policy();
7784 self.append_recorded_cycle();
7785 self.flush_completed_recordings().await;
7786 let hw_in_routes = self.midi_hw_in_routes.clone();
7787 let pending_hw_in_by_device = self.pending_hw_midi_events_by_device.clone();
7788 let mut reconfigured_tracks = Vec::new();
7789 for (track_name, track) in self.state.lock().tracks.iter() {
7790 let track_lock = track.lock();
7791 if self.jack_runtime_is_some() {
7792 if !self.pending_hw_midi_events.is_empty() {
7793 track_lock.push_hw_midi_events(&self.pending_hw_midi_events);
7794 }
7795 } else {
7796 for route in hw_in_routes.iter().filter(|r| &r.to_track == track_name) {
7797 if let Some(events) = pending_hw_in_by_device.get(&route.device) {
7798 track_lock.push_hw_midi_events_to_port(route.to_port, events);
7799 }
7800 }
7801 }
7802 if track_lock.setup() {
7803 reconfigured_tracks.push(track_name.clone());
7804 }
7805 }
7806 self.publish_track_meters().await;
7807 for track_name in reconfigured_tracks {
7808 let track = self.state.lock().tracks.get(&track_name).cloned();
7809 if let Some(track) = track {
7810 let (plugins, connections) = {
7811 let track_lock = track.lock();
7812 (
7813 track_lock.plugin_graph_plugins(),
7814 track_lock.plugin_graph_connections(),
7815 )
7816 };
7817 self.notify_clients(Ok(Action::TrackPluginGraph {
7818 track_name: track_name.clone(),
7819 plugins,
7820 connections,
7821 }))
7822 .await;
7823 }
7824 }
7825 self.pending_hw_midi_events.clear();
7826 self.pending_hw_midi_events_by_device.clear();
7827 if self.playing {
7828 if self.transport_panic_flush_pending {
7829 self.transport_panic_flush_pending = false;
7830 } else if self.transport_restart_pending {
7831 self.transport_restart_pending = false;
7832 } else {
7833 let next = self
7834 .transport_sample
7835 .saturating_add(self.current_cycle_samples());
7836 let normalized = self.normalize_transport_sample(next);
7837 let wrapped = normalized != next;
7838 self.transport_sample = normalized;
7839 if wrapped {
7840 if self.notified_loop_wrap_sample == Some(self.transport_sample) {
7841 self.notified_loop_wrap_sample = None;
7842 } else {
7843 self.notify_clients(Ok(Action::TransportPosition(
7844 self.transport_sample,
7845 )))
7846 .await;
7847 }
7848 }
7849 }
7850 }
7851 if self.send_tracks().await && self.hw_worker.is_some() {
7852 self.request_hw_cycle().await;
7853 }
7854 #[cfg(unix)]
7855 {
7856 if self.jack_runtime.is_some() {
7857 self.awaiting_hwfinished = true;
7858 }
7859 }
7860 self.handling_hwfinished = false;
7861 }
7862 Message::HWMidiEvents(events) => {
7863 for hw_event in events {
7864 let thru_targets: Vec<String> = self
7865 .midi_hw_thru_routes
7866 .iter()
7867 .filter(|route| route.from_device == hw_event.device)
7868 .map(|route| route.to_device.clone())
7869 .collect();
7870 for device in thru_targets {
7871 self.pending_hw_midi_out_events_by_device.push(HwMidiEvent {
7872 device,
7873 event: hw_event.event.clone(),
7874 });
7875 }
7876 if hw_event.event.data.len() >= 3 {
7877 let status = hw_event.event.data[0];
7878 if status & 0xF0 == 0xB0 {
7879 let channel = status & 0x0F;
7880 let cc = hw_event.event.data[1];
7881 let value = hw_event.event.data[2];
7882 self.handle_incoming_hw_cc(&hw_event.device, channel, cc, value)
7883 .await;
7884 }
7885 }
7886 self.pending_hw_midi_events_by_device
7887 .entry(hw_event.device)
7888 .or_default()
7889 .push(hw_event.event);
7890 }
7891 }
7892 _ => {}
7893 }
7894 }
7895 }
7896
7897 fn collect_hw_midi_output_events(&self) -> Vec<MidiEvent> {
7898 let mut events = vec![];
7899 for track in self.state.lock().tracks.values() {
7900 events.extend(
7901 track
7902 .lock()
7903 .take_hw_midi_out_events()
7904 .into_iter()
7905 .map(|evt| evt.event),
7906 );
7907 }
7908 events.sort_by_key(|a| a.frame);
7909 events
7910 }
7911
7912 fn collect_hw_midi_output_events_by_device(&mut self) -> Vec<HwMidiEvent> {
7913 let mut events = Vec::<HwMidiEvent>::new();
7914 let routes = self.midi_hw_out_routes.clone();
7915 let mut events_by_track = HashMap::<String, Vec<crate::track::HwMidiOutEvent>>::new();
7916 {
7917 let state = self.state.lock();
7918 for route in &routes {
7919 if events_by_track.contains_key(&route.from_track) {
7920 continue;
7921 }
7922 let Some(track) = state.tracks.get(&route.from_track) else {
7923 continue;
7924 };
7925 events_by_track.insert(
7926 route.from_track.clone(),
7927 track.lock().take_hw_midi_out_events(),
7928 );
7929 }
7930 }
7931
7932 for route in routes {
7933 let Some(track_events) = events_by_track.get(&route.from_track) else {
7934 continue;
7935 };
7936 for hw_event in track_events
7937 .iter()
7938 .filter(|evt| evt.port == route.from_port)
7939 {
7940 self.update_active_hw_notes_for_track(
7941 &route.from_track,
7942 &route.device,
7943 &hw_event.event.data,
7944 );
7945 events.push(HwMidiEvent {
7946 device: route.device.clone(),
7947 event: hw_event.event.clone(),
7948 });
7949 }
7950 }
7951 events.sort_by(|a, b| {
7952 a.event
7953 .frame
7954 .cmp(&b.event.frame)
7955 .then_with(|| a.device.cmp(&b.device))
7956 });
7957 events
7958 }
7959}
7960
7961#[cfg(test)]
7962mod tests {
7963 use super::*;
7964 use crate::mutex::UnsafeMutex;
7965 use tokio::sync::mpsc::channel;
7966 use tokio::time::{Duration as TokioDuration, timeout};
7967
7968 #[test]
7969 #[cfg(unix)]
7970 fn jack_transport_sync_decision_starts_and_syncs_position_on_external_play() {
7971 let decision = Engine::jack_transport_sync_decision(false, 128, true, 256, 64);
7972
7973 assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Start));
7974 assert_eq!(decision.position_sync, Some(256));
7975 }
7976
7977 #[test]
7978 #[cfg(unix)]
7979 fn jack_transport_sync_decision_stops_and_syncs_position_on_external_stop() {
7980 let decision = Engine::jack_transport_sync_decision(true, 512, false, 96, 64);
7981
7982 assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Stop));
7983 assert_eq!(decision.position_sync, Some(96));
7984 }
7985
7986 #[test]
7987 #[cfg(unix)]
7988 fn jack_transport_sync_decision_ignores_small_rolling_drift() {
7989 let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1040, 64);
7990
7991 assert_eq!(decision.play_sync, None);
7992 assert_eq!(decision.position_sync, None);
7993 }
7994
7995 #[test]
7996 #[cfg(unix)]
7997 fn jack_transport_sync_decision_syncs_large_rolling_jump() {
7998 let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1200, 64);
7999
8000 assert_eq!(decision.play_sync, None);
8001 assert_eq!(decision.position_sync, Some(1200));
8002 }
8003
8004 #[test]
8005 #[cfg(unix)]
8006 fn jack_transport_sync_decision_syncs_locate_while_stopped() {
8007 let decision = Engine::jack_transport_sync_decision(false, 400, false, 900, 64);
8008
8009 assert_eq!(decision.play_sync, None);
8010 assert_eq!(decision.position_sync, Some(900));
8011 }
8012
8013 fn make_engine_with_client() -> (Engine, tokio::sync::mpsc::Receiver<Message>) {
8014 let (engine_tx, engine_rx) = channel(16);
8015 let mut engine = Engine::new(engine_rx, engine_tx);
8016 let (client_tx, client_rx) = channel(16);
8017 engine.clients.push(client_tx);
8018 (engine, client_rx)
8019 }
8020
8021 fn insert_track(engine: &mut Engine, track: Track) {
8022 engine.state.lock().tracks.insert(
8023 track.name.clone(),
8024 Arc::new(UnsafeMutex::new(Box::new(track))),
8025 );
8026 }
8027
8028 fn osc_packet(address: &str) -> Vec<u8> {
8029 fn push_padded_osc_string(packet: &mut Vec<u8>, value: &str) {
8030 packet.extend_from_slice(value.as_bytes());
8031 packet.push(0);
8032 while !packet.len().is_multiple_of(4) {
8033 packet.push(0);
8034 }
8035 }
8036
8037 let mut packet = Vec::new();
8038 push_padded_osc_string(&mut packet, address);
8039 push_padded_osc_string(&mut packet, ",");
8040 packet
8041 }
8042
8043 #[tokio::test]
8044 async fn set_osc_enabled_starts_and_stops_server() {
8045 let (mut engine, _client_rx) = make_engine_with_client();
8046
8047 engine
8048 .set_osc_enabled_with(true, |tx| OscServer::start_on_addr(tx, "127.0.0.1:0"))
8049 .expect("start osc server on ephemeral port");
8050 assert!(engine.osc_server.is_some());
8051
8052 engine
8053 .set_osc_enabled_with(false, OscServer::start)
8054 .expect("stop osc server");
8055 assert!(engine.osc_server.is_none());
8056 }
8057
8058 #[tokio::test]
8059 async fn osc_server_forwards_transport_packets_to_engine_channel() {
8060 let (tx, mut rx) = channel(4);
8061 let mut server =
8062 OscServer::start_on_addr(tx, "127.0.0.1:0").expect("start osc test server");
8063 let socket = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind sender socket");
8064 let packet = osc_packet("/transport/play");
8065 socket
8066 .send_to(&packet, server.listen_addr())
8067 .expect("send osc packet");
8068
8069 let message = timeout(TokioDuration::from_secs(1), rx.recv())
8070 .await
8071 .expect("packet delivery timeout")
8072 .expect("osc message");
8073 match message {
8074 Message::Request(Action::Play) => {}
8075 other => panic!("unexpected osc message: {other:?}"),
8076 }
8077
8078 server.stop();
8079 }
8080
8081 #[tokio::test]
8082 async fn track_offline_bounce_rejects_zero_length_requests() {
8083 let (mut engine, mut client_rx) = make_engine_with_client();
8084 insert_track(
8085 &mut engine,
8086 Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
8087 );
8088
8089 engine
8090 .handle_request(Action::TrackOfflineBounce {
8091 track_name: "track".to_string(),
8092 output_path: "/tmp/out.wav".to_string(),
8093 start_sample: 0,
8094 length_samples: 0,
8095 automation_lanes: vec![],
8096 apply_fader: false,
8097 })
8098 .await;
8099
8100 match client_rx.recv().await.expect("response") {
8101 Message::Response(Err(err)) => {
8102 assert!(err.contains("has no renderable content for offline bounce"));
8103 }
8104 other => panic!("unexpected message: {other:?}"),
8105 }
8106 }
8107
8108 #[tokio::test]
8109 async fn track_offline_bounce_rejects_when_same_track_is_active() {
8110 let (mut engine, mut client_rx) = make_engine_with_client();
8111 engine.offline_bounce_jobs.insert(
8112 "other".to_string(),
8113 OfflineBounceJob {
8114 cancel: Arc::new(AtomicBool::new(false)),
8115 },
8116 );
8117
8118 engine
8119 .handle_request(Action::TrackOfflineBounce {
8120 track_name: "other".to_string(),
8121 output_path: "/tmp/out.wav".to_string(),
8122 start_sample: 0,
8123 length_samples: 128,
8124 automation_lanes: vec![],
8125 apply_fader: false,
8126 })
8127 .await;
8128
8129 match client_rx.recv().await.expect("response") {
8130 Message::Response(Err(err)) => {
8131 assert!(err.contains("already in progress"));
8132 }
8133 other => panic!("unexpected message: {other:?}"),
8134 }
8135 }
8136
8137 #[tokio::test]
8138 async fn track_offline_bounce_allows_different_track_concurrently() {
8139 let (mut engine, _client_rx) = make_engine_with_client();
8140 insert_track(
8141 &mut engine,
8142 Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
8143 );
8144 engine.offline_bounce_jobs.insert(
8145 "other".to_string(),
8146 OfflineBounceJob {
8147 cancel: Arc::new(AtomicBool::new(false)),
8148 },
8149 );
8150
8151 engine
8152 .handle_request(Action::TrackOfflineBounce {
8153 track_name: "track".to_string(),
8154 output_path: "/tmp/out.wav".to_string(),
8155 start_sample: 0,
8156 length_samples: 128,
8157 automation_lanes: vec![],
8158 apply_fader: false,
8159 })
8160 .await;
8161
8162 assert!(engine.offline_bounce_jobs.contains_key("other"));
8163 assert_eq!(engine.pending_requests.len(), 1);
8164 }
8165
8166 #[tokio::test]
8167 async fn reject_if_track_frozen_sends_error_and_blocks_operation() {
8168 let (mut engine, mut client_rx) = make_engine_with_client();
8169 let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
8170 track.set_frozen(true);
8171 insert_track(&mut engine, track);
8172
8173 let rejected = engine
8174 .reject_if_track_frozen("track", "arming/disarming")
8175 .await;
8176
8177 assert!(rejected);
8178 match client_rx.recv().await.expect("response") {
8179 Message::Response(Err(err)) => {
8180 assert_eq!(err, "Track 'track' is frozen; arming/disarming is blocked");
8181 }
8182 other => panic!("unexpected message: {other:?}"),
8183 }
8184 }
8185
8186 #[tokio::test]
8187 async fn undo_restores_original_clip_bounds_after_stretch_style_group() {
8188 let (mut engine, _client_rx) = make_engine_with_client();
8189 let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
8190 let mut clip = AudioClip::new("audio/original.wav".to_string(), 100, 220);
8191 clip.offset = 12;
8192 clip.fade_in_samples = 20;
8193 clip.fade_out_samples = 30;
8194 track.audio.clips.push(clip);
8195 insert_track(&mut engine, track);
8196
8197 engine.handle_request(Action::BeginHistoryGroup).await;
8198 engine
8199 .handle_request(Action::SetClipBounds {
8200 track_name: "track".to_string(),
8201 clip_index: 0,
8202 kind: Kind::Audio,
8203 start: 120,
8204 length: 180,
8205 offset: 0,
8206 })
8207 .await;
8208 engine
8209 .handle_request(Action::SetClipSourceName {
8210 track_name: "track".to_string(),
8211 clip_index: 0,
8212 kind: Kind::Audio,
8213 name: "audio/stretched.wav".to_string(),
8214 })
8215 .await;
8216 engine
8217 .handle_request(Action::SetClipFade {
8218 track_name: "track".to_string(),
8219 clip_index: 0,
8220 kind: Kind::Audio,
8221 fade_enabled: true,
8222 fade_in_samples: 12,
8223 fade_out_samples: 12,
8224 })
8225 .await;
8226 engine.handle_request(Action::EndHistoryGroup).await;
8227
8228 engine.handle_request(Action::Undo).await;
8229
8230 let state = engine.state.lock();
8231 let track = state.tracks.get("track").expect("track exists").lock();
8232 let clip = track.audio.clips.first().expect("clip exists");
8233 assert_eq!(clip.name, "audio/original.wav");
8234 assert_eq!(clip.start, 100);
8235 assert_eq!(clip.end, 220);
8236 assert_eq!(clip.end.saturating_sub(clip.start), 120);
8237 assert_eq!(clip.offset, 12);
8238 }
8239
8240 #[tokio::test]
8241 async fn track_offline_bounce_queues_when_no_worker_is_ready() {
8242 let (mut engine, _client_rx) = make_engine_with_client();
8243 insert_track(
8244 &mut engine,
8245 Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
8246 );
8247
8248 engine
8249 .handle_request(Action::TrackOfflineBounce {
8250 track_name: "track".to_string(),
8251 output_path: "/tmp/out.wav".to_string(),
8252 start_sample: 0,
8253 length_samples: 128,
8254 automation_lanes: vec![],
8255 apply_fader: false,
8256 })
8257 .await;
8258
8259 assert!(engine.offline_bounce_jobs.is_empty());
8260 assert_eq!(engine.pending_requests.len(), 1);
8261 assert!(matches!(
8262 engine.pending_requests.front(),
8263 Some(Action::TrackOfflineBounce { track_name, length_samples, .. })
8264 if track_name == "track" && *length_samples == 128
8265 ));
8266 }
8267
8268 #[tokio::test]
8269 async fn track_offline_bounce_returns_missing_track_error() {
8270 let (mut engine, mut client_rx) = make_engine_with_client();
8271
8272 engine
8273 .handle_request(Action::TrackOfflineBounce {
8274 track_name: "missing".to_string(),
8275 output_path: "/tmp/out.wav".to_string(),
8276 start_sample: 0,
8277 length_samples: 128,
8278 automation_lanes: vec![],
8279 apply_fader: false,
8280 })
8281 .await;
8282
8283 match client_rx.recv().await.expect("response") {
8284 Message::Response(Err(err)) => {
8285 assert_eq!(err, "Track not found: missing");
8286 }
8287 other => panic!("unexpected message: {other:?}"),
8288 }
8289 }
8290
8291 #[tokio::test]
8292 async fn track_offline_bounce_clears_job_when_worker_send_fails() {
8293 let (mut engine, mut client_rx) = make_engine_with_client();
8294 insert_track(
8295 &mut engine,
8296 Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
8297 );
8298 let (worker_tx, worker_rx) = channel(1);
8299 drop(worker_rx);
8300 engine
8301 .workers
8302 .push(WorkerData::new(worker_tx, tokio::spawn(async {})));
8303 engine.ready_refill_workers.push(0);
8304
8305 engine
8306 .handle_request(Action::TrackOfflineBounce {
8307 track_name: "track".to_string(),
8308 output_path: "/tmp/out.wav".to_string(),
8309 start_sample: 0,
8310 length_samples: 128,
8311 automation_lanes: vec![],
8312 apply_fader: false,
8313 })
8314 .await;
8315
8316 assert!(engine.offline_bounce_jobs.is_empty());
8317 match client_rx.recv().await.expect("response") {
8318 Message::Response(Err(err)) => {
8319 assert!(err.contains("Failed to schedule offline bounce"));
8320 }
8321 other => panic!("unexpected message: {other:?}"),
8322 }
8323 }
8324}