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