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