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 track.lock().toggle_master();
2522 self.notify_clients(Ok(Action::TrackToggleMaster(track_name.clone())))
2523 .await;
2524 }
2525 }
2526 Action::TrackToggleArm(ref track_name) => {
2527 if let Some(track) = self.state.lock().tracks.get(track_name) {
2528 track.lock().arm();
2529 self.notify_clients(Ok(Action::TrackToggleArm(track_name.clone())))
2530 .await;
2531 }
2532 }
2533 Action::TrackToggleInputMonitor {
2534 ref track_name,
2535 lane,
2536 } => {
2537 if let Some(track) = self.state.lock().tracks.get(track_name) {
2538 track.lock().toggle_input_monitor(lane);
2539 self.notify_clients(Ok(Action::TrackToggleInputMonitor {
2540 track_name: track_name.clone(),
2541 lane,
2542 }))
2543 .await;
2544 }
2545 }
2546 Action::TrackToggleDiskMonitor {
2547 ref track_name,
2548 lane,
2549 } => {
2550 if let Some(track) = self.state.lock().tracks.get(track_name) {
2551 track.lock().toggle_disk_monitor(lane);
2552 self.notify_clients(Ok(Action::TrackToggleDiskMonitor {
2553 track_name: track_name.clone(),
2554 lane,
2555 }))
2556 .await;
2557 }
2558 }
2559 Action::TrackToggleMidiInputMonitor {
2560 ref track_name,
2561 lane,
2562 } => {
2563 if let Some(track) = self.state.lock().tracks.get(track_name) {
2564 track.lock().toggle_midi_input_monitor(lane);
2565 self.notify_clients(Ok(Action::TrackToggleMidiInputMonitor {
2566 track_name: track_name.clone(),
2567 lane,
2568 }))
2569 .await;
2570 }
2571 }
2572 Action::TrackToggleMidiDiskMonitor {
2573 ref track_name,
2574 lane,
2575 } => {
2576 if let Some(track) = self.state.lock().tracks.get(track_name) {
2577 track.lock().toggle_midi_disk_monitor(lane);
2578 self.notify_clients(Ok(Action::TrackToggleMidiDiskMonitor {
2579 track_name: track_name.clone(),
2580 lane,
2581 }))
2582 .await;
2583 }
2584 }
2585 _ => {}
2586 }
2587 }
2588 for action in mapped_global_actions {
2589 self.handle_request_inner(action, false).await;
2590 }
2591 }
2592
2593 fn upstream_audio_track_names(
2594 &self,
2595 seeds: &std::collections::HashSet<String>,
2596 ) -> std::collections::HashSet<String> {
2597 let state = self.state.lock();
2598 let mut output_to_track: std::collections::HashMap<
2599 *const crate::audio::io::AudioIO,
2600 String,
2601 > = std::collections::HashMap::new();
2602 for (name, track) in &state.tracks {
2603 let t = track.lock();
2604 for out in &t.audio.outs {
2605 output_to_track.insert(std::sync::Arc::as_ptr(out), name.clone());
2606 }
2607 }
2608 let mut upstream = std::collections::HashSet::new();
2609 let mut to_process: Vec<String> = seeds.iter().cloned().collect();
2610 let mut processed = std::collections::HashSet::new();
2611 while let Some(target_name) = to_process.pop() {
2612 if !processed.insert(target_name.clone()) {
2613 continue;
2614 }
2615 if let Some(target_track) = state.tracks.get(&target_name) {
2616 let tt = target_track.lock();
2617 for input in &tt.audio.ins {
2618 for conn in input.connections.lock().iter() {
2619 let conn_ptr = std::sync::Arc::as_ptr(conn);
2620 if let Some(source_name) = output_to_track.get(&conn_ptr)
2621 && source_name != &target_name
2622 && !seeds.contains(source_name)
2623 {
2624 upstream.insert(source_name.clone());
2625 to_process.push(source_name.clone());
2626 }
2627 }
2628 }
2629 }
2630 }
2631 upstream
2632 }
2633
2634 fn is_track_in_soloed_folder(
2635 &self,
2636 track: &Track,
2637 tracks: &std::collections::HashMap<String, Arc<UnsafeMutex<Box<Track>>>>,
2638 ) -> bool {
2639 let mut current = track.parent_track.as_deref();
2640 while let Some(parent_name) = current {
2641 if let Some(parent) = tracks.get(parent_name) {
2642 let p = parent.lock();
2643 if p.soloed {
2644 return true;
2645 }
2646 current = p.parent_track.as_deref();
2647 } else {
2648 break;
2649 }
2650 }
2651 false
2652 }
2653
2654 fn folder_has_soloed_descendant(
2655 &self,
2656 folder_name: &str,
2657 tracks: &std::collections::HashMap<String, Arc<UnsafeMutex<Box<Track>>>>,
2658 ) -> bool {
2659 for track in tracks.values() {
2660 let t = track.lock();
2661 if !t.soloed {
2662 continue;
2663 }
2664 let mut current = t.parent_track.as_deref();
2665 while let Some(parent_name) = current {
2666 if parent_name == folder_name {
2667 return true;
2668 }
2669 if let Some(parent) = tracks.get(parent_name) {
2670 current = parent.lock().parent_track.as_deref();
2671 } else {
2672 break;
2673 }
2674 }
2675 }
2676 false
2677 }
2678
2679 fn refresh_realtime_infection(&self) {
2680 let state = self.state.lock();
2681 let live_seeds: std::collections::HashSet<String> = state
2682 .tracks
2683 .iter()
2684 .filter_map(|(name, track)| {
2685 let t = track.lock();
2686 if t.armed && t.input_monitor.iter().any(|&m| m) {
2687 Some(name.clone())
2688 } else {
2689 None
2690 }
2691 })
2692 .collect();
2693 let mut output_owner: std::collections::HashMap<*const crate::audio::io::AudioIO, String> =
2694 std::collections::HashMap::new();
2695 for (name, track) in state.tracks.iter() {
2696 let t = track.lock();
2697 for out in &t.audio.outs {
2698 output_owner.insert(std::sync::Arc::as_ptr(out), name.clone());
2699 }
2700 }
2701
2702 let mut infected = live_seeds.clone();
2703 let mut mixed_nodes = std::collections::HashSet::new();
2704 loop {
2705 let mut changed = false;
2706 for (name, track) in state.tracks.iter() {
2707 let t = track.lock();
2708 let mut upstream_owners = std::collections::HashSet::new();
2709 for input in &t.audio.ins {
2710 for conn in input.connections.lock().iter() {
2711 if let Some(owner) = output_owner.get(&std::sync::Arc::as_ptr(conn)) {
2712 upstream_owners.insert(owner.clone());
2713 }
2714 }
2715 }
2716 if upstream_owners.is_empty() {
2717 continue;
2718 }
2719 let has_realtime = upstream_owners
2720 .iter()
2721 .any(|owner| infected.contains(owner) || live_seeds.contains(owner));
2722 let has_playback = upstream_owners
2723 .iter()
2724 .any(|owner| !infected.contains(owner) && !live_seeds.contains(owner));
2725 if has_realtime && has_playback {
2726 mixed_nodes.insert(name.clone());
2727 }
2728 if has_realtime && infected.insert(name.clone()) {
2729 changed = true;
2730 }
2731 }
2732 if !changed {
2733 break;
2734 }
2735 }
2736
2737 for (name, track) in state.tracks.iter() {
2738 let forced = infected.contains(name) && !live_seeds.contains(name);
2739 let t = track.lock();
2740 t.set_shared_realtime_mixed(mixed_nodes.contains(name));
2741 t.set_force_realtime_domain(forced);
2742 }
2743 }
2744
2745 fn apply_mute_solo_policy(&mut self) {
2746 let mut newly_disabled_tracks = Vec::new();
2747 {
2748 let tracks = &self.state.lock().tracks;
2749 let soloed: std::collections::HashSet<String> = tracks
2750 .iter()
2751 .filter_map(|(name, t)| {
2752 if t.lock().soloed {
2753 Some(name.clone())
2754 } else {
2755 None
2756 }
2757 })
2758 .collect();
2759 let any_soloed = !soloed.is_empty();
2760 let upstream = if any_soloed {
2761 self.upstream_audio_track_names(&soloed)
2762 } else {
2763 std::collections::HashSet::new()
2764 };
2765 for track in tracks.values() {
2766 let t = track.lock();
2767 let was_enabled = t.output_enabled;
2768 let in_soloed_folder = self.is_track_in_soloed_folder(t, tracks);
2769 let folder_with_soloed_child =
2770 t.is_folder && self.folder_has_soloed_descendant(&t.name, tracks);
2771 let enabled = if t.is_master {
2772 !t.muted
2773 } else if any_soloed {
2774 (t.soloed
2775 || upstream.contains(&t.name)
2776 || in_soloed_folder
2777 || folder_with_soloed_child)
2778 && !t.muted
2779 } else {
2780 !t.muted
2781 };
2782 t.set_output_enabled(enabled);
2783 if was_enabled && !enabled {
2784 newly_disabled_tracks.push(t.name.clone());
2785 }
2786 }
2787 }
2788 let mut note_off_events = Vec::new();
2789 for track_name in newly_disabled_tracks {
2790 note_off_events.extend(self.note_off_events_for_track(&track_name));
2791 }
2792 if !note_off_events.is_empty() {
2793 self.pending_hw_midi_out_events_by_device
2794 .extend(note_off_events);
2795 }
2796 }
2797
2798 fn sanitize_file_stem(name: &str) -> String {
2799 let mut out = String::with_capacity(name.len());
2800 for c in name.chars() {
2801 if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
2802 out.push(c);
2803 } else {
2804 out.push('_');
2805 }
2806 }
2807 if out.is_empty() {
2808 "track".to_string()
2809 } else {
2810 out
2811 }
2812 }
2813
2814 fn next_recording_file_name(track_name: &str) -> String {
2815 let ts = SystemTime::now()
2816 .duration_since(UNIX_EPOCH)
2817 .map(|d| d.as_secs())
2818 .unwrap_or(0);
2819 format!("{}_{}.wav", Self::sanitize_file_stem(track_name), ts)
2820 }
2821
2822 fn next_midi_recording_file_name(track_name: &str) -> String {
2823 let ts = SystemTime::now()
2824 .duration_since(UNIX_EPOCH)
2825 .map(|d| d.as_secs())
2826 .unwrap_or(0);
2827 format!("{}_{}.mid", Self::sanitize_file_stem(track_name), ts)
2828 }
2829
2830 fn append_recorded_cycle(&mut self) {
2831 if !self.playing || !self.record_enabled {
2832 return;
2833 }
2834 for (name, track_handle) in &self.state.lock().tracks {
2835 let track = track_handle.lock();
2836 if !track.armed {
2837 continue;
2838 }
2839 let audio_channels = track.record_tap_outs.len();
2840 let audio_frames = track
2841 .record_tap_outs
2842 .first()
2843 .map(|ch| ch.len())
2844 .unwrap_or(0);
2845 let frames = audio_frames.max(self.current_cycle_samples());
2846 if frames == 0 {
2847 continue;
2848 }
2849 let segments = self.recording_segments_for_cycle(frames);
2850 for (segment_start, segment_end, frame_offset) in segments {
2851 let segment_len = segment_end.saturating_sub(segment_start);
2852 if segment_len == 0 {
2853 continue;
2854 }
2855
2856 if audio_channels > 0 && audio_frames > 0 {
2857 let audio_entry =
2858 self.audio_recordings
2859 .entry(name.clone())
2860 .or_insert_with(|| RecordingSession {
2861 start_sample: segment_start,
2862 samples: Vec::with_capacity(segment_len * audio_channels * 2),
2863 channels: audio_channels,
2864 file_name: Self::next_recording_file_name(name),
2865 stripe_peaks: vec![Vec::new(); audio_channels],
2866 current_stripe_frames: 0,
2867 });
2868 if audio_entry.channels != audio_channels {
2869 continue;
2870 }
2871 if let Some(entry) = self.audio_recordings.get_mut(name.as_str()) {
2872 let from = frame_offset.min(audio_frames);
2873 let to = frame_offset.saturating_add(segment_len).min(audio_frames);
2874 for frame in from..to {
2875 let is_new_stripe =
2876 entry.current_stripe_frames % RECORDING_STRIPE_FRAMES == 0;
2877 for ch in 0..audio_channels {
2878 let sample = track.record_tap_outs[ch][frame].clamp(-1.0, 1.0);
2879 if is_new_stripe {
2880 entry.stripe_peaks[ch].push([sample, sample]);
2881 } else {
2882 let idx = entry.stripe_peaks[ch].len() - 1;
2883 entry.stripe_peaks[ch][idx][0] =
2884 entry.stripe_peaks[ch][idx][0].min(sample);
2885 entry.stripe_peaks[ch][idx][1] =
2886 entry.stripe_peaks[ch][idx][1].max(sample);
2887 }
2888 entry.samples.push(track.record_tap_outs[ch][frame]);
2889 }
2890 entry.current_stripe_frames += 1;
2891 }
2892 }
2893 }
2894
2895 let entry = self.midi_recordings.entry(name.clone()).or_insert_with(|| {
2896 MidiRecordingSession {
2897 start_sample: segment_start,
2898 events: Vec::new(),
2899 file_name: Self::next_midi_recording_file_name(name),
2900 }
2901 });
2902 let from = frame_offset;
2903 let to = frame_offset.saturating_add(segment_len);
2904 for event in &track.record_tap_midi_in {
2905 let frame = event.frame as usize;
2906 if frame < from || frame >= to {
2907 continue;
2908 }
2909 let abs_sample = segment_start as u64 + (frame - from) as u64;
2910 entry.events.push((abs_sample, event.data.clone()));
2911 }
2912
2913 if self.punch_enabled
2914 && let Some((_, punch_end)) = self.punch_range_samples
2915 && segment_end == punch_end
2916 {
2917 if let Some(done) = self.audio_recordings.remove(name.as_str()) {
2918 self.completed_audio_recordings.push((name.clone(), done));
2919 }
2920 if let Some(done) = self.midi_recordings.remove(name.as_str()) {
2921 self.completed_midi_recordings.push((name.clone(), done));
2922 }
2923 } else if self.loop_enabled
2924 && let Some((_, loop_end)) = self.loop_range_samples
2925 && segment_end == loop_end
2926 {
2927 if let Some(done) = self.audio_recordings.remove(name.as_str()) {
2928 self.completed_audio_recordings.push((name.clone(), done));
2929 }
2930 if let Some(done) = self.midi_recordings.remove(name.as_str()) {
2931 self.completed_midi_recordings.push((name.clone(), done));
2932 }
2933 }
2934 }
2935 }
2936 }
2937
2938 async fn flush_completed_recordings(&mut self) {
2939 if self.completed_audio_recordings.is_empty() && self.completed_midi_recordings.is_empty() {
2940 return;
2941 }
2942 let Some(audio_dir) = self.session_audio_dir() else {
2943 self.completed_audio_recordings.clear();
2944 self.completed_midi_recordings.clear();
2945 return;
2946 };
2947 let Some(midi_dir) = self.session_midi_dir() else {
2948 self.completed_audio_recordings.clear();
2949 self.completed_midi_recordings.clear();
2950 return;
2951 };
2952 if std::fs::create_dir_all(&audio_dir).is_err()
2953 || std::fs::create_dir_all(&midi_dir).is_err()
2954 {
2955 self.completed_audio_recordings.clear();
2956 self.completed_midi_recordings.clear();
2957 return;
2958 }
2959 let rate = self
2960 .hw_driver
2961 .as_ref()
2962 .map(|o| o.lock().sample_rate())
2963 .unwrap_or(48_000);
2964 let completed_audio = std::mem::take(&mut self.completed_audio_recordings);
2965 for (track_name, rec) in completed_audio {
2966 self.flush_recording_entry(&audio_dir, rate, track_name, rec)
2967 .await;
2968 }
2969 let completed_midi = std::mem::take(&mut self.completed_midi_recordings);
2970 for (track_name, rec) in completed_midi {
2971 self.flush_midi_recording_entry(&midi_dir, rate as u32, track_name, rec)
2972 .await;
2973 }
2974 }
2975
2976 async fn flush_recordings(&mut self) {
2977 let Some(audio_dir) = self.session_audio_dir() else {
2978 if !self.audio_recordings.is_empty()
2979 || !self.midi_recordings.is_empty()
2980 || !self.completed_audio_recordings.is_empty()
2981 || !self.completed_midi_recordings.is_empty()
2982 {
2983 self.notify_clients(Err("Recording stopped: session path is not set".to_string()))
2984 .await;
2985 }
2986 self.audio_recordings.clear();
2987 self.midi_recordings.clear();
2988 self.completed_audio_recordings.clear();
2989 self.completed_midi_recordings.clear();
2990 return;
2991 };
2992 if std::fs::create_dir_all(&audio_dir).is_err() {
2993 self.notify_clients(Err(format!(
2994 "Recording stopped: failed to create audio directory {}",
2995 audio_dir.display()
2996 )))
2997 .await;
2998 self.audio_recordings.clear();
2999 self.midi_recordings.clear();
3000 self.completed_audio_recordings.clear();
3001 self.completed_midi_recordings.clear();
3002 return;
3003 }
3004 let Some(midi_dir) = self.session_midi_dir() else {
3005 self.audio_recordings.clear();
3006 self.midi_recordings.clear();
3007 self.completed_audio_recordings.clear();
3008 self.completed_midi_recordings.clear();
3009 return;
3010 };
3011 if std::fs::create_dir_all(&midi_dir).is_err() {
3012 self.audio_recordings.clear();
3013 self.midi_recordings.clear();
3014 self.completed_audio_recordings.clear();
3015 self.completed_midi_recordings.clear();
3016 return;
3017 }
3018 let rate = self
3019 .hw_driver
3020 .as_ref()
3021 .map(|o| o.lock().sample_rate())
3022 .unwrap_or(48_000);
3023 let completed_audio = std::mem::take(&mut self.completed_audio_recordings);
3024 for (track_name, rec) in completed_audio {
3025 self.flush_recording_entry(&audio_dir, rate, track_name, rec)
3026 .await;
3027 }
3028 let completed_midi = std::mem::take(&mut self.completed_midi_recordings);
3029 for (track_name, rec) in completed_midi {
3030 self.flush_midi_recording_entry(&midi_dir, rate as u32, track_name, rec)
3031 .await;
3032 }
3033 let recordings = std::mem::take(&mut self.audio_recordings);
3034 for (track_name, rec) in recordings {
3035 self.flush_recording_entry(&audio_dir, rate, track_name, rec)
3036 .await;
3037 }
3038 let midi_recordings = std::mem::take(&mut self.midi_recordings);
3039 for (track_name, rec) in midi_recordings {
3040 self.flush_midi_recording_entry(&midi_dir, rate as u32, track_name, rec)
3041 .await;
3042 }
3043 }
3044
3045 fn compute_peaks_from_stripes(
3046 stripe_peaks: &[Vec<[f32; 2]>],
3047 total_frames: usize,
3048 channels: usize,
3049 ) -> serde_json::Value {
3050 const MAX_PEAK_BINS: usize = 32_768;
3051 if total_frames == 0 || stripe_peaks.is_empty() {
3052 return serde_json::json!({"peaks": []});
3053 }
3054 let target_bins = total_frames.clamp(1024, MAX_PEAK_BINS);
3055 let mut peaks = vec![vec![[0.0_f32, 0.0_f32]; target_bins]; channels];
3056 for (ch, channel_peaks) in peaks.iter_mut().enumerate() {
3057 let mut touched = vec![false; target_bins];
3058 let empty = Vec::new();
3059 let channel_stripes = stripe_peaks.get(ch).unwrap_or(&empty);
3060 for (stripe_idx, stripe) in channel_stripes.iter().enumerate() {
3061 let stripe_start = stripe_idx * RECORDING_STRIPE_FRAMES;
3062 let stripe_end = ((stripe_idx + 1) * RECORDING_STRIPE_FRAMES).min(total_frames);
3063 let start_bin = (stripe_start * target_bins) / total_frames.max(1);
3064 let end_bin = ((stripe_end.saturating_sub(1)) * target_bins / total_frames.max(1))
3065 .min(target_bins - 1);
3066 for bin in start_bin..=end_bin {
3067 if !touched[bin] {
3068 channel_peaks[bin] = *stripe;
3069 touched[bin] = true;
3070 } else {
3071 channel_peaks[bin][0] = channel_peaks[bin][0].min(stripe[0]);
3072 channel_peaks[bin][1] = channel_peaks[bin][1].max(stripe[1]);
3073 }
3074 }
3075 }
3076 }
3077 serde_json::json!({
3078 "peaks": peaks.iter().map(|ch| {
3079 ch.iter().map(|pair| serde_json::json!([pair[0], pair[1]])).collect::<Vec<_>>()
3080 }).collect::<Vec<_>>()
3081 })
3082 }
3083
3084 async fn flush_recording_entry(
3085 &mut self,
3086 audio_dir: &Path,
3087 rate: i32,
3088 track_name: String,
3089 rec: RecordingSession,
3090 ) {
3091 if rec.samples.is_empty() || rec.channels == 0 {
3092 return;
3093 }
3094
3095 let trim_frames = self.hw_output_latency_frames;
3096 let trim_samples = trim_frames * rec.channels;
3097 let samples = if trim_samples > 0 && rec.samples.len() > trim_samples {
3098 &rec.samples[trim_samples..]
3099 } else {
3100 &rec.samples[..]
3101 };
3102 if samples.is_empty() {
3103 return;
3104 }
3105 let file_path = audio_dir.join(&rec.file_name);
3106 let write_result =
3107 crate::audio_codec::write_wav_f32(&file_path, samples, rec.channels, rate as u32);
3108 if let Err(e) = write_result {
3109 tracing::error!("flush_recording_entry: WAV write failed: {}", e);
3110 self.notify_clients(Err(format!(
3111 "Failed to write recording {}: {}",
3112 file_path.display(),
3113 e
3114 )))
3115 .await;
3116 return;
3117 }
3118
3119 let total_frames = rec.current_stripe_frames;
3120 let peaks_json =
3121 Self::compute_peaks_from_stripes(&rec.stripe_peaks, total_frames, rec.channels);
3122 let peaks_file_name = format!("{}.json", rec.file_name);
3123 let peaks_rel = format!("peaks/{}", peaks_file_name);
3124 let peaks_path = self.session_peaks_dir().map(|d| d.join(&peaks_file_name));
3125 if let Some(peaks_dir) = self.session_peaks_dir() {
3126 let _ = std::fs::create_dir_all(&peaks_dir);
3127 }
3128 if let Some(ref path) = peaks_path
3129 && let Err(e) = std::fs::write(
3130 path,
3131 serde_json::to_string_pretty(&peaks_json).unwrap_or_default(),
3132 )
3133 {
3134 tracing::warn!("Failed to write peaks file {}: {}", path.display(), e);
3135 }
3136 let length = samples.len() / rec.channels;
3137 let start_sample = rec.start_sample.saturating_add(trim_frames);
3138 let clip_rel_name = format!("audio/{}", rec.file_name);
3139 let clip = AudioClip::new(
3140 clip_rel_name.clone(),
3141 start_sample,
3142 start_sample.saturating_add(length.max(1)),
3143 );
3144 let (audio_ins, audio_outs) = if let Some(track) = self.state.lock().tracks.get(&track_name)
3145 {
3146 let track = track.lock();
3147 let audio_ins = track.audio.ins.len();
3148 let audio_outs = track.audio.outs.len();
3149 track.audio.clips.push(clip.clone());
3150 (audio_ins, audio_outs)
3151 } else {
3152 tracing::warn!(
3153 "flush_recording_entry: track '{}' not found in engine state",
3154 track_name
3155 );
3156 (0, 0)
3157 };
3158 self.notify_clients(Ok(Action::AddClip {
3159 name: clip_rel_name,
3160 track_name: track_name.clone(),
3161 start: start_sample,
3162 length,
3163 offset: 0,
3164 input_channel: 0,
3165 muted: false,
3166 peaks_file: peaks_path.is_some().then_some(peaks_rel),
3167 kind: Kind::Audio,
3168 fade_enabled: clip.fade_enabled,
3169 fade_in_samples: clip.fade_in_samples,
3170 fade_out_samples: clip.fade_out_samples,
3171 source_name: None,
3172 source_offset: None,
3173 source_length: None,
3174 preview_name: None,
3175 pitch_correction_points: vec![],
3176 pitch_correction_frame_likeness: None,
3177 pitch_correction_inertia_ms: None,
3178 pitch_correction_formant_compensation: None,
3179 plugin_graph_json: Some(Self::default_clip_plugin_graph_json(audio_ins, audio_outs)),
3180 }))
3181 .await;
3182 if let Some(track) = self.state.lock().tracks.get(&track_name).cloned() {
3183 tokio::task::spawn_blocking(move || {
3184 track.lock().preload_clips();
3185 tracing::debug!("Preloaded clips for track '{}' after recording", track_name);
3186 });
3187 }
3188 }
3189
3190 async fn flush_track_recording(&mut self, track_name: &str) {
3191 let Some(audio_dir) = self.session_audio_dir() else {
3192 self.audio_recordings.remove(track_name);
3193 self.midi_recordings.remove(track_name);
3194 self.completed_audio_recordings
3195 .retain(|(name, _)| name != track_name);
3196 self.completed_midi_recordings
3197 .retain(|(name, _)| name != track_name);
3198 return;
3199 };
3200 let Some(midi_dir) = self.session_midi_dir() else {
3201 self.audio_recordings.remove(track_name);
3202 self.midi_recordings.remove(track_name);
3203 self.completed_audio_recordings
3204 .retain(|(name, _)| name != track_name);
3205 self.completed_midi_recordings
3206 .retain(|(name, _)| name != track_name);
3207 return;
3208 };
3209 if std::fs::create_dir_all(&audio_dir).is_err()
3210 || std::fs::create_dir_all(&midi_dir).is_err()
3211 {
3212 return;
3213 }
3214 let rate = self
3215 .hw_driver
3216 .as_ref()
3217 .map(|o| o.lock().sample_rate())
3218 .unwrap_or(48_000);
3219 let mut i = 0;
3220 while i < self.completed_audio_recordings.len() {
3221 if self.completed_audio_recordings[i].0 == track_name {
3222 let (name, rec) = self.completed_audio_recordings.remove(i);
3223 self.flush_recording_entry(&audio_dir, rate, name, rec)
3224 .await;
3225 } else {
3226 i += 1;
3227 }
3228 }
3229 let mut j = 0;
3230 while j < self.completed_midi_recordings.len() {
3231 if self.completed_midi_recordings[j].0 == track_name {
3232 let (name, rec) = self.completed_midi_recordings.remove(j);
3233 self.flush_midi_recording_entry(&midi_dir, rate as u32, name, rec)
3234 .await;
3235 } else {
3236 j += 1;
3237 }
3238 }
3239
3240 let Some(rec) = self.audio_recordings.remove(track_name) else {
3241 if let Some(mrec) = self.midi_recordings.remove(track_name) {
3242 self.flush_midi_recording_entry(
3243 &midi_dir,
3244 rate as u32,
3245 track_name.to_string(),
3246 mrec,
3247 )
3248 .await;
3249 }
3250 return;
3251 };
3252 self.flush_recording_entry(&audio_dir, rate, track_name.to_string(), rec)
3253 .await;
3254 if let Some(mrec) = self.midi_recordings.remove(track_name) {
3255 self.flush_midi_recording_entry(&midi_dir, rate as u32, track_name.to_string(), mrec)
3256 .await;
3257 }
3258 }
3259
3260 async fn flush_midi_recording_entry(
3261 &mut self,
3262 midi_dir: &Path,
3263 sample_rate: u32,
3264 track_name: String,
3265 mut rec: MidiRecordingSession,
3266 ) {
3267 if rec.events.is_empty() {
3268 return;
3269 }
3270 rec.events.sort_by_key(|(sample, _)| *sample);
3271 let clip_rel_name = format!("midi/{}", rec.file_name);
3272 let clip_len_samples = rec
3273 .events
3274 .last()
3275 .map(|(s, _)| s.saturating_sub(rec.start_sample as u64) as usize + 1)
3276 .unwrap_or(1);
3277
3278 for (sample, _) in &mut rec.events {
3279 *sample = sample.saturating_sub(rec.start_sample as u64);
3280 }
3281 let path = midi_dir.join(&rec.file_name);
3282 if let Err(e) = Self::write_midi_file(&path, sample_rate, &rec.events) {
3283 self.notify_clients(Err(format!(
3284 "Failed to write MIDI recording {}: {}",
3285 path.display(),
3286 e
3287 )))
3288 .await;
3289 return;
3290 }
3291 let mut clip = MIDIClip::new(
3292 clip_rel_name.clone(),
3293 rec.start_sample,
3294 rec.start_sample.saturating_add(clip_len_samples.max(1)),
3295 );
3296 clip.offset = 0;
3297 if let Some(track) = self.state.lock().tracks.get(&track_name) {
3298 track.lock().midi.clips.push(clip);
3299 }
3300 self.notify_clients(Ok(Action::AddClip {
3301 name: clip_rel_name,
3302 track_name: track_name.clone(),
3303 start: rec.start_sample,
3304 length: clip_len_samples,
3305 offset: 0,
3306 input_channel: 0,
3307 muted: false,
3308 peaks_file: None,
3309 kind: Kind::MIDI,
3310 fade_enabled: true,
3311 fade_in_samples: 240,
3312 fade_out_samples: 240,
3313 source_name: None,
3314 source_offset: None,
3315 source_length: None,
3316 preview_name: None,
3317 pitch_correction_points: vec![],
3318 pitch_correction_frame_likeness: None,
3319 pitch_correction_inertia_ms: None,
3320 pitch_correction_formant_compensation: None,
3321 plugin_graph_json: None,
3322 }))
3323 .await;
3324 if let Some(track) = self.state.lock().tracks.get(&track_name).cloned() {
3325 tokio::task::spawn_blocking(move || {
3326 track.lock().preload_clips();
3327 tracing::debug!(
3328 "Preloaded clips for track '{}' after MIDI recording",
3329 track_name
3330 );
3331 });
3332 }
3333 }
3334
3335 fn write_midi_file(
3336 path: &Path,
3337 sample_rate: u32,
3338 events: &[(u64, Vec<u8>)],
3339 ) -> Result<(), String> {
3340 let ppq: u16 = 480;
3341 let ticks_per_second: u64 = 960;
3342 let arena = Arena::new();
3343 let mut track_events: Vec<TrackEvent<'_>> = vec![TrackEvent {
3344 delta: u28::new(0),
3345 kind: TrackEventKind::Meta(MetaMessage::Tempo(u24::new(500_000))),
3346 }];
3347 let mut prev_ticks = 0_u64;
3348 for (sample, data) in events {
3349 let ticks = sample.saturating_mul(ticks_per_second) / sample_rate.max(1) as u64;
3350 let delta = ticks.saturating_sub(prev_ticks).min(u32::MAX as u64) as u32;
3351 prev_ticks = ticks;
3352 let Ok(live) = LiveEvent::parse(data) else {
3353 continue;
3354 };
3355 let kind = live.as_track_event(&arena);
3356 track_events.push(TrackEvent {
3357 delta: u28::new(delta),
3358 kind,
3359 });
3360 }
3361 track_events.push(TrackEvent {
3362 delta: u28::new(0),
3363 kind: TrackEventKind::Meta(MetaMessage::EndOfTrack),
3364 });
3365
3366 let smf = Smf {
3367 header: Header::new(Format::SingleTrack, Timing::Metrical(u15::new(ppq))),
3368 tracks: vec![track_events],
3369 };
3370 let mut file = File::create(path).map_err(|e| e.to_string())?;
3371 smf.write_std(&mut file).map_err(|e| e.to_string())
3372 }
3373
3374 pub async fn init(&mut self) {
3375 let max_threads = num_cpus::get();
3376 for id in 0..max_threads {
3377 let (tx, rx) = channel::<Message>(32);
3378 let tx_thread = self.tx.clone();
3379 let handler = tokio::spawn(async move {
3380 let wrk = Worker::new(id, rx, tx_thread, 8);
3381 wrk.await.work().await;
3382 });
3383 self.workers.push(WorkerData::new(tx.clone(), handler));
3384 }
3385 }
3386
3387 async fn notify_clients(&mut self, action: Result<Action, String>) {
3388 self.clients.retain(|client| !client.is_closed());
3389 for client in self.clients.iter() {
3390 if client
3391 .send(Message::Response(action.clone()))
3392 .await
3393 .is_err()
3394 {}
3395 }
3396 }
3397
3398 fn spawn_plugin_host_stderr_reader(&self, stderr: std::process::ChildStderr, source: String) {
3399 let tx = self.tx.clone();
3400 std::thread::spawn(move || {
3401 use std::io::{BufRead, BufReader};
3402 let reader = BufReader::new(stderr);
3403 for line in reader.lines() {
3404 if let Ok(line) = line
3405 && !line.is_empty()
3406 {
3407 let _ = tx.blocking_send(Message::Request(Action::Log {
3408 source: source.clone(),
3409 message: line,
3410 }));
3411 }
3412 }
3413 });
3414 }
3415
3416 fn set_osc_enabled_with<F>(&mut self, enabled: bool, start_server: F) -> Result<(), String>
3417 where
3418 F: FnOnce(Sender<Message>) -> Result<OscServer, String>,
3419 {
3420 if enabled {
3421 if self.osc_server.is_none() {
3422 self.osc_server = Some(start_server(self.tx.clone())?);
3423 }
3424 } else if let Some(mut server) = self.osc_server.take() {
3425 server.stop();
3426 }
3427 Ok(())
3428 }
3429
3430 fn track_handle_by_name(&self, track_name: &str) -> Option<Arc<UnsafeMutex<Box<Track>>>> {
3431 self.state.lock().tracks.get(track_name).cloned()
3432 }
3433
3434 fn track_handle_or_err(
3435 &self,
3436 track_name: &str,
3437 ) -> Result<Arc<UnsafeMutex<Box<Track>>>, String> {
3438 self.track_handle_by_name(track_name)
3439 .ok_or_else(|| format!("Track not found: {track_name}"))
3440 }
3441
3442 fn add_clip_to_track(&self, request: ClipAddRequest<'_>) {
3443 if let Some(track) = self.state.lock().tracks.get(request.track_name) {
3444 let track = track.lock();
3445 if track.is_master || track.is_folder {
3446 return;
3447 }
3448 match request.kind {
3449 Kind::Audio => {
3450 let mut clip = AudioClip::new(
3451 request.name.to_string(),
3452 request.start,
3453 request.start.saturating_add(request.length.max(1)),
3454 );
3455 clip.offset = request.offset;
3456 let max_lane = track.audio.ins.len().saturating_sub(1);
3457 clip.input_channel = request.input_channel.min(max_lane);
3458 clip.muted = request.muted;
3459 clip.peaks_file = request.peaks_file;
3460 clip.fade_enabled = request.fade_enabled;
3461 clip.fade_in_samples = request.fade_in_samples;
3462 clip.fade_out_samples = request.fade_out_samples;
3463 clip.pitch_correction_preview_name = request.preview_name;
3464 clip.pitch_correction_source_name = request.source_name;
3465 clip.pitch_correction_source_offset = request.source_offset;
3466 clip.pitch_correction_source_length = request.source_length;
3467 clip.pitch_correction_points = request.pitch_correction_points;
3468 clip.pitch_correction_frame_likeness = request.pitch_correction_frame_likeness;
3469 clip.pitch_correction_inertia_ms = request.pitch_correction_inertia_ms;
3470 clip.pitch_correction_formant_compensation =
3471 request.pitch_correction_formant_compensation;
3472 clip.plugin_graph_json = request.plugin_graph_json;
3473 track.audio.clips.push(clip);
3474 #[cfg(unix)]
3475 track.clip_pitch_shifters.clear();
3476 }
3477 Kind::MIDI => {
3478 let mut clip = MIDIClip::new(
3479 request.name.to_string(),
3480 request.start,
3481 request.start.saturating_add(request.length.max(1)),
3482 );
3483 clip.offset = request.offset;
3484 let max_lane = track.midi.ins.len().saturating_sub(1);
3485 clip.input_channel = request.input_channel.min(max_lane);
3486 clip.muted = request.muted;
3487 track.midi.clips.push(clip);
3488 }
3489 }
3490 }
3491 }
3492
3493 fn audio_clip_from_data(data: &crate::message::AudioClipData) -> AudioClip {
3494 let mut clip = AudioClip::new(
3495 data.name.clone(),
3496 data.start,
3497 data.start.saturating_add(data.length.max(1)),
3498 );
3499 clip.offset = data.offset;
3500 clip.input_channel = data.input_channel;
3501 clip.muted = data.muted;
3502 clip.peaks_file = data.peaks_file.clone();
3503 clip.fade_enabled = data.fade_enabled;
3504 clip.fade_in_samples = data.fade_in_samples;
3505 clip.fade_out_samples = data.fade_out_samples;
3506 clip.pitch_correction_preview_name = data.preview_name.clone();
3507 clip.pitch_correction_source_name = data.source_name.clone();
3508 clip.pitch_correction_source_offset = data.source_offset;
3509 clip.pitch_correction_source_length = data.source_length;
3510 clip.pitch_correction_points = data.pitch_correction_points.clone();
3511 clip.pitch_correction_frame_likeness = data.pitch_correction_frame_likeness;
3512 clip.pitch_correction_inertia_ms = data.pitch_correction_inertia_ms;
3513 clip.pitch_correction_formant_compensation = data.pitch_correction_formant_compensation;
3514 clip.plugin_graph_json = data.plugin_graph_json.clone();
3515 clip.grouped_clips = data
3516 .grouped_clips
3517 .iter()
3518 .map(Self::audio_clip_from_data)
3519 .collect();
3520 for child in &mut clip.grouped_clips {
3521 child.fade_enabled = false;
3522 child.fade_in_samples = 0;
3523 child.fade_out_samples = 0;
3524 }
3525 clip
3526 }
3527
3528 fn midi_clip_from_data(data: &crate::message::MidiClipData) -> MIDIClip {
3529 let mut clip = MIDIClip::new(
3530 data.name.clone(),
3531 data.start,
3532 data.start.saturating_add(data.length.max(1)),
3533 );
3534 clip.offset = data.offset;
3535 clip.input_channel = data.input_channel;
3536 clip.muted = data.muted;
3537 clip.grouped_clips = data
3538 .grouped_clips
3539 .iter()
3540 .map(Self::midi_clip_from_data)
3541 .collect();
3542 clip
3543 }
3544
3545 fn add_grouped_clip_to_track(
3546 &self,
3547 track_name: &str,
3548 kind: Kind,
3549 audio_clip: Option<crate::message::AudioClipData>,
3550 midi_clip: Option<crate::message::MidiClipData>,
3551 ) {
3552 if let Some(track) = self.state.lock().tracks.get(track_name) {
3553 let track = track.lock();
3554 if track.is_master {
3555 return;
3556 }
3557 match kind {
3558 Kind::Audio => {
3559 if let Some(mut clip) = audio_clip.map(|clip| Self::audio_clip_from_data(&clip))
3560 {
3561 let max_lane = track.audio.ins.len().saturating_sub(1);
3562 clip.input_channel = clip.input_channel.min(max_lane);
3563 track.audio.clips.push(clip);
3564 #[cfg(unix)]
3565 track.clip_pitch_shifters.clear();
3566 }
3567 }
3568 Kind::MIDI => {
3569 if let Some(mut clip) = midi_clip.map(|clip| Self::midi_clip_from_data(&clip)) {
3570 let max_lane = track.midi.ins.len().saturating_sub(1);
3571 clip.input_channel = clip.input_channel.min(max_lane);
3572 track.midi.clips.push(clip);
3573 }
3574 }
3575 }
3576 }
3577 }
3578
3579 fn remove_clips_from_track(&self, track_name: &str, kind: Kind, clip_indices: &[usize]) {
3580 if let Some(track) = self.state.lock().tracks.get(track_name) {
3581 let track = track.lock();
3582 let mut indices = clip_indices.to_vec();
3583 indices.sort_unstable();
3584 indices.dedup();
3585 match kind {
3586 Kind::Audio => {
3587 for idx in indices.into_iter().rev() {
3588 if idx < track.audio.clips.len() {
3589 track.audio.clips.remove(idx);
3590 }
3591 }
3592 #[cfg(unix)]
3593 track.clip_pitch_shifters.clear();
3594 }
3595 Kind::MIDI => {
3596 for idx in indices.into_iter().rev() {
3597 if idx < track.midi.clips.len() {
3598 track.midi.clips.remove(idx);
3599 }
3600 }
3601 }
3602 }
3603 }
3604 }
3605
3606 fn rename_clip_references(
3607 &self,
3608 track_name: &str,
3609 kind: Kind,
3610 clip_index: usize,
3611 new_name: &str,
3612 ) {
3613 let Some(track) = self.state.lock().tracks.get(track_name) else {
3614 return;
3615 };
3616 let track = track.lock();
3617 let old_name = match kind {
3618 Kind::Audio => {
3619 if clip_index >= track.audio.clips.len() {
3620 return;
3621 }
3622 track.audio.clips[clip_index].name.clone()
3623 }
3624 Kind::MIDI => {
3625 if clip_index >= track.midi.clips.len() {
3626 return;
3627 }
3628 track.midi.clips[clip_index].name.clone()
3629 }
3630 };
3631
3632 let new_file_name = match kind {
3633 Kind::Audio => format!("audio/{}.wav", new_name),
3634 Kind::MIDI => {
3635 let ext = std::path::Path::new(&old_name)
3636 .extension()
3637 .and_then(|e| e.to_str())
3638 .map(|s| s.to_ascii_lowercase())
3639 .filter(|e| e == "mid" || e == "midi")
3640 .unwrap_or_else(|| "mid".to_string());
3641 format!("midi/{}.{}", new_name, ext)
3642 }
3643 };
3644 let _ = track;
3645
3646 for (_, other_track) in self.state.lock().tracks.iter() {
3647 let other_track = other_track.lock();
3648 match kind {
3649 Kind::Audio => {
3650 for clip in &mut other_track.audio.clips {
3651 if clip.name == old_name {
3652 clip.name = new_file_name.clone();
3653 }
3654 if clip.pitch_correction_source_name.as_deref() == Some(old_name.as_str()) {
3655 clip.pitch_correction_source_name = Some(new_file_name.clone());
3656 }
3657 }
3658 }
3659 Kind::MIDI => {
3660 for clip in &mut other_track.midi.clips {
3661 if clip.name == old_name {
3662 clip.name = new_file_name.clone();
3663 }
3664 }
3665 }
3666 }
3667 }
3668 }
3669
3670 fn set_clip_fade(
3671 &self,
3672 track_name: &str,
3673 clip_index: usize,
3674 kind: Kind,
3675 fade_enabled: bool,
3676 fade_in_samples: usize,
3677 fade_out_samples: usize,
3678 ) {
3679 let Some(track) = self.state.lock().tracks.get(track_name) else {
3680 return;
3681 };
3682 let track = track.lock();
3683 match kind {
3684 Kind::Audio => {
3685 if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3686 clip.fade_enabled = fade_enabled;
3687 clip.fade_in_samples = fade_in_samples;
3688 clip.fade_out_samples = fade_out_samples;
3689 }
3690 }
3691 Kind::MIDI => {}
3692 }
3693 }
3694
3695 fn set_clip_bounds(
3696 &self,
3697 track_name: &str,
3698 clip_index: usize,
3699 kind: Kind,
3700 start: usize,
3701 length: usize,
3702 offset: usize,
3703 ) {
3704 let Some(track) = self.state.lock().tracks.get(track_name) else {
3705 return;
3706 };
3707 let track = track.lock();
3708 match kind {
3709 Kind::Audio => {
3710 if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3711 clip.start = start;
3712 clip.end = start.saturating_add(length.max(1));
3713 clip.offset = offset;
3714 clip.pitch_correction_preview_name = None;
3715 clip.pitch_correction_source_name = None;
3716 clip.pitch_correction_source_offset = None;
3717 clip.pitch_correction_source_length = None;
3718 clip.pitch_correction_points.clear();
3719 clip.pitch_correction_frame_likeness = None;
3720 clip.pitch_correction_inertia_ms = None;
3721 clip.pitch_correction_formant_compensation = None;
3722 }
3723 #[cfg(unix)]
3724 track.clip_pitch_shifters.clear();
3725 }
3726 Kind::MIDI => {
3727 if let Some(clip) = track.midi.clips.get_mut(clip_index) {
3728 clip.start = start;
3729 clip.end = start.saturating_add(length.max(1));
3730 clip.offset = offset;
3731 }
3732 }
3733 }
3734 }
3735
3736 fn set_clip_source_name(&self, track_name: &str, clip_index: usize, kind: Kind, name: String) {
3737 let Some(track) = self.state.lock().tracks.get(track_name) else {
3738 return;
3739 };
3740 let track = track.lock();
3741 match kind {
3742 Kind::Audio => {
3743 if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3744 clip.name = name;
3745 }
3746 #[cfg(unix)]
3747 track.clip_pitch_shifters.clear();
3748 }
3749 Kind::MIDI => {
3750 if let Some(clip) = track.midi.clips.get_mut(clip_index) {
3751 clip.name = name;
3752 }
3753 }
3754 }
3755 }
3756
3757 fn set_clip_muted(&self, track_name: &str, clip_index: usize, kind: Kind, muted: bool) {
3758 let Some(track) = self.state.lock().tracks.get(track_name) else {
3759 return;
3760 };
3761 let track = track.lock();
3762 match kind {
3763 Kind::Audio => {
3764 if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3765 clip.muted = muted;
3766 }
3767 }
3768 Kind::MIDI => {
3769 if let Some(clip) = track.midi.clips.get_mut(clip_index) {
3770 clip.muted = muted;
3771 }
3772 }
3773 }
3774 }
3775
3776 #[allow(clippy::too_many_arguments)]
3777 fn set_clip_pitch_correction(
3778 &self,
3779 track_name: &str,
3780 clip_index: usize,
3781 preview_name: Option<String>,
3782 source_name: Option<String>,
3783 source_offset: Option<usize>,
3784 source_length: Option<usize>,
3785 pitch_correction_points: Vec<crate::message::PitchCorrectionPointData>,
3786 pitch_correction_frame_likeness: Option<f32>,
3787 pitch_correction_inertia_ms: Option<u16>,
3788 pitch_correction_formant_compensation: Option<bool>,
3789 ) {
3790 if let Some(track) = self.state.lock().tracks.get(track_name) {
3791 let track = track.lock();
3792 if let Some(clip) = track.audio.clips.get_mut(clip_index) {
3793 clip.pitch_correction_preview_name = preview_name;
3794 clip.pitch_correction_source_name = source_name;
3795 clip.pitch_correction_source_offset = source_offset;
3796 clip.pitch_correction_source_length = source_length;
3797 clip.pitch_correction_points = pitch_correction_points;
3798 clip.pitch_correction_frame_likeness = pitch_correction_frame_likeness;
3799 clip.pitch_correction_inertia_ms = pitch_correction_inertia_ms;
3800 clip.pitch_correction_formant_compensation = pitch_correction_formant_compensation;
3801 }
3802 #[cfg(unix)]
3803 track.clip_pitch_shifters.clear();
3804 }
3805 }
3806
3807 async fn request_hw_cycle(&mut self) {
3808 if self.awaiting_hwfinished {
3809 tracing::debug!("request_hw_cycle skipped (already awaiting)");
3810 return;
3811 }
3812 tracing::debug!("request_hw_cycle sending TracksFinished");
3813 self.apply_hw_out_gain_and_meter().await;
3814 if let Some((after_frames, loop_start, cycle_end_sample)) =
3815 self.scheduled_loop_wrap_for_next_cycle()
3816 {
3817 self.notified_loop_wrap_sample = Some(cycle_end_sample);
3818 self.notify_clients(Ok(Action::TransportPositionAt {
3819 sample: loop_start,
3820 after_frames,
3821 }))
3822 .await;
3823 } else {
3824 self.notified_loop_wrap_sample = None;
3825 }
3826 if let Some(worker) = &self.hw_worker {
3827 if !self.pending_hw_midi_out_events_by_device.is_empty() {
3828 let out_events = std::mem::take(&mut self.pending_hw_midi_out_events_by_device);
3829 if let Err(e) = worker.tx.send(Message::HWMidiOutEvents(out_events)).await {
3830 error!("Error sending HWMidiOutEvents {e}");
3831 }
3832 }
3833 match worker.tx.send(Message::TracksFinished).await {
3834 Ok(_) => {
3835 self.awaiting_hwfinished = true;
3836 }
3837 Err(e) => {
3838 error!("Error sending TracksFinished {e}");
3839 }
3840 }
3841 }
3842 }
3843
3844 async fn clear_hw_midi_output_state(&mut self, send_panic: bool) {
3845 self.pending_hw_midi_out_events.clear();
3846 self.pending_hw_midi_out_events_by_device.clear();
3847 {
3848 let state = self.state.lock();
3849 for track in state.tracks.values() {
3850 track.lock().take_hw_midi_out_events();
3851 }
3852 }
3853
3854 let panic_events = if send_panic {
3855 self.note_off_events_for_all_active_tracks()
3856 } else {
3857 vec![]
3858 };
3859
3860 if let Some(worker) = &self.hw_worker {
3861 if let Err(e) = worker.tx.send(Message::ClearHWMidiOutEvents).await {
3862 error!("Error clearing pending HWMidiOutEvents {e}");
3863 }
3864 if !panic_events.is_empty()
3865 && let Err(e) = worker.tx.send(Message::HWMidiOutEvents(panic_events)).await
3866 {
3867 error!("Error sending transport restart MIDI panic events {e}");
3868 }
3869 } else if !panic_events.is_empty() {
3870 self.pending_hw_midi_out_events_by_device
3871 .extend(panic_events);
3872 }
3873 }
3874
3875 fn invalidate_track_cycle_state(&mut self) {
3876 self.track_process_epoch = self.track_process_epoch.saturating_add(1);
3877 self.task_processing_started_at.clear();
3878 self.cycle_tasks.clear();
3879 self.cycle_task_deps.clear();
3880 self.cycle_tasks_running.clear();
3881 self.cycle_tasks_finished.clear();
3882 let state = self.state.lock();
3883 for track in state.tracks.values() {
3884 let t = track.lock();
3885 t.audio.finished = false;
3886 t.audio.processing = false;
3887 }
3888 }
3889
3890 fn force_stalled_task_completions(&mut self) {
3891 let now = Instant::now();
3892 let running: Vec<ProcessTask> = self.cycle_tasks_running.clone();
3893 for task in running {
3894 let key = Self::task_key(&task);
3895 let Some(started) = self.task_processing_started_at.get(&key).copied() else {
3896 continue;
3897 };
3898 if now.duration_since(started) < Self::TRACK_PROCESS_TIMEOUT {
3899 continue;
3900 }
3901 if Self::task_running_finished_contains(&self.cycle_tasks_finished, &task) {
3902 self.task_processing_started_at.remove(&key);
3903 continue;
3904 }
3905 let track = match &task {
3906 ProcessTask::Track(t)
3907 | ProcessTask::FolderInput(t)
3908 | ProcessTask::FolderOutput(t) => t.clone(),
3909 ProcessTask::Plugin { track, .. } => track.clone(),
3910 };
3911 {
3912 let t = track.lock();
3913 if t.audio.finished || !t.audio.processing {
3914 self.task_processing_started_at.remove(&key);
3915 continue;
3916 }
3917 for out in &t.audio.outs {
3918 out.buffer.lock().fill(0.0);
3919 *out.finished.lock() = true;
3920 }
3921 t.audio.processing = false;
3922 t.audio.finished = true;
3923 }
3924 self.cycle_tasks_running
3925 .retain(|t| Self::task_key(t) != key);
3926 self.cycle_tasks_finished.push(task.clone());
3927 self.task_processing_started_at.remove(&key);
3928 tracing::warn!(
3929 "Task '{}' exceeded process timeout ({} ms); forcing silent completion for cycle",
3930 Self::task_track_name(&task),
3931 Self::TRACK_PROCESS_TIMEOUT.as_millis()
3932 );
3933 }
3934 }
3935
3936 fn should_publish_hw_out_meters(&mut self) -> bool {
3937 let now = Instant::now();
3938 match self.last_hw_out_meter_publish {
3939 Some(last) if now.duration_since(last) < Self::METER_PUBLISH_INTERVAL => false,
3940 _ => {
3941 self.last_hw_out_meter_publish = Some(now);
3942 true
3943 }
3944 }
3945 }
3946
3947 fn should_publish_track_meters(&mut self) -> bool {
3948 let now = Instant::now();
3949 match self.last_track_meter_publish {
3950 Some(last) if now.duration_since(last) < Self::METER_PUBLISH_INTERVAL => false,
3951 _ => {
3952 self.last_track_meter_publish = Some(now);
3953 true
3954 }
3955 }
3956 }
3957
3958 fn should_publish_hw_out_linear(&mut self, peaks_linear: &[f32]) -> bool {
3959 #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
3960 {
3961 self.hw_out_meter_publish_phase = !self.hw_out_meter_publish_phase;
3962 if !self.hw_out_meter_publish_phase {
3963 return false;
3964 }
3965 let changed = if self.last_hw_out_meter_linear.len() != peaks_linear.len() {
3966 true
3967 } else {
3968 self.last_hw_out_meter_linear
3969 .iter()
3970 .zip(peaks_linear.iter())
3971 .any(|(prev, next)| (prev - next).abs() >= Self::HW_OUT_METER_LINEAR_EPSILON)
3972 };
3973 if !changed {
3974 return false;
3975 }
3976 self.last_hw_out_meter_linear.clear();
3977 self.last_hw_out_meter_linear
3978 .extend_from_slice(peaks_linear);
3979 true
3980 }
3981 #[cfg(not(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd")))]
3982 {
3983 let _ = peaks_linear;
3984 false
3985 }
3986 }
3987
3988 async fn maybe_notify_hw_out_meter(&mut self, _meter_db: Vec<f32>) {
3989 {}
3990 }
3991
3992 fn collect_changed_track_meters(
3993 &mut self,
3994 _tracks: &[(String, Arc<UnsafeMutex<Box<Track>>>)],
3995 ) -> Vec<(String, Vec<f32>)> {
3996 Vec::new()
3997 }
3998
3999 async fn apply_hw_out_gain_and_meter(&mut self) {
4000 let gain = if self.hw_out_muted {
4001 0.0
4002 } else {
4003 10.0_f32.powf(self.hw_out_level_db / 20.0)
4004 };
4005 let should_notify_interval = self.should_publish_hw_out_meters();
4006 if let Some(oss) = self.hw_driver.clone() {
4007 let hw = oss.lock();
4008 hw.set_output_gain_balance(gain, self.hw_out_balance);
4009 if !should_notify_interval {
4010 return;
4011 }
4012 } else {
4013 #[cfg(unix)]
4014 {
4015 if let Some(jack) = self.jack_runtime.clone() {
4016 jack.lock().set_output_gain_linear(gain);
4017 jack.lock().set_output_balance(self.hw_out_balance);
4018 if !should_notify_interval {
4019 return;
4020 }
4021 } else {
4022 return;
4023 }
4024 }
4025 #[cfg(not(unix))]
4026 {
4027 return;
4028 }
4029 }
4030 let peaks_linear = if let Some(oss) = self.hw_driver.clone() {
4031 oss.lock().output_meter_linear(gain, self.hw_out_balance)
4032 } else {
4033 #[cfg(unix)]
4034 {
4035 if let Some(jack) = self.jack_runtime.clone() {
4036 let outs = jack.lock().audio_outs();
4037 let out_count = outs.len();
4038 let b = if out_count == 2 {
4039 self.hw_out_balance.clamp(-1.0, 1.0)
4040 } else {
4041 0.0
4042 };
4043 let mut meters_linear = Vec::with_capacity(out_count);
4044 for (channel_idx, channel) in outs.iter().enumerate() {
4045 let balance_gain = if out_count == 2 {
4046 if channel_idx == 0 {
4047 (1.0 - b).clamp(0.0, 1.0)
4048 } else {
4049 (1.0 + b).clamp(0.0, 1.0)
4050 }
4051 } else {
4052 1.0
4053 };
4054 let buf = channel.buffer.lock();
4055 let peak = crate::simd::peak_abs(buf) * gain * balance_gain;
4056 meters_linear.push(peak);
4057 }
4058 meters_linear
4059 } else {
4060 return;
4061 }
4062 }
4063 #[cfg(not(unix))]
4064 {
4065 return;
4066 }
4067 };
4068 if self.hw_out_peak_hold_linear.len() != peaks_linear.len() {
4069 self.hw_out_peak_hold_linear.resize(peaks_linear.len(), 0.0);
4070 }
4071 let mut held_peaks = Vec::with_capacity(peaks_linear.len());
4072 for (idx, peak_now) in peaks_linear.iter().copied().enumerate() {
4073 let held = self.hw_out_peak_hold_linear[idx] * 0.92;
4074 let next = peak_now.max(held);
4075 self.hw_out_peak_hold_linear[idx] = next;
4076 held_peaks.push(next);
4077 }
4078 let should_notify =
4079 should_notify_interval && self.should_publish_hw_out_linear(&held_peaks);
4080 let meter_db: Vec<f32> = held_peaks
4081 .into_iter()
4082 .map(Self::meter_linear_to_db)
4083 .collect();
4084 self.latest_hw_out_meter_db = Arc::new(meter_db.clone());
4085 if should_notify {
4086 self.maybe_notify_hw_out_meter(meter_db).await;
4087 }
4088 }
4089
4090 fn preload_track_clips_spawn(&self) {
4091 let tracks: Vec<_> = self.state.lock().tracks.values().cloned().collect();
4092 for track in tracks {
4093 tokio::task::spawn_blocking(move || {
4094 track.lock().preload_clips();
4095 });
4096 }
4097 }
4098
4099 async fn preload_track_clips(&self) {
4100 let tracks: Vec<_> = self.state.lock().tracks.values().cloned().collect();
4101 if tracks.is_empty() {
4102 return;
4103 }
4104 let mut handles = Vec::with_capacity(tracks.len());
4105 for track in tracks {
4106 handles.push(tokio::task::spawn_blocking(move || {
4107 track.lock().preload_clips();
4108 }));
4109 }
4110 for handle in handles {
4111 if let Err(e) = handle.await {
4112 tracing::warn!("Clip preload task panicked: {e}");
4113 }
4114 }
4115 }
4116
4117 fn build_task_graph(
4118 &self,
4119 ) -> (
4120 Vec<ProcessTask>,
4121 std::collections::HashMap<String, Vec<String>>,
4122 ) {
4123 let state = self.state.lock();
4124 let ordered: Vec<(String, Arc<UnsafeMutex<Box<Track>>>)> = state
4125 .tracks
4126 .iter()
4127 .map(|(name, track)| (name.clone(), track.clone()))
4128 .collect();
4129 let mut tasks = Vec::new();
4130 let mut deps = std::collections::HashMap::new();
4131
4132 for (_name, track) in &ordered {
4133 let t = track.lock();
4134 if t.parent_track.is_some() {
4135 continue;
4136 }
4137 self.append_track_tasks(track.clone(), None, &mut tasks, &mut deps);
4138 }
4139
4140 (tasks, deps)
4141 }
4142
4143 fn append_track_tasks(
4144 &self,
4145 track: Arc<UnsafeMutex<Box<Track>>>,
4146 predecessor: Option<String>,
4147 tasks: &mut Vec<ProcessTask>,
4148 deps: &mut std::collections::HashMap<String, Vec<String>>,
4149 ) -> (String, String) {
4150 use crate::message::ConnectableRef;
4151 let t = track.lock();
4152 if t.is_folder {
4153 let folder_input = ProcessTask::FolderInput(track.clone());
4154 let folder_input_key = Self::task_key(&folder_input);
4155 tasks.push(folder_input.clone());
4156 let folder_input_deps: Vec<_> = predecessor.into_iter().collect();
4157 deps.insert(folder_input_key.clone(), folder_input_deps);
4158
4159 let mut source_keys: std::collections::HashMap<ConnectableRef, String> =
4160 std::collections::HashMap::new();
4161 let mut target_keys: std::collections::HashMap<ConnectableRef, String> =
4162 std::collections::HashMap::new();
4163 source_keys.insert(ConnectableRef::TrackInput, folder_input_key.clone());
4164 target_keys.insert(ConnectableRef::TrackInput, folder_input_key.clone());
4165
4166 let mut plugin_keys: Vec<String> = Vec::new();
4167 for idx in 0..t.clap_plugins.len() {
4168 let plugin_task = ProcessTask::Plugin {
4169 track: track.clone(),
4170 kind: PluginKind::Clap,
4171 index: idx,
4172 };
4173 let plugin_key = Self::task_key(&plugin_task);
4174 let id = t.clap_plugins[idx].id;
4175 source_keys.insert(ConnectableRef::ClapPlugin(id), plugin_key.clone());
4176 target_keys.insert(ConnectableRef::ClapPlugin(id), plugin_key.clone());
4177 tasks.push(plugin_task);
4178 deps.insert(plugin_key.clone(), vec![folder_input_key.clone()]);
4179 plugin_keys.push(plugin_key);
4180 }
4181 for idx in 0..t.vst3_plugins.len() {
4182 let plugin_task = ProcessTask::Plugin {
4183 track: track.clone(),
4184 kind: PluginKind::Vst3,
4185 index: idx,
4186 };
4187 let plugin_key = Self::task_key(&plugin_task);
4188 let id = t.vst3_plugins[idx].id;
4189 source_keys.insert(ConnectableRef::Vst3Plugin(id), plugin_key.clone());
4190 target_keys.insert(ConnectableRef::Vst3Plugin(id), plugin_key.clone());
4191 tasks.push(plugin_task);
4192 deps.insert(plugin_key.clone(), vec![folder_input_key.clone()]);
4193 plugin_keys.push(plugin_key);
4194 }
4195 #[cfg(all(unix, not(target_os = "macos")))]
4196 for idx in 0..t.lv2_plugins.len() {
4197 let plugin_task = ProcessTask::Plugin {
4198 track: track.clone(),
4199 kind: PluginKind::Lv2,
4200 index: idx,
4201 };
4202 let plugin_key = Self::task_key(&plugin_task);
4203 let id = t.lv2_plugins[idx].id;
4204 source_keys.insert(ConnectableRef::Lv2Plugin(id), plugin_key.clone());
4205 target_keys.insert(ConnectableRef::Lv2Plugin(id), plugin_key.clone());
4206 tasks.push(plugin_task);
4207 deps.insert(plugin_key.clone(), vec![folder_input_key.clone()]);
4208 plugin_keys.push(plugin_key);
4209 }
4210
4211 let mut child_keys = Vec::new();
4212 for child_track in &t.child_tracks {
4213 let (child_first, child_last) = self.append_track_tasks(
4214 child_track.clone(),
4215 Some(folder_input_key.clone()),
4216 tasks,
4217 deps,
4218 );
4219 let child_name = child_track.lock().name.clone();
4220 source_keys.insert(
4221 ConnectableRef::ChildTrack(child_name.clone()),
4222 child_last.clone(),
4223 );
4224 target_keys.insert(ConnectableRef::ChildTrack(child_name), child_first.clone());
4225 child_keys.push((child_first, child_last.clone()));
4226 }
4227
4228 let folder_output = ProcessTask::FolderOutput(track.clone());
4229 let folder_output_key = Self::task_key(&folder_output);
4230 source_keys.insert(ConnectableRef::TrackOutput, folder_output_key.clone());
4231 target_keys.insert(ConnectableRef::TrackOutput, folder_output_key.clone());
4232 tasks.push(folder_output.clone());
4233 let mut folder_output_deps = vec![folder_input_key.clone()];
4234 folder_output_deps.extend(plugin_keys);
4235 folder_output_deps.extend(child_keys.iter().map(|(_, last)| last.clone()));
4236 deps.insert(folder_output_key.clone(), folder_output_deps);
4237
4238 for conn in t.connectable_connections() {
4241 let Some(source_key) = source_keys.get(&conn.from) else {
4242 continue;
4243 };
4244 let Some(target_key) = target_keys.get(&conn.to) else {
4245 continue;
4246 };
4247 if source_key == target_key {
4248 continue;
4249 }
4250 let entry = deps.entry(target_key.clone()).or_default();
4251 if !entry.contains(source_key) {
4252 entry.push(source_key.clone());
4253 }
4254 }
4255
4256 (folder_input_key, folder_output_key)
4257 } else {
4258 let task = ProcessTask::Track(track.clone());
4259 let task_key = Self::task_key(&task);
4260 tasks.push(task.clone());
4261 deps.insert(
4262 task_key.clone(),
4263 predecessor.into_iter().collect::<Vec<_>>(),
4264 );
4265 (task_key.clone(), task_key)
4266 }
4267 }
4268
4269 fn task_track_name(task: &ProcessTask) -> String {
4270 match task {
4271 ProcessTask::Track(t) | ProcessTask::FolderInput(t) | ProcessTask::FolderOutput(t) => {
4272 t.lock().name.clone()
4273 }
4274 ProcessTask::Plugin { track, .. } => track.lock().name.clone(),
4275 }
4276 }
4277
4278 fn task_key(task: &ProcessTask) -> String {
4279 match task {
4280 ProcessTask::Track(t) => format!("Track:{:p}", std::sync::Arc::as_ptr(t)),
4281 ProcessTask::FolderInput(t) => {
4282 format!("FolderInput:{:p}", std::sync::Arc::as_ptr(t))
4283 }
4284 ProcessTask::FolderOutput(t) => {
4285 format!("FolderOutput:{:p}", std::sync::Arc::as_ptr(t))
4286 }
4287 ProcessTask::Plugin { track, kind, index } => format!(
4288 "Plugin:{:?}:{:p}:{}",
4289 kind,
4290 std::sync::Arc::as_ptr(track),
4291 index
4292 ),
4293 }
4294 }
4295
4296 fn task_running_finished_contains(haystack: &[ProcessTask], needle: &ProcessTask) -> bool {
4297 let needle_key = Self::task_key(needle);
4298 haystack.iter().any(|t| Self::task_key(t) == needle_key)
4299 }
4300
4301 fn task_ready(&self, task: &ProcessTask) -> bool {
4302 match task {
4303 ProcessTask::Track(t) | ProcessTask::FolderInput(t) => {
4304 let track = t.lock();
4305 track.audio.ready()
4306 }
4307 ProcessTask::Plugin { .. } | ProcessTask::FolderOutput(_) => true,
4308 }
4309 }
4310
4311 fn task_dependencies_satisfied(&self, task: &ProcessTask) -> bool {
4312 let key = Self::task_key(task);
4313 let Some(deps) = self.cycle_task_deps.get(&key) else {
4314 return true;
4315 };
4316 let finished_keys: std::collections::HashSet<String> = self
4317 .cycle_tasks_finished
4318 .iter()
4319 .map(Self::task_key)
4320 .collect();
4321 deps.iter().all(|d| finished_keys.contains(d))
4322 }
4323
4324 fn prepare_task_track(&self, task: &ProcessTask) {
4325 let track = match task {
4326 ProcessTask::Track(t) | ProcessTask::FolderInput(t) | ProcessTask::FolderOutput(t) => t,
4327 ProcessTask::Plugin { track, .. } => track,
4328 };
4329 let t = track.lock();
4330 t.set_transport_sample(self.transport_sample);
4331 t.set_loop_config(self.loop_enabled, self.loop_range_samples);
4332 t.set_transport_timing(self.tempo_bpm, self.tsig_num, self.tsig_denom);
4333 t.process_epoch = self.track_process_epoch;
4334 t.set_clip_playback_enabled(self.clip_playback_enabled && self.playing);
4335 t.set_record_tap_enabled(self.playing && self.record_enabled);
4336 t.audio.processing = true;
4337 }
4338
4339 async fn send_tasks(&mut self) -> bool {
4340 if !self.playing {
4341 return false;
4342 }
4343 self.refresh_realtime_infection();
4344 self.force_stalled_task_completions();
4345
4346 if self.cycle_tasks.is_empty() {
4347 let (tasks, deps) = self.build_task_graph();
4348 let task_names: Vec<String> = tasks.iter().map(Self::task_track_name).collect();
4349 tracing::debug!(
4350 "send_tasks rebuilt graph: {} tasks ({:?})",
4351 tasks.len(),
4352 task_names
4353 );
4354 self.cycle_tasks = tasks;
4355 self.cycle_task_deps = deps;
4356 self.cycle_tasks_running.clear();
4357 self.cycle_tasks_finished.clear();
4358 }
4359
4360 let mut finished = true;
4361 let mut dispatched = 0;
4362 loop {
4363 let next_task = {
4364 let mut next = None;
4365 tracing::debug!(
4366 "selecting next: cycle={} running={} finished={}",
4367 self.cycle_tasks.len(),
4368 self.cycle_tasks_running.len(),
4369 self.cycle_tasks_finished.len()
4370 );
4371 for task in &self.cycle_tasks {
4372 let in_running =
4373 Self::task_running_finished_contains(&self.cycle_tasks_running, task);
4374 let in_finished =
4375 Self::task_running_finished_contains(&self.cycle_tasks_finished, task);
4376 tracing::debug!(
4377 "checking task {} in_running={} in_finished={}",
4378 Self::task_track_name(task),
4379 in_running,
4380 in_finished
4381 );
4382 if in_finished || in_running {
4383 continue;
4384 }
4385 finished = false;
4386 if !self.task_dependencies_satisfied(task) {
4387 continue;
4388 }
4389 if !self.task_ready(task) {
4390 continue;
4391 }
4392 next = Some(task.clone());
4393 break;
4394 }
4395 next
4396 };
4397
4398 let Some(task) = next_task else {
4399 tracing::debug!(
4400 "send_tasks returning finished={} (dispatched {})",
4401 finished,
4402 dispatched
4403 );
4404 return finished;
4405 };
4406 let Some(worker_index) = self.take_ready_worker_index() else {
4407 self.force_stalled_task_completions();
4408 tracing::debug!(
4409 "send_tasks returning false (no ready worker; dispatched {})",
4410 dispatched
4411 );
4412 return false;
4413 };
4414
4415 if Self::task_running_finished_contains(&self.cycle_tasks_finished, &task)
4416 || Self::task_running_finished_contains(&self.cycle_tasks_running, &task)
4417 {
4418 continue;
4419 }
4420 dispatched += 1;
4421 let task_key = Self::task_key(&task);
4422 tracing::debug!(
4423 "send_tasks dispatching {} (running={} finished={})",
4424 Self::task_track_name(&task),
4425 self.cycle_tasks_running.len(),
4426 self.cycle_tasks_finished.len()
4427 );
4428 self.prepare_task_track(&task);
4429 self.cycle_tasks_running.push(task.clone());
4430 tracing::debug!(
4431 "inserted task {} -> running_size={}",
4432 Self::task_track_name(&task),
4433 self.cycle_tasks_running.len()
4434 );
4435 self.task_processing_started_at
4436 .insert(task_key.clone(), Instant::now());
4437 let worker = &self.workers[worker_index];
4438 if let Err(e) = worker.tx.send(Message::ProcessTask(task.clone())).await {
4439 self.cycle_tasks_running
4440 .retain(|t| Self::task_key(t) != task_key);
4441 self.task_processing_started_at.remove(&task_key);
4442 self.notify_clients(Err(format!("Failed to send task to worker: {}", e)))
4443 .await;
4444 }
4445 }
4446 }
4447
4448 async fn on_all_tracks_finished(&mut self) {
4449 if self.transport_restart_pending {
4450 let state = self.state.lock();
4451 for track in state.tracks.values() {
4452 track.lock().take_hw_midi_out_events();
4453 }
4454 } else if self.hw_worker.is_some() {
4455 self.active_hw_notes_cycle_start = self.active_hw_notes_by_track.clone();
4456 let mut out_events = self.collect_hw_midi_output_events_by_device();
4457 if self.loop_enabled
4458 && let Some((_, loop_end)) = self.loop_range_samples
4459 {
4460 let cycle_end = self
4461 .transport_sample
4462 .saturating_add(self.current_cycle_samples());
4463 if self.transport_sample < loop_end && cycle_end >= loop_end {
4464 let wrap_frame = loop_end
4465 .saturating_sub(self.transport_sample)
4466 .min(self.current_cycle_samples())
4467 as u32;
4468 out_events.extend(self.note_off_events_for_active_snapshot(
4469 &self.active_hw_notes_cycle_start,
4470 wrap_frame,
4471 ));
4472 out_events.sort_by(|a, b| {
4473 a.event
4474 .frame
4475 .cmp(&b.event.frame)
4476 .then_with(|| a.device.cmp(&b.device))
4477 });
4478 }
4479 }
4480 self.pending_hw_midi_out_events_by_device.extend(out_events);
4481 } else {
4482 self.pending_hw_midi_out_events = self.collect_hw_midi_output_events();
4483 }
4484 self.request_hw_cycle().await;
4485 }
4486
4487 fn take_ready_worker_index(&mut self) -> Option<usize> {
4488 while !self.ready_workers.is_empty() {
4489 let worker_index = self.ready_workers.remove(0);
4490 if worker_index < self.workers.len() {
4491 return Some(worker_index);
4492 }
4493 }
4494 None
4495 }
4496
4497 fn push_ready_worker(&mut self, worker_index: usize) {
4498 self.ready_workers.push(worker_index);
4499 }
4500
4501 async fn publish_track_meters(&mut self) {
4502 if !self.should_publish_track_meters() {
4503 return;
4504 }
4505 let tracks: Vec<(String, Arc<UnsafeMutex<Box<Track>>>)> = self
4506 .state
4507 .lock()
4508 .tracks
4509 .iter()
4510 .map(|(name, track)| (name.clone(), track.clone()))
4511 .collect();
4512 let mut snapshot = Vec::with_capacity(tracks.len());
4513 for (name, track) in &tracks {
4514 let linear = self
4515 .track_meter_linear_by_track
4516 .get(name)
4517 .cloned()
4518 .unwrap_or_else(|| track.lock().output_meter_linear());
4519 let output_db = linear
4520 .iter()
4521 .copied()
4522 .map(Self::meter_linear_to_db)
4523 .collect::<Vec<_>>();
4524 snapshot.push((name.clone(), output_db));
4525 }
4526 self.latest_track_meter_snapshot = Arc::new(snapshot);
4527 let meters = self.collect_changed_track_meters(&tracks);
4528 for (track_name, output_db) in meters {
4529 self.notify_clients(Ok(Action::TrackMeters {
4530 track_name,
4531 output_db,
4532 }))
4533 .await;
4534 }
4535 }
4536
4537 async fn publish_clap_state_dirty(&mut self) {
4538 let tracks: Vec<(String, Arc<UnsafeMutex<Box<Track>>>)> = self
4539 .state
4540 .lock()
4541 .tracks
4542 .iter()
4543 .map(|(name, track)| (name.clone(), track.clone()))
4544 .collect();
4545 for (track_name, track) in &tracks {
4546 let dirty = track.lock().take_dirty_clap_instances();
4547 for instance_id in dirty {
4548 self.notify_clients(Ok(Action::TrackClapStateDirty {
4549 track_name: track_name.clone(),
4550 instance_id,
4551 }))
4552 .await;
4553 }
4554 }
4555 }
4556
4557 fn reset_meters_after_stop(&mut self) {
4558 self.last_hw_out_meter_publish = None;
4559 self.last_track_meter_publish = None;
4560 self.hw_out_peak_hold_linear.fill(0.0);
4561 #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
4562 {
4563 self.last_hw_out_meter_linear.clear();
4564 }
4565 let hw_channels = self.latest_hw_out_meter_db.len();
4566 self.latest_hw_out_meter_db = Arc::new(vec![-90.0; hw_channels]);
4567
4568 let tracks: Vec<(String, Arc<UnsafeMutex<Box<Track>>>)> = self
4569 .state
4570 .lock()
4571 .tracks
4572 .iter()
4573 .map(|(name, track)| (name.clone(), track.clone()))
4574 .collect();
4575 self.track_meter_linear_by_track.clear();
4576 let mut snapshot = Vec::with_capacity(tracks.len());
4577 for (name, track) in tracks {
4578 let t = track.lock();
4579 t.clear_output_meters();
4580 let width = t.output_meter_linear().len();
4581 let zero_linear = vec![0.0; width];
4582 self.track_meter_linear_by_track
4583 .insert(name.clone(), zero_linear);
4584 snapshot.push((name, vec![-90.0; width]));
4585 }
4586 self.latest_track_meter_snapshot = Arc::new(snapshot);
4587 }
4588
4589 pub fn check_if_leads_to_kind(
4590 &self,
4591 kind: Kind,
4592 current_track_name: &str,
4593 target_track_name: &str,
4594 ) -> bool {
4595 routing::would_create_cycle(
4596 &target_track_name.to_string(),
4597 ¤t_track_name.to_string(),
4598 |track_name| self.connected_neighbors(kind, track_name),
4599 )
4600 }
4601
4602 fn connected_neighbors(&self, kind: Kind, current_track_name: &str) -> Vec<String> {
4603 let state = self.state.lock();
4604 let mut found_neighbors = Vec::new();
4605
4606 if let Some(current_track_handle) = state.tracks.get(current_track_name) {
4607 let current_track = current_track_handle.lock();
4608
4609 match kind {
4610 Kind::Audio => {
4611 for out_port in ¤t_track.audio.outs {
4612 let conns = out_port.connections.lock();
4613 for conn in conns.iter() {
4614 for (name, next_track_handle) in &state.tracks {
4615 let next_track = next_track_handle.lock();
4616 let is_connected =
4617 next_track.audio.ins.iter().any(|ins_port| {
4618 Arc::ptr_eq(&ins_port.buffer, &conn.buffer)
4619 });
4620
4621 if is_connected {
4622 found_neighbors.push(name.clone());
4623 }
4624 }
4625 }
4626 }
4627 }
4628 Kind::MIDI => {
4629 for out_port in ¤t_track.midi.outs {
4630 let conns = out_port.lock().connections.clone();
4631 for conn in conns.iter() {
4632 for (name, next_track_handle) in &state.tracks {
4633 let next_track = next_track_handle.lock();
4634 let is_connected = next_track
4635 .midi
4636 .ins
4637 .iter()
4638 .any(|ins_port| Arc::ptr_eq(ins_port, conn));
4639
4640 if is_connected {
4641 found_neighbors.push(name.clone());
4642 }
4643 }
4644 }
4645 }
4646 }
4647 }
4648 }
4649 found_neighbors
4650 }
4651
4652 async fn handle_request(&mut self, a: Action) {
4653 match a {
4654 Action::Log { source, message } => {
4655 self.notify_clients(Ok(Action::Log { source, message }))
4656 .await;
4657 }
4658 Action::Undo => {
4659 let actions = match self.history.undo() {
4660 Some(actions) => actions,
4661 None => {
4662 self.notify_clients(Ok(Action::Undo)).await;
4663 self.notify_clients(Ok(Action::HistoryState {
4664 dirty: self.history.is_dirty(),
4665 }))
4666 .await;
4667 return;
4668 }
4669 };
4670
4671 let was_suspended = self.history_suspended;
4672 self.history_suspended = true;
4673 for action in actions {
4674 self.handle_request_inner(action, false).await;
4675 }
4676 self.history_suspended = was_suspended;
4677 self.notify_clients(Ok(Action::Undo)).await;
4678 self.notify_clients(Ok(Action::HistoryState {
4679 dirty: self.history.is_dirty(),
4680 }))
4681 .await;
4682 }
4683 Action::Redo => {
4684 let actions = match self.history.redo() {
4685 Some(actions) => actions,
4686 None => {
4687 self.notify_clients(Ok(Action::Redo)).await;
4688 self.notify_clients(Ok(Action::HistoryState {
4689 dirty: self.history.is_dirty(),
4690 }))
4691 .await;
4692 return;
4693 }
4694 };
4695
4696 let was_suspended = self.history_suspended;
4697 self.history_suspended = true;
4698 for action in actions {
4699 self.handle_request_inner(action, false).await;
4700 }
4701 self.history_suspended = was_suspended;
4702 self.notify_clients(Ok(Action::Redo)).await;
4703 self.notify_clients(Ok(Action::HistoryState {
4704 dirty: self.history.is_dirty(),
4705 }))
4706 .await;
4707 }
4708 Action::ApplyGroupedActions(actions) => {
4709 self.handle_request_inner(Action::BeginHistoryGroup, true)
4710 .await;
4711 for action in actions {
4712 self.handle_request_inner(action, true).await;
4713 }
4714 self.handle_request_inner(Action::EndHistoryGroup, true)
4715 .await;
4716 }
4717 other => {
4718 self.handle_request_inner(other, true).await;
4719 }
4720 }
4721 }
4722
4723 fn find_audio_io_owner(
4724 &self,
4725 state: &crate::state::State,
4726 io: &std::sync::Arc<crate::audio::io::AudioIO>,
4727 ) -> Option<(String, usize)> {
4728 for (name, track) in &state.tracks {
4729 let t = track.lock();
4730 for (i, out) in t.audio.outs.iter().enumerate() {
4731 if std::sync::Arc::ptr_eq(out, io) {
4732 return Some((name.clone(), i));
4733 }
4734 }
4735 for (i, inp) in t.audio.ins.iter().enumerate() {
4736 if std::sync::Arc::ptr_eq(inp, io) {
4737 return Some((name.clone(), i));
4738 }
4739 }
4740 }
4741 None
4742 }
4743
4744 fn find_midi_io_owner(
4745 &self,
4746 state: &crate::state::State,
4747 io: &std::sync::Arc<crate::mutex::UnsafeMutex<Box<crate::midi::io::MIDIIO>>>,
4748 ) -> Option<(String, usize, bool)> {
4749 for (name, track) in &state.tracks {
4750 let t = track.lock();
4751 for (i, out) in t.midi.outs.iter().enumerate() {
4752 if std::sync::Arc::ptr_eq(out, io) {
4753 return Some((name.clone(), i, false));
4754 }
4755 }
4756 for (i, inp) in t.midi.ins.iter().enumerate() {
4757 if std::sync::Arc::ptr_eq(inp, io) {
4758 return Some((name.clone(), i, true));
4759 }
4760 }
4761 }
4762 None
4763 }
4764
4765 fn collect_descendant_track_names(&self, name: &str, out: &mut Vec<String>) {
4766 let child_arcs: Vec<Arc<UnsafeMutex<Box<Track>>>> = {
4769 let state = self.state.lock();
4770 if let Some(track) = state.tracks.get(name) {
4771 track.lock().child_tracks.clone()
4772 } else {
4773 Vec::new()
4774 }
4775 };
4776 for child in child_arcs {
4777 let child_name = { child.lock().name.clone() };
4778 self.collect_descendant_track_names(&child_name, out);
4779 out.push(child_name);
4780 }
4781 }
4782
4783 async fn remove_single_track(&mut self, name: &str) {
4784 let children: Vec<Arc<UnsafeMutex<Box<Track>>>> = {
4785 let state = self.state.lock();
4786 if let Some(removed) = state.tracks.get(name).cloned() {
4787 removed.lock().child_tracks.clone()
4788 } else {
4789 Vec::new()
4790 }
4791 };
4792 let parent_name: Option<String> = {
4793 let state = self.state.lock();
4794 state
4795 .tracks
4796 .get(name)
4797 .map(|t| t.lock().parent_track.clone())
4798 .unwrap_or(None)
4799 };
4800 if let Some(parent_name) = parent_name {
4801 let state = self.state.lock();
4802 if let Some(parent) = state.tracks.get(&parent_name).cloned() {
4803 let parent = parent.lock();
4804 parent.child_tracks.retain(|c| c.lock().name != *name);
4805 }
4806 }
4807 if let Some(removed_track) = self.state.lock().tracks.get(name).cloned() {
4808 for child in children {
4809 let removed = removed_track.lock();
4810 child.lock().disconnect_from_parent(removed);
4811 child.lock().parent_track = None;
4812 }
4813 }
4814 self.state.lock().tracks.remove(name);
4815 self.audio_recordings.remove(name);
4816 self.midi_recordings.remove(name);
4817 self.midi_hw_in_routes.retain(|r| r.to_track != *name);
4818 self.midi_hw_out_routes.retain(|r| r.from_track != *name);
4819 if self
4820 .pending_midi_learn
4821 .as_ref()
4822 .is_some_and(|(track_name, _, _)| track_name == name)
4823 {
4824 self.pending_midi_learn = None;
4825 }
4826 }
4827
4828 async fn handle_request_inner(&mut self, mut action_to_process: Action, record_history: bool) {
4829 let a = action_to_process.clone();
4830 let suppress_timing_history = self.playing
4831 && matches!(
4832 &action_to_process,
4833 Action::SetTempo(_) | Action::SetTimeSignature { .. }
4834 );
4835 let mut extra_inverse_actions: Vec<Action> = Vec::new();
4836 if record_history
4837 && !self.history_suspended
4838 && let Action::RemoveTrack(ref track_name) = action_to_process
4839 {
4840 for route in self
4841 .midi_hw_in_routes
4842 .iter()
4843 .filter(|route| &route.to_track == track_name)
4844 {
4845 extra_inverse_actions.push(Action::Connect {
4846 from_track: format!("midi:hw:in:{}", route.device),
4847 from_port: 0,
4848 to_track: route.to_track.clone(),
4849 to_port: route.to_port,
4850 kind: Kind::MIDI,
4851 });
4852 }
4853 for route in self
4854 .midi_hw_out_routes
4855 .iter()
4856 .filter(|route| &route.from_track == track_name)
4857 {
4858 extra_inverse_actions.push(Action::Connect {
4859 from_track: route.from_track.clone(),
4860 from_port: route.from_port,
4861 to_track: format!("midi:hw:out:{}", route.device),
4862 to_port: 0,
4863 kind: Kind::MIDI,
4864 });
4865 }
4866 }
4867 if record_history
4868 && !self.history_suspended
4869 && matches!(action_to_process, Action::ClearAllMidiLearnBindings)
4870 {
4871 if let Some(binding) = self.global_midi_learn_play_pause.clone() {
4872 extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
4873 target: crate::message::GlobalMidiLearnTarget::PlayPause,
4874 binding: Some(binding),
4875 });
4876 }
4877 if let Some(binding) = self.global_midi_learn_stop.clone() {
4878 extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
4879 target: crate::message::GlobalMidiLearnTarget::Stop,
4880 binding: Some(binding),
4881 });
4882 }
4883 if let Some(binding) = self.global_midi_learn_record_toggle.clone() {
4884 extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
4885 target: crate::message::GlobalMidiLearnTarget::RecordToggle,
4886 binding: Some(binding),
4887 });
4888 }
4889 }
4890 let mut inverse_actions = if record_history
4891 && !suppress_timing_history
4892 && should_record(&action_to_process)
4893 && !self.history_suspended
4894 {
4895 let state = self.state.lock();
4896 create_inverse_actions(&action_to_process, state).map(|mut actions| {
4897 actions.extend(extra_inverse_actions);
4898 actions
4899 })
4900 } else {
4901 None
4902 };
4903 if record_history && !suppress_timing_history && !self.history_suspended {
4904 match &action_to_process {
4905 Action::SetTempo(_) => {
4906 inverse_actions = Some(vec![Action::SetTempo(self.tempo_bpm)]);
4907 }
4908 Action::SetLoopEnabled(_) => {
4909 inverse_actions = Some(vec![Action::SetLoopEnabled(self.loop_enabled)]);
4910 }
4911 Action::SetLoopRange(_) => {
4912 inverse_actions = Some(vec![
4913 Action::SetLoopRange(self.loop_range_samples),
4914 Action::SetLoopEnabled(self.loop_enabled),
4915 ]);
4916 }
4917 Action::SetPunchEnabled(_) => {
4918 inverse_actions = Some(vec![Action::SetPunchEnabled(self.punch_enabled)]);
4919 }
4920 Action::SetPunchRange(_) => {
4921 inverse_actions = Some(vec![
4922 Action::SetPunchRange(self.punch_range_samples),
4923 Action::SetPunchEnabled(self.punch_enabled),
4924 ]);
4925 }
4926 Action::SetMetronomeEnabled(_) => {
4927 inverse_actions =
4928 Some(vec![Action::SetMetronomeEnabled(self.metronome_enabled)]);
4929 }
4930 Action::SetTimeSignature { .. } => {
4931 inverse_actions = Some(vec![Action::SetTimeSignature {
4932 numerator: self.tsig_num,
4933 denominator: self.tsig_denom,
4934 }]);
4935 }
4936 Action::SetClipPlaybackEnabled(_) => {
4937 inverse_actions = Some(vec![Action::SetClipPlaybackEnabled(
4938 self.clip_playback_enabled,
4939 )]);
4940 }
4941 Action::SetRecordEnabled(_) => {
4942 inverse_actions = Some(vec![Action::SetRecordEnabled(self.record_enabled)]);
4943 }
4944 Action::SetGlobalMidiLearnBinding { target, .. } => {
4945 let binding = match target {
4946 crate::message::GlobalMidiLearnTarget::PlayPause => {
4947 self.global_midi_learn_play_pause.clone()
4948 }
4949 crate::message::GlobalMidiLearnTarget::Stop => {
4950 self.global_midi_learn_stop.clone()
4951 }
4952 crate::message::GlobalMidiLearnTarget::RecordToggle => {
4953 self.global_midi_learn_record_toggle.clone()
4954 }
4955 };
4956 inverse_actions = Some(vec![Action::SetGlobalMidiLearnBinding {
4957 target: *target,
4958 binding,
4959 }]);
4960 }
4961 _ => {}
4962 }
4963 }
4964
4965 match action_to_process {
4966 Action::Play => {
4967 tracing::debug!(
4968 "Action::Play pressed, transport_sample={}",
4969 self.transport_sample
4970 );
4971 self.playing = true;
4972 self.transport_restart_pending = true;
4973 self.notified_loop_wrap_sample = None;
4974 self.invalidate_track_cycle_state();
4975 if let Some(driver) = self.hw_driver.as_mut() {
4976 driver.lock().set_playing(true);
4977 }
4978 #[cfg(unix)]
4979 if let Some(jack) = &self.jack_runtime
4980 && let Err(e) = jack.lock().transport_start()
4981 {
4982 self.notify_clients(Err(e)).await;
4983 }
4984 self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
4985 .await;
4986 self.preload_track_clips().await;
4987 {
4988 let echoes = self.apply_modulators(self.transport_sample);
4989 for action in echoes {
4990 self.notify_clients(Ok(action)).await;
4991 }
4992 }
4993 let send_result = self.send_tasks().await;
4994 tracing::debug!("send_tasks after Play returned finished={}", send_result);
4995 if !self.awaiting_hwfinished
4996 && !self.handling_hwfinished
4997 && send_result
4998 && self.hw_worker.is_some()
4999 {
5000 self.transport_restart_pending = false;
5001 self.request_hw_cycle().await;
5002 }
5003 }
5004 Action::Pause => {
5005 self.clip_playback_enabled = false;
5006 for track in self.state.lock().tracks.values() {
5007 track.lock().set_clip_playback_enabled(false);
5008 }
5009 if !self.playing {
5010 self.playing = true;
5011 self.transport_restart_pending = true;
5012 self.notified_loop_wrap_sample = None;
5013 self.invalidate_track_cycle_state();
5014 if let Some(driver) = self.hw_driver.as_mut() {
5015 driver.lock().set_playing(true);
5016 }
5017 #[cfg(unix)]
5018 if let Some(jack) = &self.jack_runtime
5019 && let Err(e) = jack.lock().transport_start()
5020 {
5021 self.notify_clients(Err(e)).await;
5022 }
5023 self.preload_track_clips().await;
5024 if !self.awaiting_hwfinished
5025 && !self.handling_hwfinished
5026 && self.send_tasks().await
5027 && self.hw_worker.is_some()
5028 {
5029 self.transport_restart_pending = false;
5030 self.request_hw_cycle().await;
5031 }
5032 }
5033 self.notify_clients(Ok(Action::Pause)).await;
5034 self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
5035 .await;
5036 }
5037 Action::Stop => {
5038 self.playing = false;
5039 self.transport_panic_flush_pending = false;
5040 self.transport_restart_pending = false;
5041 self.notified_loop_wrap_sample = None;
5042 self.invalidate_track_cycle_state();
5043 if let Some(driver) = self.hw_driver.as_mut() {
5044 driver.lock().set_playing(false);
5045 }
5046 #[cfg(unix)]
5047 if let Some(jack) = &self.jack_runtime
5048 && let Err(e) = jack.lock().transport_stop()
5049 {
5050 self.notify_clients(Err(e)).await;
5051 }
5052 let panic_events = self.note_off_events_for_all_active_tracks();
5053 if let Some(worker) = &self.hw_worker {
5054 if !panic_events.is_empty()
5055 && let Err(e) = worker.tx.send(Message::HWMidiOutEvents(panic_events)).await
5056 {
5057 error!("Error sending stop MIDI panic events {e}");
5058 }
5059 } else {
5060 self.pending_hw_midi_out_events_by_device
5061 .extend(panic_events);
5062 }
5063 self.reset_meters_after_stop();
5064 self.flush_recordings().await;
5065 self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
5066 .await;
5067 }
5068 Action::JumpToEnd => {
5069 self.transport_sample = self.normalize_transport_sample(self.session_end_sample());
5070 self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
5071 .await;
5072 }
5073 Action::Panic => {
5074 let panic_events = self.panic_events_for_all_hw_midi_outputs();
5075 if let Some(worker) = &self.hw_worker {
5076 if !panic_events.is_empty() {
5077 if let Err(e) = worker.tx.send(Message::ClearHWMidiOutEvents).await {
5078 error!("Error clearing HW MIDI queue for panic {e}");
5079 }
5080 self.midi_hub
5081 .lock()
5082 .write_events_blocking(&panic_events, Duration::from_millis(250));
5083 }
5084 } else if !panic_events.is_empty() {
5085 self.pending_hw_midi_out_events_by_device
5086 .extend(panic_events);
5087 }
5088 }
5089 Action::SetClipPlaybackEnabled(enabled) => {
5090 self.clip_playback_enabled = enabled;
5091 for track in self.state.lock().tracks.values() {
5092 track.lock().set_clip_playback_enabled(enabled);
5093 }
5094 }
5095 Action::TransportPosition(sample) => {
5096 self.transport_sample = self.normalize_transport_sample(sample);
5097 self.notified_loop_wrap_sample = None;
5098 {
5099 let echoes = self.apply_modulators(self.transport_sample);
5100 for action in echoes {
5101 self.notify_clients(Ok(action)).await;
5102 }
5103 }
5104 #[cfg(unix)]
5105 if let Some(jack) = &self.jack_runtime
5106 && let Err(e) = jack.lock().transport_locate(self.transport_sample)
5107 {
5108 self.notify_clients(Err(e)).await;
5109 }
5110 if self.playing {
5111 self.transport_restart_pending = true;
5112 self.invalidate_track_cycle_state();
5113 self.transport_panic_flush_pending = self.hw_worker.is_some();
5114 self.clear_hw_midi_output_state(true).await;
5115 if !self.awaiting_hwfinished && !self.handling_hwfinished {
5116 if self.hw_worker.is_some() {
5117 self.request_hw_cycle().await;
5118 } else if self.send_tasks().await {
5119 self.transport_restart_pending = false;
5120 self.request_hw_cycle().await;
5121 }
5122 }
5123 }
5124 }
5125 Action::SetLoopEnabled(enabled) => {
5126 self.loop_enabled = enabled && self.loop_range_samples.is_some();
5127 self.notified_loop_wrap_sample = None;
5128 }
5129 Action::SetLoopRange(range) => {
5130 self.loop_range_samples = range.and_then(|(start, end)| {
5131 if end > start {
5132 Some((start, end))
5133 } else {
5134 None
5135 }
5136 });
5137 self.loop_enabled = self.loop_range_samples.is_some();
5138 self.notified_loop_wrap_sample = None;
5139 if self.loop_enabled
5140 && let Some((loop_start, loop_end)) = self.loop_range_samples
5141 && self.transport_sample >= loop_end
5142 {
5143 self.transport_sample = loop_start;
5144 self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
5145 .await;
5146 }
5147 }
5148 Action::SetPunchEnabled(enabled) => {
5149 self.punch_enabled = enabled && self.punch_range_samples.is_some();
5150 }
5151 Action::SetPunchRange(range) => {
5152 self.punch_range_samples = range.and_then(|(start, end)| {
5153 if end > start {
5154 Some((start, end))
5155 } else {
5156 None
5157 }
5158 });
5159 self.punch_enabled = self.punch_range_samples.is_some();
5160 }
5161 Action::SetMetronomeEnabled(enabled) => {
5162 self.metronome_enabled = enabled;
5163 if enabled {
5164 self.ensure_metronome_track().await;
5165 }
5166 if let Some(track) = self.state.lock().tracks.get(Self::METRONOME_TRACK).cloned() {
5167 track.lock().set_metronome_enabled(enabled);
5168 }
5169 }
5170 Action::SetTempo(bpm) => {
5171 self.tempo_bpm = bpm.max(1.0);
5172 }
5173 Action::SetTimeSignature {
5174 numerator,
5175 denominator,
5176 } => {
5177 self.tsig_num = numerator.max(1);
5178 self.tsig_denom = denominator.max(1);
5179 }
5180 Action::SetOscEnabled(enabled) => {
5181 if let Err(err) = self.set_osc_enabled_with(enabled, OscServer::start) {
5182 self.notify_clients(Err(err)).await;
5183 }
5184 }
5185 Action::SetRecordEnabled(enabled) => {
5186 self.record_enabled = enabled;
5187 if !enabled {
5188 if self.awaiting_hwfinished {
5189 self.append_recorded_cycle();
5190 }
5191 self.flush_recordings().await;
5192 } else if self.session_dir.is_none() {
5193 self.notify_clients(Err(
5194 "Recording enabled but session path is not set".to_string()
5195 ))
5196 .await;
5197 }
5198 }
5199 Action::SetModulators(ref modulators) => {
5200 self.modulators = modulators.clone();
5201 let echoes = self.apply_modulators(self.transport_sample);
5202 for action in echoes {
5203 self.notify_clients(Ok(action)).await;
5204 }
5205 }
5206 Action::SetStepRecording(enabled) => {
5207 self.step_recording_enabled = enabled;
5208 }
5209 Action::BeginHistoryGroup if self.history_group.is_none() => {
5210 self.history_group = Some(UndoEntry {
5211 forward_actions: vec![],
5212 inverse_actions: vec![],
5213 });
5214 }
5215 Action::EndHistoryGroup => {
5216 if let Some(mut group) = self.history_group.take()
5217 && !group.forward_actions.is_empty()
5218 && !group.inverse_actions.is_empty()
5219 {
5220 let mut add_tracks = Vec::new();
5221 let mut connections = Vec::new();
5222 let mut rest = Vec::new();
5223 for action in group.inverse_actions {
5224 if matches!(action, Action::AddTrack { .. }) {
5225 add_tracks.push(action);
5226 } else if matches!(action, Action::Connect { .. }) {
5227 connections.push(action);
5228 } else {
5229 rest.push(action);
5230 }
5231 }
5232 group.inverse_actions = add_tracks;
5233 group.inverse_actions.extend(rest);
5234 group.inverse_actions.extend(connections);
5235 self.history.record(group);
5236 }
5237 }
5238 Action::SetSessionPath(ref path) => {
5239 self.session_dir = Some(Path::new(path).to_path_buf());
5240 self.ensure_session_subdirs();
5241 #[cfg(all(unix, not(target_os = "macos")))]
5242 let _lv2_dir = self.session_plugins_dir();
5243 for track in self.state.lock().tracks.values() {
5244 track.lock().set_session_base_dir(self.session_dir.clone());
5245 }
5246 }
5247 Action::MarkHistorySavePoint => {
5248 self.history.mark_save_point();
5249 self.notify_clients(Ok(Action::HistoryState {
5250 dirty: self.history.is_dirty(),
5251 }))
5252 .await;
5253 }
5254 Action::ClearHistory => {
5255 self.history.clear();
5256 self.history.mark_save_point();
5257 }
5258 Action::BeginSessionRestore => {
5259 self.history_suspended = true;
5260 self.history.clear();
5261 }
5262 Action::EndSessionRestore => {
5263 self.history.clear();
5264 self.history_suspended = false;
5265 self.preload_track_clips_spawn();
5266 }
5267 Action::Quit => {
5268 self.flush_recordings().await;
5269 if let Some(worker) = self.hw_worker.take() {
5277 if let Some(hw) = &self.hw_driver {
5278 hw.lock().request_stop();
5279 }
5280 let panic_events = self.panic_events_for_all_hw_midi_outputs();
5283 if !panic_events.is_empty() {
5284 let _ = worker.tx.send(Message::HWMidiOutEvents(panic_events)).await;
5285 }
5286 if let Err(e) = worker.tx.send(Message::Request(a.clone())).await {
5289 error!("Error sending quit message to HW worker: {e}");
5290 }
5291 worker
5292 .handle
5293 .await
5294 .unwrap_or_else(|e| error!("Error waiting for HW worker to quit: {e}"));
5295 }
5296 if let Some(hw) = &self.hw_driver {
5302 hw.lock().close_fds();
5303 }
5304 self.midi_hub.lock().close_all();
5305 self.hw_driver = None;
5306 self.notify_clients(Ok(Action::Quit)).await;
5307 self.ready_workers.clear();
5308 while !self.workers.is_empty() {
5309 let worker = self.workers.remove(0);
5310 if let Err(e) = worker.tx.send(Message::Request(a.clone())).await {
5311 error!("Error sending quit message to worker: {e}");
5312 }
5313 worker
5314 .handle
5315 .await
5316 .unwrap_or_else(|e| error!("Error waiting for worker to quit: {e}"));
5317 }
5318 #[cfg(unix)]
5319 {
5320 self.jack_runtime = None;
5321 }
5322 self.osc_server = None;
5323 return;
5324 }
5325 Action::AddTrack {
5326 ref name,
5327 audio_ins,
5328 midi_ins,
5329 audio_outs,
5330 midi_outs,
5331 folder,
5332 } => {
5333 let tracks = &mut self.state.lock().tracks;
5334 if tracks.contains_key(name) {
5335 self.notify_clients(Err(format!("Track {} already exists", name)))
5336 .await;
5337 return;
5338 }
5339 let maybe_hw = if let Some(oss) = &self.hw_driver {
5340 let hw = oss.lock();
5341 Some((hw.cycle_samples(), hw.sample_rate() as f64))
5342 } else {
5343 #[cfg(unix)]
5344 if let Some(jack) = &self.jack_runtime {
5345 let j = jack.lock();
5346 Some((j.buffer_size, j.sample_rate as f64))
5347 } else {
5348 None
5349 }
5350 #[cfg(not(unix))]
5351 None
5352 };
5353
5354 if let Some((chsamples, sample_rate)) = maybe_hw {
5355 let track = if folder {
5356 Track::new_folder(
5357 name.clone(),
5358 audio_ins,
5359 audio_outs,
5360 midi_ins,
5361 midi_outs,
5362 chsamples,
5363 sample_rate,
5364 )
5365 } else {
5366 Track::new(
5367 name.clone(),
5368 audio_ins,
5369 audio_outs,
5370 midi_ins,
5371 midi_outs,
5372 chsamples,
5373 sample_rate,
5374 )
5375 };
5376 tracks.insert(name.clone(), Arc::new(UnsafeMutex::new(Box::new(track))));
5377 if let Some(track) = tracks.get(name) {
5378 let t = track.lock();
5379 t.set_clip_playback_enabled(self.clip_playback_enabled);
5380 t.set_transport_timing(self.tempo_bpm, self.tsig_num, self.tsig_denom);
5381 t.set_session_base_dir(self.session_dir.clone());
5382 }
5383 } else {
5384 self.notify_clients(Err(
5385 "Engine needs to open audio device before adding audio track".to_string(),
5386 ))
5387 .await;
5388 }
5389 }
5390 Action::TrackAddAudioInput(ref name) => {
5391 let track = match self.track_handle_or_err(name) {
5392 Ok(track) => track,
5393 Err(e) => {
5394 self.notify_clients(Err(e)).await;
5395 return;
5396 }
5397 };
5398 if let Err(e) = track.lock().add_audio_input() {
5399 self.notify_clients(Err(e)).await;
5400 return;
5401 }
5402 }
5403 Action::TrackAddAudioOutput(ref name) => {
5404 let track = match self.track_handle_or_err(name) {
5405 Ok(track) => track,
5406 Err(e) => {
5407 self.notify_clients(Err(e)).await;
5408 return;
5409 }
5410 };
5411 if let Err(e) = track.lock().add_audio_output() {
5412 self.notify_clients(Err(e)).await;
5413 return;
5414 }
5415 }
5416 Action::TrackRemoveAudioInput(ref name) => {
5417 let track = match self.track_handle_or_err(name) {
5418 Ok(track) => track,
5419 Err(e) => {
5420 self.notify_clients(Err(e)).await;
5421 return;
5422 }
5423 };
5424 if let Err(e) = track.lock().remove_audio_input() {
5425 self.notify_clients(Err(e)).await;
5426 return;
5427 }
5428 }
5429 Action::TrackRemoveAudioOutput(ref name) => {
5430 let track = match self.track_handle_or_err(name) {
5431 Ok(track) => track,
5432 Err(e) => {
5433 self.notify_clients(Err(e)).await;
5434 return;
5435 }
5436 };
5437 let (hw_outputs, track_inputs) = {
5438 let state = self.state.lock();
5439 let hw_outputs = self.all_hw_output_audio_ports();
5440 let track_inputs = state
5441 .tracks
5442 .iter()
5443 .filter(|(track_name, _)| *track_name != name)
5444 .flat_map(|(_, handle)| handle.lock().audio.ins.clone())
5445 .collect::<Vec<_>>();
5446 (hw_outputs, track_inputs)
5447 };
5448 if let Err(e) = track.lock().remove_audio_output(&hw_outputs, &track_inputs) {
5449 self.notify_clients(Err(e)).await;
5450 return;
5451 }
5452 }
5453 Action::RenameTrack {
5454 ref old_name,
5455 ref new_name,
5456 } => {
5457 if self.state.lock().tracks.contains_key(new_name) {
5458 self.notify_clients(Err(format!("Track '{}' already exists", new_name)))
5459 .await;
5460 return;
5461 }
5462
5463 let Some(track) = self.state.lock().tracks.remove(old_name) else {
5464 self.notify_clients(Err(format!("Track '{}' not found", old_name)))
5465 .await;
5466 return;
5467 };
5468
5469 track.lock().name = new_name.clone();
5470 self.state.lock().tracks.insert(new_name.clone(), track);
5471 for other in self.state.lock().tracks.values() {
5472 let other = other.lock();
5473 if other.parent_track.as_deref() == Some(old_name.as_str()) {
5474 other.parent_track = Some(new_name.clone());
5475 }
5476 }
5477
5478 if let Some(recording) = self.audio_recordings.remove(old_name) {
5479 self.audio_recordings.insert(new_name.clone(), recording);
5480 }
5481 if let Some(recording) = self.midi_recordings.remove(old_name) {
5482 self.midi_recordings.insert(new_name.clone(), recording);
5483 }
5484
5485 for route in &mut self.midi_hw_in_routes {
5486 if route.to_track == *old_name {
5487 route.to_track = new_name.clone();
5488 }
5489 }
5490 for route in &mut self.midi_hw_out_routes {
5491 if route.from_track == *old_name {
5492 route.from_track = new_name.clone();
5493 }
5494 }
5495 if let Some((armed_track, target, device)) = self.pending_midi_learn.clone()
5496 && armed_track == *old_name
5497 {
5498 self.pending_midi_learn = Some((new_name.clone(), target, device));
5499 }
5500
5501 self.notify_clients(Ok(Action::RenameTrack {
5502 old_name: old_name.clone(),
5503 new_name: new_name.clone(),
5504 }))
5505 .await;
5506 }
5507 Action::RemoveTrack(ref name) => {
5508 let mut descendant_names = Vec::new();
5509 self.collect_descendant_track_names(name, &mut descendant_names);
5510 let names_to_remove: Vec<String> = descendant_names
5511 .iter()
5512 .cloned()
5513 .chain(std::iter::once(name.clone()))
5514 .collect();
5515
5516 let combined_inverse = if record_history && !self.history_suspended {
5517 let state = self.state.lock();
5518 let mut inv = Vec::new();
5519 for n in &names_to_remove {
5520 if let Some(mut actions) =
5521 create_inverse_actions(&Action::RemoveTrack(n.clone()), state)
5522 {
5523 inv.append(&mut actions);
5524 }
5525 for route in self.midi_hw_in_routes.iter().filter(|r| &r.to_track == n) {
5526 inv.push(Action::Connect {
5527 from_track: format!("midi:hw:in:{}", route.device),
5528 from_port: 0,
5529 to_track: route.to_track.clone(),
5530 to_port: route.to_port,
5531 kind: Kind::MIDI,
5532 });
5533 }
5534 for route in self
5535 .midi_hw_out_routes
5536 .iter()
5537 .filter(|r| &r.from_track == n)
5538 {
5539 inv.push(Action::Connect {
5540 from_track: route.from_track.clone(),
5541 from_port: route.from_port,
5542 to_track: format!("midi:hw:out:{}", route.device),
5543 to_port: 0,
5544 kind: Kind::MIDI,
5545 });
5546 }
5547 }
5548
5549 let mut add_tracks = Vec::new();
5553 let mut connections = Vec::new();
5554 let mut rest = Vec::new();
5555 for action in inv {
5556 match action {
5557 Action::AddTrack { .. } => add_tracks.push(action),
5558 Action::Connect { .. } => connections.push(action),
5559 _ => rest.push(action),
5560 }
5561 }
5562 let mut ordered = add_tracks;
5563 ordered.extend(rest);
5564 ordered.extend(connections);
5565 ordered
5566 } else {
5567 Vec::new()
5568 };
5569
5570 for n in &descendant_names {
5571 self.remove_single_track(n).await;
5572 self.notify_clients(Ok(Action::RemoveTrack(n.clone())))
5573 .await;
5574 }
5575 self.remove_single_track(name).await;
5576
5577 if record_history && !self.history_suspended && !combined_inverse.is_empty() {
5578 self.history.record(UndoEntry {
5579 forward_actions: vec![Action::RemoveTrack(name.clone())],
5580 inverse_actions: combined_inverse,
5581 });
5582 }
5583
5584 inverse_actions = None;
5588 }
5589 Action::TrackLevel(ref name, level) => {
5590 if name == "hw:out" {
5591 self.hw_out_level_db = level;
5592 } else if let Some(track) = self.state.lock().tracks.get(name) {
5593 track.lock().set_level(level);
5594 }
5595 }
5596 Action::TrackBalance(ref name, balance) => {
5597 if name == "hw:out" {
5598 self.hw_out_balance = balance.clamp(-1.0, 1.0);
5599 } else if let Some(track) = self.state.lock().tracks.get(name) {
5600 track.lock().set_balance(balance);
5601 }
5602 }
5603 Action::TrackAutomationLevel(ref name, level) => {
5604 tracing::debug!(%name, level, "engine received TrackAutomationLevel");
5605 if name == "hw:out" {
5606 self.hw_out_level_db = level;
5607 } else if let Some(track) = self.state.lock().tracks.get(name) {
5608 track.lock().set_level(level);
5609 }
5610 }
5611 Action::TrackAutomationBalance(ref name, balance) => {
5612 if name == "hw:out" {
5613 self.hw_out_balance = balance.clamp(-1.0, 1.0);
5614 } else if let Some(track) = self.state.lock().tracks.get(name) {
5615 track.lock().set_balance(balance);
5616 }
5617 }
5618 Action::TrackMidiCc {
5619 ref track_name,
5620 channel,
5621 cc,
5622 value,
5623 } => {
5624 if let Some(track) = self.state.lock().tracks.get(track_name) {
5625 track
5626 .lock()
5627 .pending_automation_midi_events
5628 .push(MidiEvent::new(
5629 0,
5630 vec![0xB0 | channel.min(15), cc.min(127), value.min(127)],
5631 ));
5632 }
5633 }
5634 Action::RequestMeterSnapshot => {
5635 self.notify_clients(Ok(Action::MeterSnapshot {
5636 hw_out_db: self.latest_hw_out_meter_db.clone(),
5637 track_meters: self.latest_track_meter_snapshot.clone(),
5638 }))
5639 .await;
5640 return;
5641 }
5642 Action::TrackMeters { .. } => {}
5643 Action::MeterSnapshot { .. } => {}
5644 Action::TrackToggleArm(ref name) => {
5645 if self.reject_if_track_frozen(name, "arming/disarming").await {
5646 return;
5647 }
5648 if let Some(track) = self.state.lock().tracks.get(name).cloned() {
5649 track.lock().arm();
5650 let armed = track.lock().armed;
5651 if !armed && self.audio_recordings.contains_key(name) {
5652 self.flush_track_recording(name).await;
5653 }
5654 } else {
5655 tracing::warn!(
5656 "TrackToggleArm for '{}' but track not found in engine",
5657 name
5658 );
5659 }
5660 }
5661 Action::TrackToggleMute(ref name) => {
5662 if name == "hw:out" {
5663 self.hw_out_muted = !self.hw_out_muted;
5664 } else if let Some(track) = self.state.lock().tracks.get(name) {
5665 track.lock().mute();
5666 }
5667 }
5668 Action::TrackTogglePhase(ref name) => {
5669 if let Some(track) = self.state.lock().tracks.get(name) {
5670 track.lock().invert_phase();
5671 }
5672 }
5673 Action::TrackToggleSolo(ref name) => {
5674 if name == "hw:out" {
5675 return;
5676 }
5677 if let Some(track) = self.state.lock().tracks.get(name) {
5678 track.lock().solo();
5679 }
5680 }
5681 Action::TrackToggleMaster(ref name) => {
5682 if let Some(track) = self.state.lock().tracks.get(name) {
5683 track.lock().toggle_master();
5684 }
5685 }
5686 Action::TrackToggleInputMonitor {
5687 ref track_name,
5688 lane,
5689 } => {
5690 if let Some(track) = self.state.lock().tracks.get(track_name) {
5691 track.lock().toggle_input_monitor(lane);
5692 }
5693 }
5694 Action::TrackToggleDiskMonitor {
5695 ref track_name,
5696 lane,
5697 } => {
5698 if let Some(track) = self.state.lock().tracks.get(track_name) {
5699 track.lock().toggle_disk_monitor(lane);
5700 }
5701 }
5702 Action::TrackToggleMidiInputMonitor {
5703 ref track_name,
5704 lane,
5705 } => {
5706 if let Some(track) = self.state.lock().tracks.get(track_name) {
5707 track.lock().toggle_midi_input_monitor(lane);
5708 }
5709 }
5710 Action::TrackToggleMidiDiskMonitor {
5711 ref track_name,
5712 lane,
5713 } => {
5714 if let Some(track) = self.state.lock().tracks.get(track_name) {
5715 track.lock().toggle_midi_disk_monitor(lane);
5716 }
5717 }
5718 Action::TrackSetColor {
5719 ref track_name,
5720 color,
5721 } => {
5722 if let Some(track) = self.state.lock().tracks.get(track_name) {
5723 track.lock().color = color;
5724 }
5725 }
5726 Action::TrackArmMidiLearn {
5727 ref track_name,
5728 target,
5729 } => {
5730 if let Err(e) = self.track_handle_or_err(track_name) {
5731 self.notify_clients(Err(e)).await;
5732 return;
5733 }
5734 self.pending_midi_learn = Some((track_name.clone(), target, None));
5735 }
5736 Action::GlobalArmMidiLearn { target } => {
5737 self.pending_global_midi_learn = Some(target);
5738 }
5739 Action::TrackSetMidiLearnBinding {
5740 ref track_name,
5741 target,
5742 ref binding,
5743 } => {
5744 if let Some(binding) = binding.as_ref() {
5745 let conflicts = self.midi_learn_slot_conflicts(
5746 binding,
5747 Some(MidiLearnSlot::Track(track_name.clone(), target)),
5748 );
5749 if !conflicts.is_empty() {
5750 self.notify_clients(Err(format!(
5751 "MIDI learn conflict for '{}' {:?}: {}",
5752 track_name,
5753 target,
5754 conflicts.join(", ")
5755 )))
5756 .await;
5757 return;
5758 }
5759 }
5760 let track = match self.track_handle_or_err(track_name) {
5761 Ok(track) => track,
5762 Err(e) => {
5763 self.notify_clients(Err(e)).await;
5764 return;
5765 }
5766 };
5767 match target {
5768 crate::message::TrackMidiLearnTarget::Volume => {
5769 track.lock().midi_learn_volume = binding.clone();
5770 }
5771 crate::message::TrackMidiLearnTarget::Balance => {
5772 track.lock().midi_learn_balance = binding.clone();
5773 }
5774 crate::message::TrackMidiLearnTarget::Mute => {
5775 track.lock().midi_learn_mute = binding.clone();
5776 }
5777 crate::message::TrackMidiLearnTarget::Solo => {
5778 track.lock().midi_learn_solo = binding.clone();
5779 }
5780 crate::message::TrackMidiLearnTarget::Arm => {
5781 track.lock().midi_learn_arm = binding.clone();
5782 }
5783 crate::message::TrackMidiLearnTarget::InputMonitor => {
5784 track.lock().midi_learn_input_monitor = binding.clone();
5785 }
5786 crate::message::TrackMidiLearnTarget::DiskMonitor => {
5787 track.lock().midi_learn_disk_monitor = binding.clone();
5788 }
5789 }
5790 }
5791 Action::SetGlobalMidiLearnBinding {
5792 target,
5793 ref binding,
5794 } => {
5795 if let Some(binding) = binding.as_ref() {
5796 let conflicts = self
5797 .midi_learn_slot_conflicts(binding, Some(MidiLearnSlot::Global(target)));
5798 if !conflicts.is_empty() {
5799 self.notify_clients(Err(format!(
5800 "Global MIDI learn conflict for {:?}: {}",
5801 target,
5802 conflicts.join(", ")
5803 )))
5804 .await;
5805 return;
5806 }
5807 }
5808 match target {
5809 crate::message::GlobalMidiLearnTarget::PlayPause => {
5810 self.global_midi_learn_play_pause = binding.clone();
5811 }
5812 crate::message::GlobalMidiLearnTarget::Stop => {
5813 self.global_midi_learn_stop = binding.clone();
5814 }
5815 crate::message::GlobalMidiLearnTarget::RecordToggle => {
5816 self.global_midi_learn_record_toggle = binding.clone();
5817 }
5818 }
5819 }
5820 Action::TrackSetFolder {
5821 ref track_name,
5822 is_folder,
5823 } => {
5824 let track = match self.track_handle_or_err(track_name) {
5825 Ok(track) => track,
5826 Err(e) => {
5827 self.notify_clients(Err(e)).await;
5828 return;
5829 }
5830 };
5831 {
5832 let track = track.lock();
5833 track.is_folder = is_folder;
5834 track.ensure_default_audio_passthrough();
5835 track.ensure_default_midi_passthrough();
5836 }
5837 self.notify_clients(Ok(Action::TrackSetFolder {
5838 track_name: track_name.clone(),
5839 is_folder,
5840 }))
5841 .await;
5842 }
5843 Action::TrackSetParent {
5844 ref track_name,
5845 ref parent_name,
5846 } => {
5847 let track = match self.track_handle_or_err(track_name) {
5848 Ok(track) => track,
5849 Err(e) => {
5850 self.notify_clients(Err(e)).await;
5851 return;
5852 }
5853 };
5854 if parent_name.as_deref() == Some(track_name.as_str()) {
5855 self.notify_clients(Err("Track cannot be its own parent".to_string()))
5856 .await;
5857 return;
5858 }
5859
5860 if let Some(parent_name) = parent_name {
5862 let state = self.state.lock();
5863 let parent = state.tracks.get(parent_name);
5864 if parent.is_none() {
5865 self.notify_clients(Err(format!(
5866 "Parent track '{}' does not exist",
5867 parent_name
5868 )))
5869 .await;
5870 return;
5871 }
5872 if !parent.unwrap().lock().is_folder {
5873 self.notify_clients(Err(format!(
5874 "Track '{}' is not a folder",
5875 parent_name
5876 )))
5877 .await;
5878 return;
5879 }
5880 }
5881
5882 {
5884 let old_parent_name = track.lock().parent_track.clone();
5885 if let Some(old_parent_name) = old_parent_name {
5886 let state = self.state.lock();
5887 if let (Some(parent_arc), Some(child_arc)) = (
5888 state.tracks.get(&old_parent_name).cloned(),
5889 state.tracks.get(track_name).cloned(),
5890 ) {
5891 {
5892 let parent = parent_arc.lock();
5893 parent.child_tracks.retain(|c| c.lock().name != *track_name);
5894 }
5895 {
5896 let child = child_arc.lock();
5897 let parent = parent_arc.lock();
5898 child.disconnect_from_parent(parent);
5899 }
5900 }
5901 }
5902 }
5903
5904 let mut disconnect_actions = Vec::new();
5905
5906 {
5908 let state = self.state.lock();
5909 let hw_inputs = self.all_hw_input_audio_ports();
5910 let hw_outputs = self.all_hw_output_audio_ports();
5911 if let Some(child_arc) = state.tracks.get(track_name).cloned() {
5912 let child = child_arc.lock();
5913 for (port_idx, inp) in child.audio.ins.iter().enumerate() {
5914 let sources = inp.connections.lock().clone();
5915 for src in sources {
5916 let _ = AudioIO::disconnect(&src, inp);
5917 if let Some((src_name, src_port)) =
5918 self.find_audio_io_owner(state, &src)
5919 {
5920 disconnect_actions.push(Action::Disconnect {
5921 from_track: src_name,
5922 from_port: src_port,
5923 to_track: track_name.clone(),
5924 to_port: port_idx,
5925 kind: Kind::Audio,
5926 });
5927 } else if let Some(src_port) = hw_inputs
5928 .iter()
5929 .position(|hw_in| std::sync::Arc::ptr_eq(hw_in, &src))
5930 {
5931 disconnect_actions.push(Action::Disconnect {
5932 from_track: "hw:in".to_string(),
5933 from_port: src_port,
5934 to_track: track_name.clone(),
5935 to_port: port_idx,
5936 kind: Kind::Audio,
5937 });
5938 }
5939 }
5940 }
5941 for (port_idx, out) in child.audio.outs.iter().enumerate() {
5942 let targets = out.connections.lock().clone();
5943 for tgt in targets {
5944 let _ = AudioIO::disconnect(out, &tgt);
5945 if let Some((tgt_name, tgt_port)) =
5946 self.find_audio_io_owner(state, &tgt)
5947 {
5948 disconnect_actions.push(Action::Disconnect {
5949 from_track: track_name.clone(),
5950 from_port: port_idx,
5951 to_track: tgt_name,
5952 to_port: tgt_port,
5953 kind: Kind::Audio,
5954 });
5955 } else if let Some(tgt_port) = hw_outputs
5956 .iter()
5957 .position(|hw_out| std::sync::Arc::ptr_eq(hw_out, &tgt))
5958 {
5959 disconnect_actions.push(Action::Disconnect {
5960 from_track: track_name.clone(),
5961 from_port: port_idx,
5962 to_track: "hw:out".to_string(),
5963 to_port: tgt_port,
5964 kind: Kind::Audio,
5965 });
5966 }
5967 }
5968 }
5969
5970 for route in self
5972 .midi_hw_in_routes
5973 .iter()
5974 .filter(|r| r.to_track == *track_name)
5975 {
5976 disconnect_actions.push(Action::Disconnect {
5977 from_track: format!("midi:hw:in:{}", route.device),
5978 from_port: 0,
5979 to_track: track_name.clone(),
5980 to_port: route.to_port,
5981 kind: Kind::MIDI,
5982 });
5983 }
5984 self.midi_hw_in_routes.retain(|r| r.to_track != *track_name);
5985
5986 for route in self
5987 .midi_hw_out_routes
5988 .iter()
5989 .filter(|r| r.from_track == *track_name)
5990 {
5991 disconnect_actions.push(Action::Disconnect {
5992 from_track: track_name.clone(),
5993 from_port: route.from_port,
5994 to_track: format!("midi:hw:out:{}", route.device),
5995 to_port: 0,
5996 kind: Kind::MIDI,
5997 });
5998 }
5999 self.midi_hw_out_routes
6000 .retain(|r| r.from_track != *track_name);
6001
6002 for (port_idx, out) in child.midi.outs.iter().enumerate() {
6004 let targets = out.lock().connections.clone();
6005 for tgt in targets {
6006 if let Some((tgt_name, tgt_port, _)) =
6007 self.find_midi_io_owner(state, &tgt)
6008 {
6009 let _ = MIDIIO::disconnect(out, &tgt);
6010 disconnect_actions.push(Action::Disconnect {
6011 from_track: track_name.clone(),
6012 from_port: port_idx,
6013 to_track: tgt_name,
6014 to_port: tgt_port,
6015 kind: Kind::MIDI,
6016 });
6017 }
6018 }
6019 }
6020 }
6021
6022 let child_input_arcs: Vec<_> =
6024 if let Some(child_arc) = state.tracks.get(track_name).cloned() {
6025 let child = child_arc.lock();
6026 child.midi.ins.clone()
6027 } else {
6028 Vec::new()
6029 };
6030 for (other_name, other_track) in &state.tracks {
6031 if other_name == track_name {
6032 continue;
6033 }
6034 let other = other_track.lock();
6035 for (out_port, out) in other.midi.outs.iter().enumerate() {
6036 let targets = out.lock().connections.clone();
6037 for tgt in targets {
6038 if let Some(to_port) = child_input_arcs
6039 .iter()
6040 .position(|inp| std::sync::Arc::ptr_eq(inp, &tgt))
6041 {
6042 let _ = MIDIIO::disconnect(out, &tgt);
6043 disconnect_actions.push(Action::Disconnect {
6044 from_track: other_name.clone(),
6045 from_port: out_port,
6046 to_track: track_name.clone(),
6047 to_port,
6048 kind: Kind::MIDI,
6049 });
6050 }
6051 }
6052 }
6053 }
6054 }
6055
6056 {
6058 track.lock().parent_track = parent_name.clone();
6059 }
6060
6061 if let Some(parent_name) = parent_name {
6063 let state = self.state.lock();
6064 if let (Some(parent_arc), Some(child_arc)) = (
6065 state.tracks.get(parent_name).cloned(),
6066 state.tracks.get(track_name).cloned(),
6067 ) {
6068 {
6069 let parent = parent_arc.lock();
6070 parent.child_tracks.push(child_arc.clone());
6071 }
6072 {
6073 let child = child_arc.lock();
6074 let parent = parent_arc.lock();
6075 if parent.audio.ins.len() == child.audio.ins.len() {
6077 for (parent_in, child_in) in
6078 parent.audio.ins.iter().zip(child.audio.ins.iter())
6079 {
6080 Track::connect_directed_audio(parent_in, child_in);
6081 }
6082 }
6083 if parent.audio.outs.len() == child.audio.outs.len() {
6085 for (child_out, parent_out) in
6086 child.audio.outs.iter().zip(parent.audio.outs.iter())
6087 {
6088 AudioIO::connect(child_out, parent_out);
6089 }
6090 }
6091 if parent.midi.ins.len() == child.midi.ins.len() {
6093 for (parent_in, child_in) in
6094 parent.midi.ins.iter().zip(child.midi.ins.iter())
6095 {
6096 let child_in_lock = child_in.lock();
6097 if !child_in_lock
6098 .connections
6099 .iter()
6100 .any(|c| Arc::ptr_eq(c, parent_in))
6101 {
6102 child_in_lock.connections.push(parent_in.clone());
6103 }
6104 }
6105 }
6106 if parent.midi.outs.len() == child.midi.outs.len() {
6108 for (child_out, parent_out) in
6109 child.midi.outs.iter().zip(parent.midi.outs.iter())
6110 {
6111 let child_out_lock = child_out.lock();
6112 if !child_out_lock
6113 .connections
6114 .iter()
6115 .any(|c| Arc::ptr_eq(c, parent_out))
6116 {
6117 child_out_lock.connections.push(parent_out.clone());
6118 }
6119 }
6120 }
6121 child.invalidate_audio_route_cache();
6122 parent.invalidate_audio_route_cache();
6123 child.invalidate_midi_route_cache();
6124 parent.invalidate_midi_route_cache();
6125 }
6126 }
6127 }
6128
6129 {
6132 let state = self.state.lock();
6133 if let Some(child_arc) = state.tracks.get(track_name).cloned() {
6134 let child = child_arc.lock();
6135 child.ensure_default_audio_passthrough();
6136 child.ensure_default_midi_passthrough();
6137 }
6138 }
6139
6140 for action in disconnect_actions {
6141 self.notify_clients(Ok(action)).await;
6142 }
6143
6144 self.notify_clients(Ok(Action::TrackSetParent {
6145 track_name: track_name.clone(),
6146 parent_name: parent_name.clone(),
6147 }))
6148 .await;
6149 }
6150 Action::TrackToggleFolder { ref track_name } => {
6151 let track = match self.track_handle_or_err(track_name) {
6152 Ok(track) => track,
6153 Err(e) => {
6154 self.notify_clients(Err(e)).await;
6155 return;
6156 }
6157 };
6158 {
6159 let t = track.lock();
6160 t.folder_open = !t.folder_open;
6161 }
6162 self.notify_clients(Ok(Action::TrackToggleFolder {
6163 track_name: track_name.clone(),
6164 }))
6165 .await;
6166
6167 self.notify_clients(Ok(Action::TrackSetFolder {
6168 track_name: track_name.clone(),
6169 is_folder: track.lock().is_folder,
6170 }))
6171 .await;
6172 }
6173 Action::TrackSetMidiLaneChannel {
6174 ref track_name,
6175 lane,
6176 channel,
6177 } => {
6178 let track = match self.track_handle_or_err(track_name) {
6179 Ok(track) => track,
6180 Err(e) => {
6181 self.notify_clients(Err(e)).await;
6182 return;
6183 }
6184 };
6185 track.lock().set_midi_lane_channel(lane, channel);
6186 }
6187 Action::TrackSetFrozen {
6188 ref track_name,
6189 frozen,
6190 } => {
6191 let track = match self.track_handle_or_err(track_name) {
6192 Ok(track) => track,
6193 Err(e) => {
6194 self.notify_clients(Err(e)).await;
6195 return;
6196 }
6197 };
6198 track.lock().set_frozen(frozen);
6199 }
6200 Action::TrackOfflineBounce {
6201 track_name,
6202 output_path,
6203 start_sample,
6204 length_samples,
6205 automation_lanes,
6206 apply_fader,
6207 } => {
6208 if self.offline_bounce_jobs.contains_key(&track_name) {
6209 self.notify_clients(Err(format!(
6210 "Offline bounce for track '{}' is already in progress",
6211 track_name
6212 )))
6213 .await;
6214 return;
6215 }
6216 if let Err(e) = self.track_handle_or_err(&track_name) {
6217 self.notify_clients(Err(e)).await;
6218 return;
6219 }
6220 if length_samples == 0 {
6221 self.notify_clients(Err(format!(
6222 "Track '{}' has no renderable content for offline bounce",
6223 track_name
6224 )))
6225 .await;
6226 return;
6227 }
6228 let Some(worker_index) = self.take_ready_worker_index() else {
6229 self.pending_requests
6230 .push_front(Action::TrackOfflineBounce {
6231 track_name,
6232 output_path,
6233 start_sample,
6234 length_samples,
6235 automation_lanes,
6236 apply_fader,
6237 });
6238 return;
6239 };
6240 let cancel = Arc::new(AtomicBool::new(false));
6241 self.offline_bounce_jobs.insert(
6242 track_name.clone(),
6243 OfflineBounceJob {
6244 cancel: cancel.clone(),
6245 },
6246 );
6247 let track_name_clone = track_name.clone();
6248 let worker = &self.workers[worker_index];
6249 let job = crate::message::OfflineBounceWork {
6250 state: self.state.clone(),
6251 track_name,
6252 output_path,
6253 start_sample,
6254 length_samples,
6255 tempo_bpm: self.tempo_bpm,
6256 tsig_num: self.tsig_num,
6257 tsig_denom: self.tsig_denom,
6258 automation_lanes,
6259 cancel,
6260 apply_fader,
6261 };
6262 if let Err(e) = worker.tx.send(Message::ProcessOfflineBounce(job)).await {
6263 self.offline_bounce_jobs.remove(&track_name_clone);
6264 self.notify_clients(Err(format!("Failed to schedule offline bounce: {e}")))
6265 .await;
6266 }
6267 return;
6268 }
6269 Action::TrackOfflineBounceCancel { .. } => {}
6270 Action::TrackOfflineBounceCancelAll => {}
6271 Action::TrackOfflineBounceCanceled { .. } => {}
6272 Action::TrackOfflineBounceProgress { .. } => {}
6273 Action::PianoKey {
6274 ref track_name,
6275 note,
6276 velocity,
6277 on,
6278 } => {
6279 if let Some(track) = self.state.lock().tracks.get(track_name) {
6280 let status = if on { 0x90 } else { 0x80 };
6281 let event = MidiEvent::new(0, vec![status, note.min(127), velocity.min(127)]);
6282 track.lock().push_hw_midi_events(&[event]);
6283 }
6284 }
6285 Action::ModifyMidiNotes { .. }
6286 | Action::ModifyMidiControllers { .. }
6287 | Action::DeleteMidiControllers { .. }
6288 | Action::InsertMidiControllers { .. }
6289 | Action::DeleteMidiNotes { .. }
6290 | Action::InsertMidiNotes { .. } => {
6291 if let Err(e) = self.apply_midi_edit_action(&action_to_process) {
6292 self.notify_clients(Err(e)).await;
6293 return;
6294 }
6295 }
6296 Action::SetMidiSysExEvents { .. } => {
6297 if let Err(e) = self.apply_midi_edit_action(&action_to_process) {
6298 self.notify_clients(Err(e)).await;
6299 return;
6300 }
6301 }
6302 Action::TrackClearDefaultPassthrough { ref track_name } => {
6303 if self
6304 .reject_if_track_frozen(track_name, "plugin graph editing")
6305 .await
6306 {
6307 return;
6308 }
6309 let track = match self.track_handle_or_err(track_name) {
6310 Ok(track) => track,
6311 Err(e) => {
6312 self.notify_clients(Err(e)).await;
6313 return;
6314 }
6315 };
6316 track.lock().clear_default_passthrough();
6317 }
6318 Action::TrackClearPlugins { ref track_name } => {
6319 if self
6320 .reject_if_track_frozen(track_name, "plugin graph editing")
6321 .await
6322 {
6323 return;
6324 }
6325 let track = match self.track_handle_or_err(track_name) {
6326 Ok(track) => track,
6327 Err(e) => {
6328 self.notify_clients(Err(e)).await;
6329 return;
6330 }
6331 };
6332 track.lock().clear_plugins();
6333 self.notify_clients(Ok(Action::Log {
6334 source: "engine".to_string(),
6335 message: format!("Cleared plugins from track '{track_name}'"),
6336 }))
6337 .await;
6338 }
6339 #[cfg(all(unix, not(target_os = "macos")))]
6340 Action::ClipSetLv2PluginState { ref track_name, .. } => {
6341 self.notify_clients(Err(format!(
6342 "Track '{}': clip LV2 plugin state changes are not supported",
6343 track_name
6344 )))
6345 .await;
6346 }
6347 Action::TrackGetClapNoteNames { ref track_name } => {
6348 let track = match self.track_handle_or_err(track_name) {
6349 Ok(track) => track,
6350 Err(e) => {
6351 self.notify_clients(Err(e)).await;
6352 return;
6353 }
6354 };
6355 let note_names = track.lock().get_clap_note_names();
6356 self.notify_clients(Ok(Action::TrackClapNoteNames {
6357 track_name: track_name.clone(),
6358 note_names,
6359 }))
6360 .await;
6361 }
6362 Action::TrackGetPluginGraph { ref track_name } => {
6363 let track = match self.track_handle_or_err(track_name) {
6364 Ok(track) => track,
6365 Err(e) => {
6366 self.notify_clients(Err(e)).await;
6367 return;
6368 }
6369 };
6370 let (plugins, connections, connectable_connections) = {
6371 let track = track.lock();
6372 (
6373 track.plugin_graph_plugins(),
6374 track.plugin_graph_connections(),
6375 track.connectable_connections(),
6376 )
6377 };
6378 self.notify_clients(Ok(Action::TrackPluginGraph {
6379 track_name: track_name.clone(),
6380 plugins,
6381 connections,
6382 connectable_connections,
6383 }))
6384 .await;
6385 return;
6386 }
6387 Action::TrackPluginGraph { .. } => {}
6388 Action::TrackConnectPluginAudio {
6389 ref track_name,
6390 ref from_node,
6391 from_port,
6392 ref to_node,
6393 to_port,
6394 } => {
6395 if self
6396 .reject_if_track_frozen(track_name, "plugin routing changes")
6397 .await
6398 {
6399 return;
6400 }
6401 let track = match self.track_handle_or_err(track_name) {
6402 Ok(track) => track,
6403 Err(e) => {
6404 self.notify_clients(Err(e)).await;
6405 return;
6406 }
6407 };
6408 if let Err(e) = track.lock().connect_plugin_audio(
6409 from_node.clone(),
6410 from_port,
6411 to_node.clone(),
6412 to_port,
6413 ) {
6414 self.notify_clients(Err(e)).await;
6415 return;
6416 }
6417 }
6418 Action::TrackConnectPluginMidi {
6419 ref track_name,
6420 ref from_node,
6421 from_port,
6422 ref to_node,
6423 to_port,
6424 } => {
6425 if self
6426 .reject_if_track_frozen(track_name, "plugin routing changes")
6427 .await
6428 {
6429 return;
6430 }
6431 let track = match self.track_handle_or_err(track_name) {
6432 Ok(track) => track,
6433 Err(e) => {
6434 self.notify_clients(Err(e)).await;
6435 return;
6436 }
6437 };
6438 if let Err(e) = track.lock().connect_plugin_midi(
6439 from_node.clone(),
6440 from_port,
6441 to_node.clone(),
6442 to_port,
6443 ) {
6444 self.notify_clients(Err(e)).await;
6445 return;
6446 }
6447 }
6448 Action::TrackDisconnectPluginAudio {
6449 ref track_name,
6450 ref from_node,
6451 from_port,
6452 ref to_node,
6453 to_port,
6454 } => {
6455 if self
6456 .reject_if_track_frozen(track_name, "plugin routing changes")
6457 .await
6458 {
6459 return;
6460 }
6461 let track = match self.track_handle_or_err(track_name) {
6462 Ok(track) => track,
6463 Err(e) => {
6464 self.notify_clients(Err(e)).await;
6465 return;
6466 }
6467 };
6468 if let Err(e) = track.lock().disconnect_plugin_audio(
6469 from_node.clone(),
6470 from_port,
6471 to_node.clone(),
6472 to_port,
6473 ) {
6474 self.notify_clients(Err(e)).await;
6475 return;
6476 }
6477 }
6478 Action::TrackDisconnectPluginMidi {
6479 ref track_name,
6480 ref from_node,
6481 from_port,
6482 ref to_node,
6483 to_port,
6484 } => {
6485 if self
6486 .reject_if_track_frozen(track_name, "plugin routing changes")
6487 .await
6488 {
6489 return;
6490 }
6491 let track = match self.track_handle_or_err(track_name) {
6492 Ok(track) => track,
6493 Err(e) => {
6494 self.notify_clients(Err(e)).await;
6495 return;
6496 }
6497 };
6498 if let Err(e) = track.lock().disconnect_plugin_midi(
6499 from_node.clone(),
6500 from_port,
6501 to_node.clone(),
6502 to_port,
6503 ) {
6504 self.notify_clients(Err(e)).await;
6505 return;
6506 }
6507 }
6508 Action::TrackConnectAudio {
6509 ref track_name,
6510 ref from,
6511 from_port,
6512 ref to,
6513 to_port,
6514 } => {
6515 if self
6516 .reject_if_track_frozen(track_name, "routing changes")
6517 .await
6518 {
6519 return;
6520 }
6521 let track = match self.track_handle_or_err(track_name) {
6522 Ok(track) => track,
6523 Err(e) => {
6524 self.notify_clients(Err(e)).await;
6525 return;
6526 }
6527 };
6528 if let Err(e) = track.lock().connect_audio_connectable(
6529 from.clone(),
6530 from_port,
6531 to.clone(),
6532 to_port,
6533 ) {
6534 self.notify_clients(Err(e)).await;
6535 return;
6536 }
6537 }
6538 Action::TrackDisconnectAudio {
6539 ref track_name,
6540 ref from,
6541 from_port,
6542 ref to,
6543 to_port,
6544 } => {
6545 if self
6546 .reject_if_track_frozen(track_name, "routing changes")
6547 .await
6548 {
6549 return;
6550 }
6551 let track = match self.track_handle_or_err(track_name) {
6552 Ok(track) => track,
6553 Err(e) => {
6554 self.notify_clients(Err(e)).await;
6555 return;
6556 }
6557 };
6558 if let Err(e) = track.lock().disconnect_audio_connectable(
6559 from.clone(),
6560 from_port,
6561 to.clone(),
6562 to_port,
6563 ) {
6564 self.notify_clients(Err(e)).await;
6565 return;
6566 }
6567 }
6568 Action::TrackConnectMidi {
6569 ref track_name,
6570 ref from,
6571 from_port,
6572 ref to,
6573 to_port,
6574 } => {
6575 if self
6576 .reject_if_track_frozen(track_name, "routing changes")
6577 .await
6578 {
6579 return;
6580 }
6581 let track = match self.track_handle_or_err(track_name) {
6582 Ok(track) => track,
6583 Err(e) => {
6584 self.notify_clients(Err(e)).await;
6585 return;
6586 }
6587 };
6588 if let Err(e) = track.lock().connect_midi_connectable(
6589 from.clone(),
6590 from_port,
6591 to.clone(),
6592 to_port,
6593 ) {
6594 self.notify_clients(Err(e)).await;
6595 return;
6596 }
6597 }
6598 Action::TrackDisconnectMidi {
6599 ref track_name,
6600 ref from,
6601 from_port,
6602 ref to,
6603 to_port,
6604 } => {
6605 if self
6606 .reject_if_track_frozen(track_name, "routing changes")
6607 .await
6608 {
6609 return;
6610 }
6611 let track = match self.track_handle_or_err(track_name) {
6612 Ok(track) => track,
6613 Err(e) => {
6614 self.notify_clients(Err(e)).await;
6615 return;
6616 }
6617 };
6618 if let Err(e) = track.lock().disconnect_midi_connectable(
6619 from.clone(),
6620 from_port,
6621 to.clone(),
6622 to_port,
6623 ) {
6624 self.notify_clients(Err(e)).await;
6625 return;
6626 }
6627 }
6628 #[cfg(all(unix, not(target_os = "macos")))]
6629 Action::ListLv2Plugins => {
6630 match crate::plugins::scan_plugins::<crate::plugins::types::Lv2PluginInfo>("lv2") {
6631 Ok(plugins) => {
6632 self.notify_clients(Ok(Action::Lv2Plugins(plugins))).await;
6633 }
6634 Err(e) => {
6635 tracing::error!("LV2 plugin scan failed: {e}");
6636 self.notify_clients(Ok(Action::Lv2PluginsUnavailable { error: e }))
6637 .await;
6638 }
6639 }
6640 return;
6641 }
6642 #[cfg(all(unix, not(target_os = "macos")))]
6643 Action::Lv2Plugins(_) => {}
6644 #[cfg(all(unix, not(target_os = "macos")))]
6645 Action::Lv2PluginsUnavailable { .. } => {}
6646 Action::ListVst3Plugins => {
6647 match crate::plugins::scan_plugins::<crate::plugins::types::Vst3PluginInfo>("vst3")
6648 {
6649 Ok(plugins) => {
6650 self.notify_clients(Ok(Action::Vst3Plugins(plugins))).await;
6651 }
6652 Err(e) => {
6653 tracing::error!("VST3 plugin scan failed: {e}");
6654 self.notify_clients(Ok(Action::Vst3PluginsUnavailable { error: e }))
6655 .await;
6656 }
6657 }
6658 return;
6659 }
6660 Action::Vst3Plugins(_) => {}
6661 Action::Vst3PluginsUnavailable { .. } => {}
6662 Action::ListClapPlugins => {
6663 match crate::plugins::scan_plugins::<crate::plugins::types::ClapPluginInfo>("clap")
6664 {
6665 Ok(plugins) => {
6666 self.notify_clients(Ok(Action::ClapPlugins(plugins))).await;
6667 }
6668 Err(e) => {
6669 tracing::error!("CLAP plugin scan failed: {e}");
6670 self.notify_clients(Ok(Action::ClapPluginsUnavailable { error: e }))
6671 .await;
6672 }
6673 }
6674 return;
6675 }
6676 Action::ListClapPluginsWithCapabilities => {
6677 match crate::plugins::scan_plugins::<crate::plugins::types::ClapPluginInfo>("clap")
6678 {
6679 Ok(plugins) => {
6680 self.notify_clients(Ok(Action::ClapPlugins(plugins))).await;
6681 }
6682 Err(e) => {
6683 tracing::error!("CLAP plugin scan failed: {e}");
6684 self.notify_clients(Ok(Action::ClapPluginsUnavailable { error: e }))
6685 .await;
6686 }
6687 }
6688 return;
6689 }
6690 Action::ClapPlugins(_) => {}
6691 Action::ClapPluginsUnavailable { .. } => {}
6692 Action::TrackLoadClapPlugin {
6693 ref track_name,
6694 ref plugin_path,
6695 instance_id,
6696 } => {
6697 if self
6698 .reject_if_track_frozen(track_name, "CLAP plugin loading")
6699 .await
6700 {
6701 return;
6702 }
6703 let track = match self.track_handle_or_err(track_name) {
6704 Ok(track) => track,
6705 Err(e) => {
6706 self.notify_clients(Err(e)).await;
6707 return;
6708 }
6709 };
6710 let track = track.lock();
6711 if track.audio.processing {
6712 self.notify_clients(Err(format!(
6713 "Track '{}' is currently processing audio; stop playback before loading CLAP plugins",
6714 track_name
6715 )))
6716 .await;
6717 return;
6718 }
6719 if let Err(e) = track.load_clap_plugin(plugin_path, instance_id) {
6720 self.notify_clients(Err(e)).await;
6721 return;
6722 }
6723 self.notify_clients(Ok(Action::Log {
6724 source: "engine".to_string(),
6725 message: format!("CLAP plugin loaded on track '{track_name}': {plugin_path}"),
6726 }))
6727 .await;
6728 if let Some(instance) = track.clap_plugins.last()
6729 && let Some(stderr) = instance.processor.lock().take_stderr()
6730 {
6731 let source = format!("clap:{plugin_path}");
6732 self.spawn_plugin_host_stderr_reader(stderr, source);
6733 self.notify_clients(Ok(Action::Log {
6734 source: "engine".to_string(),
6735 message: format!(
6736 "Attached stderr reader for CLAP plugin on track '{track_name}'"
6737 ),
6738 }))
6739 .await;
6740 }
6741 }
6742 Action::TrackUnloadClapPlugin {
6743 ref track_name,
6744 ref plugin_path,
6745 } => {
6746 if self
6747 .reject_if_track_frozen(track_name, "CLAP plugin unloading")
6748 .await
6749 {
6750 return;
6751 }
6752 let track = match self.track_handle_or_err(track_name) {
6753 Ok(track) => track,
6754 Err(e) => {
6755 self.notify_clients(Err(e)).await;
6756 return;
6757 }
6758 };
6759 let track = track.lock();
6760 if track.audio.processing {
6761 self.notify_clients(Err(format!(
6762 "Track '{}' is currently processing audio; stop playback before unloading CLAP plugins",
6763 track_name
6764 )))
6765 .await;
6766 return;
6767 }
6768 if let Err(e) = track.unload_clap_plugin(plugin_path) {
6769 self.notify_clients(Err(e)).await;
6770 return;
6771 }
6772 }
6773 Action::TrackUnloadClapPluginInstance {
6774 ref track_name,
6775 instance_id,
6776 } => {
6777 if self
6778 .reject_if_track_frozen(track_name, "CLAP plugin unloading")
6779 .await
6780 {
6781 return;
6782 }
6783 let track = match self.track_handle_or_err(track_name) {
6784 Ok(track) => track,
6785 Err(e) => {
6786 self.notify_clients(Err(e)).await;
6787 return;
6788 }
6789 };
6790 let track = track.lock();
6791 if track.audio.processing {
6792 self.notify_clients(Err(format!(
6793 "Track '{}' is currently processing audio; stop playback before unloading CLAP plugins",
6794 track_name
6795 )))
6796 .await;
6797 return;
6798 }
6799 if let Err(e) = track.unload_clap_plugin_instance(instance_id) {
6800 self.notify_clients(Err(e)).await;
6801 return;
6802 }
6803 }
6804 Action::TrackShowClapGui {
6805 ref track_name,
6806 instance_id,
6807 } => {
6808 let track = match self.track_handle_or_err(track_name) {
6809 Ok(track) => track,
6810 Err(e) => {
6811 self.notify_clients(Err(e)).await;
6812 return;
6813 }
6814 };
6815 if let Err(e) = track.lock().show_clap_gui(instance_id) {
6816 self.notify_clients(Err(e)).await;
6817 return;
6818 }
6819 }
6820 Action::TrackLoadVst3Plugin {
6821 ref track_name,
6822 ref plugin_path,
6823 instance_id,
6824 } => {
6825 if self
6826 .reject_if_track_frozen(track_name, "VST3 plugin loading")
6827 .await
6828 {
6829 return;
6830 }
6831 let track = match self.track_handle_or_err(track_name) {
6832 Ok(track) => track,
6833 Err(e) => {
6834 self.notify_clients(Err(e)).await;
6835 return;
6836 }
6837 };
6838 let track = track.lock();
6839 if track.audio.processing {
6840 self.notify_clients(Err(format!(
6841 "Track '{}' is currently processing audio; stop playback before loading VST3 plugins",
6842 track_name
6843 )))
6844 .await;
6845 return;
6846 }
6847 if let Err(e) = track.load_vst3_plugin(plugin_path, instance_id) {
6848 self.notify_clients(Err(e)).await;
6849 return;
6850 }
6851 if let Some(instance) = track.vst3_plugins.last()
6852 && let Some(stderr) = instance.processor.lock().take_stderr()
6853 {
6854 let source = format!("vst3:{plugin_path}");
6855 self.spawn_plugin_host_stderr_reader(stderr, source);
6856 }
6857 }
6858 Action::TrackUnloadVst3Plugin {
6859 ref track_name,
6860 ref plugin_path,
6861 } => {
6862 if self
6863 .reject_if_track_frozen(track_name, "VST3 plugin unloading")
6864 .await
6865 {
6866 return;
6867 }
6868 let track = match self.track_handle_or_err(track_name) {
6869 Ok(track) => track,
6870 Err(e) => {
6871 self.notify_clients(Err(e)).await;
6872 return;
6873 }
6874 };
6875 let track = track.lock();
6876 if track.audio.processing {
6877 self.notify_clients(Err(format!(
6878 "Track '{}' is currently processing audio; stop playback before unloading VST3 plugins",
6879 track_name
6880 )))
6881 .await;
6882 return;
6883 }
6884 if let Err(e) = track.unload_vst3_plugin(plugin_path) {
6885 self.notify_clients(Err(e)).await;
6886 return;
6887 }
6888 }
6889 Action::TrackUnloadVst3PluginInstance {
6890 ref track_name,
6891 instance_id,
6892 } => {
6893 if self
6894 .reject_if_track_frozen(track_name, "VST3 plugin unloading")
6895 .await
6896 {
6897 return;
6898 }
6899 let track = match self.track_handle_or_err(track_name) {
6900 Ok(track) => track,
6901 Err(e) => {
6902 self.notify_clients(Err(e)).await;
6903 return;
6904 }
6905 };
6906 let track = track.lock();
6907 if track.audio.processing {
6908 self.notify_clients(Err(format!(
6909 "Track '{}' is currently processing audio; stop playback before unloading VST3 plugins",
6910 track_name
6911 )))
6912 .await;
6913 return;
6914 }
6915 if let Err(e) = track.unload_vst3_plugin_instance(instance_id) {
6916 self.notify_clients(Err(e)).await;
6917 return;
6918 }
6919 }
6920 Action::TrackShowVst3Gui {
6921 ref track_name,
6922 instance_id,
6923 } => {
6924 let track = match self.track_handle_or_err(track_name) {
6925 Ok(track) => track,
6926 Err(e) => {
6927 self.notify_clients(Err(e)).await;
6928 return;
6929 }
6930 };
6931 if let Err(e) = track.lock().show_vst3_gui(instance_id) {
6932 self.notify_clients(Err(e)).await;
6933 return;
6934 }
6935 }
6936 #[cfg(all(unix, not(target_os = "macos")))]
6937 Action::TrackLoadLv2Plugin {
6938 ref track_name,
6939 ref plugin_uri,
6940 instance_id,
6941 } => {
6942 if self
6943 .reject_if_track_frozen(track_name, "LV2 plugin loading")
6944 .await
6945 {
6946 return;
6947 }
6948 let track = match self.track_handle_or_err(track_name) {
6949 Ok(track) => track,
6950 Err(e) => {
6951 self.notify_clients(Err(e)).await;
6952 return;
6953 }
6954 };
6955 let track = track.lock();
6956 if track.audio.processing {
6957 self.notify_clients(Err(format!(
6958 "Track '{}' is currently processing audio; stop playback before loading LV2 plugins",
6959 track_name
6960 )))
6961 .await;
6962 return;
6963 }
6964 if let Err(e) = track.load_lv2_plugin(plugin_uri, instance_id) {
6965 self.notify_clients(Err(e)).await;
6966 return;
6967 }
6968 if let Some(instance) = track.lv2_plugins.last()
6969 && let Some(stderr) = instance.processor.lock().take_stderr()
6970 {
6971 let source = format!("lv2:{plugin_uri}");
6972 self.spawn_plugin_host_stderr_reader(stderr, source);
6973 }
6974 }
6975 #[cfg(all(unix, not(target_os = "macos")))]
6976 Action::TrackUnloadLv2Plugin {
6977 ref track_name,
6978 ref plugin_uri,
6979 } => {
6980 if self
6981 .reject_if_track_frozen(track_name, "LV2 plugin unloading")
6982 .await
6983 {
6984 return;
6985 }
6986 let track = match self.track_handle_or_err(track_name) {
6987 Ok(track) => track,
6988 Err(e) => {
6989 self.notify_clients(Err(e)).await;
6990 return;
6991 }
6992 };
6993 let track = track.lock();
6994 if track.audio.processing {
6995 self.notify_clients(Err(format!(
6996 "Track '{}' is currently processing audio; stop playback before unloading LV2 plugins",
6997 track_name
6998 )))
6999 .await;
7000 return;
7001 }
7002 if let Err(e) = track.unload_lv2_plugin(plugin_uri) {
7003 self.notify_clients(Err(e)).await;
7004 return;
7005 }
7006 }
7007 #[cfg(all(unix, not(target_os = "macos")))]
7008 Action::TrackUnloadLv2PluginInstance {
7009 ref track_name,
7010 instance_id,
7011 } => {
7012 if self
7013 .reject_if_track_frozen(track_name, "LV2 plugin unloading")
7014 .await
7015 {
7016 return;
7017 }
7018 let track = match self.track_handle_or_err(track_name) {
7019 Ok(track) => track,
7020 Err(e) => {
7021 self.notify_clients(Err(e)).await;
7022 return;
7023 }
7024 };
7025 let track = track.lock();
7026 if track.audio.processing {
7027 self.notify_clients(Err(format!(
7028 "Track '{}' is currently processing audio; stop playback before unloading LV2 plugins",
7029 track_name
7030 )))
7031 .await;
7032 return;
7033 }
7034 if let Err(e) = track.unload_lv2_plugin_instance(instance_id) {
7035 self.notify_clients(Err(e)).await;
7036 return;
7037 }
7038 }
7039 #[cfg(all(unix, not(target_os = "macos")))]
7040 Action::TrackShowLv2Gui {
7041 ref track_name,
7042 instance_id,
7043 } => {
7044 let track = match self.track_handle_or_err(track_name) {
7045 Ok(track) => track,
7046 Err(e) => {
7047 self.notify_clients(Err(e)).await;
7048 return;
7049 }
7050 };
7051 if let Err(e) = track.lock().show_lv2_gui(instance_id) {
7052 self.notify_clients(Err(e)).await;
7053 return;
7054 }
7055 }
7056 Action::TrackSetPluginResourceDir {
7057 ref track_name,
7058 instance_id,
7059 ref format,
7060 ref directory,
7061 } => {
7062 let track = match self.track_handle_or_err(track_name) {
7063 Ok(track) => track,
7064 Err(e) => {
7065 self.notify_clients(Err(e)).await;
7066 return;
7067 }
7068 };
7069 let dir = std::path::Path::new(directory);
7070 let result = if format.eq_ignore_ascii_case("CLAP") {
7071 track.lock().set_clap_plugin_resource_dir(instance_id, dir)
7072 } else if format.eq_ignore_ascii_case("LV2") {
7073 #[cfg(all(unix, not(target_os = "macos")))]
7074 {
7075 track.lock().set_lv2_plugin_resource_dir(instance_id, dir)
7076 }
7077 #[cfg(not(all(unix, not(target_os = "macos"))))]
7078 Err("LV2 is not supported on this platform".to_string())
7079 } else {
7080 Err(format!(
7081 "Unsupported plugin format for resource dir: {format}"
7082 ))
7083 };
7084 if let Err(e) = result {
7085 self.notify_clients(Err(e)).await;
7086 return;
7087 }
7088 }
7089 Action::TrackClapFileReferences {
7090 ref track_name,
7091 instance_id,
7092 refs: _,
7093 } => match self.track_handle_or_err(track_name) {
7094 Ok(track) => {
7095 let refs = track.lock().clap_file_references(instance_id).unwrap_or_else(|e| {
7096 tracing::warn!(track_name = %track_name, instance_id, error = %e, "Failed to enumerate CLAP file references");
7097 Vec::new()
7098 });
7099 self.notify_clients(Ok(Action::TrackClapFileReferences {
7100 track_name: track_name.clone(),
7101 instance_id,
7102 refs,
7103 }))
7104 .await;
7105 }
7106 Err(e) => {
7107 self.notify_clients(Err(e)).await;
7108 }
7109 },
7110 Action::TrackUpdateClapFileReference {
7111 ref track_name,
7112 instance_id,
7113 index,
7114 ref path,
7115 } => {
7116 let track = match self.track_handle_or_err(track_name) {
7117 Ok(track) => track,
7118 Err(e) => {
7119 self.notify_clients(Err(e)).await;
7120 return;
7121 }
7122 };
7123 if let Err(e) = track
7124 .lock()
7125 .update_clap_file_reference(instance_id, index, path)
7126 {
7127 self.notify_clients(Err(e)).await;
7128 return;
7129 }
7130 }
7131 Action::ClipSetPluginResourceDir {
7132 ref track_name,
7133 clip_idx,
7134 instance_id,
7135 ref format,
7136 ref directory,
7137 } => {
7138 let track = match self.track_handle_or_err(track_name) {
7139 Ok(track) => track,
7140 Err(e) => {
7141 self.notify_clients(Err(e)).await;
7142 return;
7143 }
7144 };
7145 let dir = std::path::Path::new(directory);
7146 let track = track.lock();
7147 let result = if format.eq_ignore_ascii_case("CLAP") {
7148 track.clip_set_clap_plugin_resource_dir(clip_idx, instance_id, dir)
7149 } else if format.eq_ignore_ascii_case("LV2") {
7150 #[cfg(all(unix, not(target_os = "macos")))]
7151 {
7152 track.clip_set_lv2_plugin_resource_dir(clip_idx, instance_id, dir)
7153 }
7154 #[cfg(not(all(unix, not(target_os = "macos"))))]
7155 Err("LV2 is not supported on this platform".to_string())
7156 } else {
7157 Err(format!(
7158 "Unsupported plugin format for resource dir: {format}"
7159 ))
7160 };
7161 if let Err(e) = result {
7162 self.notify_clients(Err(e)).await;
7163 return;
7164 }
7165 }
7166 Action::ClipClapFileReferences {
7167 ref track_name,
7168 clip_idx,
7169 instance_id,
7170 refs: _,
7171 } => match self.track_handle_or_err(track_name) {
7172 Ok(track) => {
7173 let track = track.lock();
7174 let refs = track
7175 .clip_clap_file_references(clip_idx, instance_id)
7176 .unwrap_or_else(|e| {
7177 tracing::warn!(
7178 track_name = %track_name,
7179 clip_idx,
7180 instance_id,
7181 error = %e,
7182 "Failed to enumerate clip CLAP file references"
7183 );
7184 Vec::new()
7185 });
7186 self.notify_clients(Ok(Action::ClipClapFileReferences {
7187 track_name: track_name.clone(),
7188 clip_idx,
7189 instance_id,
7190 refs,
7191 }))
7192 .await;
7193 }
7194 Err(e) => {
7195 self.notify_clients(Err(e)).await;
7196 }
7197 },
7198 Action::ClipUpdateClapFileReference {
7199 ref track_name,
7200 clip_idx,
7201 instance_id,
7202 index,
7203 ref path,
7204 } => {
7205 let track = match self.track_handle_or_err(track_name) {
7206 Ok(track) => track,
7207 Err(e) => {
7208 self.notify_clients(Err(e)).await;
7209 return;
7210 }
7211 };
7212 if let Err(e) =
7213 track
7214 .lock()
7215 .clip_update_clap_file_reference(clip_idx, instance_id, index, path)
7216 {
7217 self.notify_clients(Err(e)).await;
7218 return;
7219 }
7220 }
7221 Action::TrackSetClapParameter {
7222 ref track_name,
7223 instance_id,
7224 param_id,
7225 value,
7226 } => {
7227 if self
7228 .reject_if_track_frozen(track_name, "CLAP parameter changes")
7229 .await
7230 {
7231 return;
7232 }
7233 match self.track_handle_or_err(track_name) {
7234 Ok(track) => {
7235 if let Err(e) =
7236 track
7237 .lock()
7238 .set_clap_parameter(instance_id, param_id, value)
7239 {
7240 self.notify_clients(Err(e)).await;
7241 return;
7242 }
7243 self.notify_clients(Ok(a.clone())).await;
7244 }
7245 Err(e) => {
7246 self.notify_clients(Err(e)).await;
7247 }
7248 }
7249 }
7250 Action::ClipSetClapParameter {
7251 ref track_name,
7252 clip_idx,
7253 instance_id,
7254 param_id,
7255 value,
7256 } => {
7257 if self
7258 .reject_if_track_frozen(track_name, "CLAP parameter changes")
7259 .await
7260 {
7261 return;
7262 }
7263 match self.track_handle_or_err(track_name) {
7264 Ok(track) => {
7265 if let Err(e) = track.lock().clip_set_clap_parameter(
7266 clip_idx,
7267 instance_id,
7268 param_id,
7269 value,
7270 ) {
7271 self.notify_clients(Err(e)).await;
7272 return;
7273 }
7274 self.notify_clients(Ok(a.clone())).await;
7275 }
7276 Err(e) => {
7277 self.notify_clients(Err(e)).await;
7278 }
7279 }
7280 }
7281 Action::TrackSetClapParameterAt {
7282 ref track_name,
7283 instance_id,
7284 param_id,
7285 value,
7286 frame,
7287 } => {
7288 if self
7289 .reject_if_track_frozen(track_name, "CLAP parameter changes")
7290 .await
7291 {
7292 return;
7293 }
7294 match self.track_handle_or_err(track_name) {
7295 Ok(track) => {
7296 if let Err(e) =
7297 track
7298 .lock()
7299 .set_clap_parameter_at(instance_id, param_id, value, frame)
7300 {
7301 self.notify_clients(Err(e)).await;
7302 return;
7303 }
7304 self.notify_clients(Ok(a.clone())).await;
7305 }
7306 Err(e) => {
7307 self.notify_clients(Err(e)).await;
7308 }
7309 }
7310 }
7311 Action::TrackBeginClapParameterEdit {
7312 ref track_name,
7313 instance_id,
7314 param_id,
7315 frame,
7316 } => {
7317 if self
7318 .reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
7319 .await
7320 {
7321 return;
7322 }
7323 match self.track_handle_or_err(track_name) {
7324 Ok(track) => {
7325 if let Err(e) =
7326 track
7327 .lock()
7328 .begin_clap_parameter_edit(instance_id, param_id, frame)
7329 {
7330 self.notify_clients(Err(e)).await;
7331 return;
7332 }
7333 self.notify_clients(Ok(a.clone())).await;
7334 }
7335 Err(e) => {
7336 self.notify_clients(Err(e)).await;
7337 }
7338 }
7339 }
7340 Action::TrackEndClapParameterEdit {
7341 ref track_name,
7342 instance_id,
7343 param_id,
7344 frame,
7345 } => {
7346 if self
7347 .reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
7348 .await
7349 {
7350 return;
7351 }
7352 match self.track_handle_or_err(track_name) {
7353 Ok(track) => {
7354 if let Err(e) =
7355 track
7356 .lock()
7357 .end_clap_parameter_edit(instance_id, param_id, frame)
7358 {
7359 self.notify_clients(Err(e)).await;
7360 return;
7361 }
7362 self.notify_clients(Ok(a.clone())).await;
7363 }
7364 Err(e) => {
7365 self.notify_clients(Err(e)).await;
7366 }
7367 }
7368 }
7369 Action::TrackGetClapParameters {
7370 ref track_name,
7371 instance_id,
7372 } => match self.track_handle_or_err(track_name) {
7373 Ok(track) => match track.lock().get_clap_parameters(instance_id) {
7374 Ok(parameters) => {
7375 self.notify_clients(Ok(Action::TrackClapParameters {
7376 track_name: track_name.clone(),
7377 instance_id,
7378 parameters,
7379 }))
7380 .await;
7381 }
7382 Err(e) => {
7383 self.notify_clients(Err(e)).await;
7384 }
7385 },
7386 Err(e) => {
7387 self.notify_clients(Err(e)).await;
7388 }
7389 },
7390 Action::TrackClapParameters { .. } => {}
7391 Action::TrackClapSnapshotState {
7392 ref track_name,
7393 instance_id,
7394 } => match self.track_handle_or_err(track_name) {
7395 Ok(track) => {
7396 let plugin_path = track
7397 .lock()
7398 .clap_plugins
7399 .iter()
7400 .find(|instance| instance.id == instance_id)
7401 .map(|instance| instance.processor.lock().path().to_string())
7402 .unwrap_or_default();
7403 match track.lock().clap_snapshot_state(instance_id) {
7404 Ok(state) => {
7405 self.notify_clients(Ok(Action::TrackClapStateSnapshot {
7406 track_name: track_name.clone(),
7407 instance_id,
7408 plugin_path,
7409 state,
7410 }))
7411 .await;
7412 }
7413 Err(e) => {
7414 self.notify_clients(Err(e)).await;
7415 }
7416 }
7417 }
7418 Err(e) => {
7419 self.notify_clients(Err(e)).await;
7420 }
7421 },
7422 Action::ClipClapSnapshotState {
7423 ref track_name,
7424 clip_idx,
7425 instance_id,
7426 } => match self.track_handle_or_err(track_name) {
7427 Ok(track) => match track.lock().clip_clap_snapshot_state(clip_idx, instance_id) {
7428 Ok((plugin_path, state)) => {
7429 self.notify_clients(Ok(Action::ClipClapStateSnapshot {
7430 track_name: track_name.clone(),
7431 clip_idx,
7432 instance_id,
7433 plugin_path,
7434 state,
7435 }))
7436 .await;
7437 }
7438 Err(e) => {
7439 self.notify_clients(Err(e)).await;
7440 }
7441 },
7442 Err(e) => {
7443 self.notify_clients(Err(e)).await;
7444 }
7445 },
7446 Action::TrackClapStateSnapshot { .. } => {}
7447 Action::ClipClapStateSnapshot { .. } => {}
7448 Action::TrackClapStateDirty { .. } => {}
7449 Action::ClipClapStateDirty { .. } => {}
7450 Action::TrackClapRestoreState {
7451 ref track_name,
7452 instance_id,
7453 ref state,
7454 } => {
7455 if self
7456 .reject_if_track_frozen(track_name, "CLAP state restore")
7457 .await
7458 {
7459 return;
7460 }
7461 let track = match self.track_handle_or_err(track_name) {
7462 Ok(track) => track,
7463 Err(e) => {
7464 self.notify_clients(Err(e)).await;
7465 return;
7466 }
7467 };
7468 let track = track.lock();
7469 if track.audio.processing {
7470 self.notify_clients(Err(format!(
7471 "Track '{}' is currently processing audio; stop playback before restoring CLAP state",
7472 track_name
7473 )))
7474 .await;
7475 return;
7476 }
7477 if let Err(e) = track.clap_restore_state(instance_id, state) {
7478 self.notify_clients(Err(e)).await;
7479 return;
7480 }
7481 }
7482 Action::ClipClapRestoreState {
7483 ref track_name,
7484 clip_idx,
7485 instance_id,
7486 ref state,
7487 } => {
7488 if self
7489 .reject_if_track_frozen(track_name, "CLAP state restore")
7490 .await
7491 {
7492 return;
7493 }
7494 let track = match self.track_handle_or_err(track_name) {
7495 Ok(track) => track,
7496 Err(e) => {
7497 self.notify_clients(Err(e)).await;
7498 return;
7499 }
7500 };
7501 let track = track.lock();
7502 if track.audio.processing {
7503 self.notify_clients(Err(format!(
7504 "Track '{}' is currently processing audio; stop playback before restoring CLAP state",
7505 track_name
7506 )))
7507 .await;
7508 return;
7509 }
7510 if let Err(e) = track.clip_clap_restore_state(clip_idx, instance_id, state) {
7511 self.notify_clients(Err(e)).await;
7512 return;
7513 }
7514 }
7515 Action::TrackSnapshotAllClapStates { ref track_name } => {
7516 let track = match self.track_handle_or_err(track_name) {
7517 Ok(track) => track,
7518 Err(e) => {
7519 self.notify_clients(Err(e)).await;
7520 return;
7521 }
7522 };
7523 let instances: Vec<_> = {
7524 let locked = track.lock();
7525 locked
7526 .clap_plugins
7527 .iter()
7528 .map(|i| (i.id, i.processor.lock().path().to_string()))
7529 .collect()
7530 };
7531 for (instance_id, plugin_path) in instances {
7532 match track.lock().clap_snapshot_state(instance_id) {
7533 Ok(state) => {
7534 self.notify_clients(Ok(Action::TrackClapStateSnapshot {
7535 track_name: track_name.clone(),
7536 instance_id,
7537 plugin_path,
7538 state,
7539 }))
7540 .await;
7541 }
7542 Err(_e) => {}
7543 }
7544 }
7545 self.notify_clients(Ok(Action::TrackSnapshotAllClapStatesDone {
7546 track_name: track_name.clone(),
7547 }))
7548 .await;
7549 }
7550 Action::TrackSnapshotAllClapStatesDone { .. } => {}
7551 Action::TrackGetVst3Graph { ref track_name } => {
7552 match self.track_handle_or_err(track_name) {
7553 Ok(track) => {
7554 let t = track.lock();
7555 let plugins = t.vst3_graph_plugins();
7556 let connections = t.vst3_graph_connections();
7557 self.notify_clients(Ok(Action::TrackVst3Graph {
7558 track_name: track_name.clone(),
7559 plugins,
7560 connections,
7561 }))
7562 .await;
7563 }
7564 Err(e) => {
7565 self.notify_clients(Err(e)).await;
7566 }
7567 }
7568 }
7569 Action::TrackVst3Graph { .. } => {}
7570 Action::TrackSetVst3Parameter {
7571 ref track_name,
7572 instance_id,
7573 param_id,
7574 value,
7575 } => {
7576 if self
7577 .reject_if_track_frozen(track_name, "VST3 parameter changes")
7578 .await
7579 {
7580 return;
7581 }
7582 match self.track_handle_or_err(track_name) {
7583 Ok(track) => {
7584 if let Err(e) =
7585 track
7586 .lock()
7587 .set_vst3_parameter(instance_id, param_id, value)
7588 {
7589 self.notify_clients(Err(e)).await;
7590 return;
7591 }
7592 self.notify_clients(Ok(a.clone())).await;
7593 }
7594 Err(e) => {
7595 self.notify_clients(Err(e)).await;
7596 }
7597 }
7598 }
7599 Action::TrackSetPluginBypassed {
7600 ref track_name,
7601 instance_id,
7602 ref format,
7603 bypassed,
7604 } => match self.track_handle_or_err(track_name) {
7605 Ok(track) => {
7606 let result = match format.as_str() {
7607 "CLAP" => track.lock().set_clap_plugin_bypassed(instance_id, bypassed),
7608 "VST3" => track.lock().set_vst3_plugin_bypassed(instance_id, bypassed),
7609 #[cfg(all(unix, not(target_os = "macos")))]
7610 "LV2" => track.lock().set_lv2_plugin_bypassed(instance_id, bypassed),
7611 _ => Err(format!("Unknown plugin format for bypass: {format}")),
7612 };
7613 if let Err(e) = result {
7614 self.notify_clients(Err(e)).await;
7615 return;
7616 }
7617 self.notify_clients(Ok(a.clone())).await;
7618 }
7619 Err(e) => {
7620 self.notify_clients(Err(e)).await;
7621 }
7622 },
7623 Action::TrackGetVst3Parameters {
7624 ref track_name,
7625 instance_id,
7626 } => match self.track_handle_or_err(track_name) {
7627 Ok(track) => match track.lock().get_vst3_parameters(instance_id) {
7628 Ok(parameters) => {
7629 self.notify_clients(Ok(Action::TrackVst3Parameters {
7630 track_name: track_name.clone(),
7631 instance_id,
7632 parameters,
7633 }))
7634 .await;
7635 }
7636 Err(e) => {
7637 self.notify_clients(Err(e)).await;
7638 }
7639 },
7640 Err(e) => {
7641 self.notify_clients(Err(e)).await;
7642 }
7643 },
7644 Action::TrackVst3Parameters { .. } => {}
7645 Action::TrackVst3SnapshotState {
7646 ref track_name,
7647 instance_id,
7648 } => match self.track_handle_or_err(track_name) {
7649 Ok(track) => match track.lock().vst3_snapshot_state(instance_id) {
7650 Ok(state) => {
7651 self.notify_clients(Ok(Action::TrackVst3StateSnapshot {
7652 track_name: track_name.clone(),
7653 instance_id,
7654 state,
7655 }))
7656 .await;
7657 }
7658 Err(e) => {
7659 self.notify_clients(Err(e)).await;
7660 }
7661 },
7662 Err(e) => {
7663 self.notify_clients(Err(e)).await;
7664 }
7665 },
7666 Action::ClipVst3SnapshotState {
7667 ref track_name,
7668 clip_idx,
7669 instance_id,
7670 } => match self.track_handle_or_err(track_name) {
7671 Ok(track) => match track.lock().clip_vst3_snapshot_state(clip_idx, instance_id) {
7672 Ok(state) => {
7673 self.notify_clients(Ok(Action::ClipVst3StateSnapshot {
7674 track_name: track_name.clone(),
7675 clip_idx,
7676 instance_id,
7677 state,
7678 }))
7679 .await;
7680 }
7681 Err(e) => {
7682 self.notify_clients(Err(e)).await;
7683 }
7684 },
7685 Err(e) => {
7686 self.notify_clients(Err(e)).await;
7687 }
7688 },
7689 Action::TrackVst3StateSnapshot { .. } => {}
7690 Action::ClipVst3StateSnapshot { .. } => {}
7691 Action::TrackVst3RestoreState {
7692 ref track_name,
7693 instance_id,
7694 ref state,
7695 } => match self.track_handle_or_err(track_name) {
7696 Ok(track) => {
7697 if let Err(e) = track.lock().vst3_restore_state(instance_id, state) {
7698 self.notify_clients(Err(e)).await;
7699 return;
7700 }
7701 self.notify_clients(Ok(a.clone())).await;
7702 }
7703 Err(e) => {
7704 self.notify_clients(Err(e)).await;
7705 }
7706 },
7707 Action::TrackConnectVst3Audio {
7708 ref track_name,
7709 ref from_node,
7710 from_port,
7711 ref to_node,
7712 to_port,
7713 } => {
7714 if self
7715 .reject_if_track_frozen(track_name, "VST3 routing changes")
7716 .await
7717 {
7718 return;
7719 }
7720 match self.track_handle_or_err(track_name) {
7721 Ok(track) => {
7722 if let Err(e) = track
7723 .lock()
7724 .connect_vst3_audio(from_node, from_port, to_node, to_port)
7725 {
7726 self.notify_clients(Err(e)).await;
7727 return;
7728 }
7729 self.notify_clients(Ok(a.clone())).await;
7730 }
7731 Err(e) => {
7732 self.notify_clients(Err(e)).await;
7733 }
7734 }
7735 }
7736 Action::TrackDisconnectVst3Audio {
7737 ref track_name,
7738 ref from_node,
7739 from_port,
7740 ref to_node,
7741 to_port,
7742 } => {
7743 if self
7744 .reject_if_track_frozen(track_name, "VST3 routing changes")
7745 .await
7746 {
7747 return;
7748 }
7749 match self.track_handle_or_err(track_name) {
7750 Ok(track) => {
7751 if let Err(e) = track
7752 .lock()
7753 .disconnect_vst3_audio(from_node, from_port, to_node, to_port)
7754 {
7755 self.notify_clients(Err(e)).await;
7756 return;
7757 }
7758 self.notify_clients(Ok(a.clone())).await;
7759 }
7760 Err(e) => {
7761 self.notify_clients(Err(e)).await;
7762 }
7763 }
7764 }
7765 Action::ClipMove {
7766 ref kind,
7767 ref from,
7768 ref to,
7769 copy,
7770 } => {
7771 if let Some(from_track_handle) = self.state.lock().tracks.get(&from.track_name)
7772 && let Some(to_track_handle) = self.state.lock().tracks.get(&to.track_name)
7773 {
7774 let from_track = from_track_handle.lock();
7775 let to_track = to_track_handle.lock();
7776 match kind {
7777 Kind::Audio => {
7778 if from.clip_index >= from_track.audio.clips.len() {
7779 self.notify_clients(Err(format!(
7780 "Clip index {} is too high, as track {} has only {} clips!",
7781 from.clip_index,
7782 from_track.name.clone(),
7783 from_track.audio.clips.len(),
7784 )))
7785 .await;
7786 return;
7787 }
7788 if from_track.audio.ins.len() != to_track.audio.ins.len() {
7789 self.notify_clients(Err(format!(
7790 "Cannot move/copy audio clip from '{}' ({} inputs) to '{}' ({} inputs)",
7791 from_track.name,
7792 from_track.audio.ins.len(),
7793 to_track.name,
7794 to_track.audio.ins.len()
7795 )))
7796 .await;
7797 return;
7798 }
7799 let clip_copy = from_track.audio.clips[from.clip_index].clone();
7800 if !copy {
7801 from_track.audio.clips.remove(from.clip_index);
7802 }
7803 let mut clip_copy = clip_copy;
7804 clip_copy.start = to.sample_offset;
7805 let max_lane = to_track.audio.ins.len().saturating_sub(1);
7806 clip_copy.input_channel = to.input_channel.min(max_lane);
7807 to_track.audio.clips.push(clip_copy);
7808 }
7809 Kind::MIDI => {
7810 if from.clip_index >= from_track.midi.clips.len() {
7811 self.notify_clients(Err(format!(
7812 "Clip index {} is too high, as track {} has only {} clips!",
7813 from.clip_index,
7814 from_track.name.clone(),
7815 from_track.midi.clips.len(),
7816 )))
7817 .await;
7818 return;
7819 }
7820 let clip_copy = from_track.midi.clips[from.clip_index].clone();
7821 if !copy {
7822 from_track.midi.clips.remove(from.clip_index);
7823 }
7824 let mut clip_copy = clip_copy;
7825 clip_copy.start = to.sample_offset;
7826 let max_lane = to_track.midi.ins.len().saturating_sub(1);
7827 clip_copy.input_channel = to.input_channel.min(max_lane);
7828 to_track.midi.clips.push(clip_copy);
7829 }
7830 }
7831 }
7832 }
7833 Action::AddClip {
7834 ref name,
7835 ref track_name,
7836 start,
7837 length,
7838 offset,
7839 input_channel,
7840 muted,
7841 ref peaks_file,
7842 kind,
7843 fade_enabled,
7844 fade_in_samples,
7845 fade_out_samples,
7846 ref source_name,
7847 source_offset,
7848 source_length,
7849 ref preview_name,
7850 ref pitch_correction_points,
7851 pitch_correction_frame_likeness,
7852 pitch_correction_inertia_ms,
7853 pitch_correction_formant_compensation,
7854 ref plugin_graph_json,
7855 } => {
7856 self.add_clip_to_track(ClipAddRequest {
7857 name,
7858 track_name,
7859 start,
7860 length,
7861 offset,
7862 input_channel,
7863 muted,
7864 peaks_file: peaks_file.clone(),
7865 kind,
7866 fade_enabled,
7867 fade_in_samples,
7868 fade_out_samples,
7869 source_name: source_name.clone(),
7870 source_offset,
7871 source_length,
7872 preview_name: preview_name.clone(),
7873 pitch_correction_points: pitch_correction_points.clone(),
7874 pitch_correction_frame_likeness,
7875 pitch_correction_inertia_ms,
7876 pitch_correction_formant_compensation,
7877 plugin_graph_json: plugin_graph_json.clone(),
7878 });
7879 if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
7880 let track_name = track_name.clone();
7881 tokio::task::spawn_blocking(move || {
7882 track.lock().preload_clips();
7883 tracing::debug!("Preloaded clips for track '{}' after AddClip", track_name);
7884 });
7885 }
7886 }
7887 Action::AddGroupedClip {
7888 ref track_name,
7889 kind,
7890 ref audio_clip,
7891 ref midi_clip,
7892 } => {
7893 self.add_grouped_clip_to_track(
7894 track_name,
7895 kind,
7896 audio_clip.clone(),
7897 midi_clip.clone(),
7898 );
7899 if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
7900 let track_name = track_name.clone();
7901 tokio::task::spawn_blocking(move || {
7902 track.lock().preload_clips();
7903 tracing::debug!(
7904 "Preloaded clips for track '{}' after AddGroupedClip",
7905 track_name
7906 );
7907 });
7908 }
7909 }
7910 Action::RemoveClip {
7911 ref track_name,
7912 kind,
7913 ref clip_indices,
7914 } => {
7915 self.remove_clips_from_track(track_name, kind, clip_indices);
7916 }
7917 Action::RenameClip {
7918 ref track_name,
7919 kind,
7920 clip_index,
7921 ref new_name,
7922 } => {
7923 self.rename_clip_references(track_name, kind, clip_index, new_name);
7924 }
7925 Action::SetClipSourceName {
7926 ref track_name,
7927 kind,
7928 clip_index,
7929 ref name,
7930 } => {
7931 self.set_clip_source_name(track_name, clip_index, kind, name.clone());
7932 }
7933 Action::SetClipFade {
7934 ref track_name,
7935 clip_index,
7936 kind,
7937 fade_enabled,
7938 fade_in_samples,
7939 fade_out_samples,
7940 } => {
7941 self.set_clip_fade(
7942 track_name,
7943 clip_index,
7944 kind,
7945 fade_enabled,
7946 fade_in_samples,
7947 fade_out_samples,
7948 );
7949 }
7950 Action::SetClipBounds {
7951 ref track_name,
7952 clip_index,
7953 kind,
7954 start,
7955 length,
7956 offset,
7957 } => {
7958 self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
7959 }
7960 Action::SyncClipBounds {
7961 ref track_name,
7962 clip_index,
7963 kind,
7964 start,
7965 length,
7966 offset,
7967 } => {
7968 self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
7969 }
7970 Action::SetClipMuted {
7971 ref track_name,
7972 clip_index,
7973 kind,
7974 muted,
7975 } => {
7976 self.set_clip_muted(track_name, clip_index, kind, muted);
7977 }
7978 Action::SetClipPluginGraphJson {
7979 ref track_name,
7980 clip_index,
7981 ref plugin_graph_json,
7982 } => {
7983 self.set_clip_plugin_graph_json(track_name, clip_index, plugin_graph_json.clone());
7984 }
7985 Action::SetClipPitchCorrection {
7986 ref track_name,
7987 clip_index,
7988 ref preview_name,
7989 ref source_name,
7990 source_offset,
7991 source_length,
7992 ref pitch_correction_points,
7993 pitch_correction_frame_likeness,
7994 pitch_correction_inertia_ms,
7995 pitch_correction_formant_compensation,
7996 } => {
7997 self.set_clip_pitch_correction(
7998 track_name,
7999 clip_index,
8000 preview_name.clone(),
8001 source_name.clone(),
8002 source_offset,
8003 source_length,
8004 pitch_correction_points.clone(),
8005 pitch_correction_frame_likeness,
8006 pitch_correction_inertia_ms,
8007 pitch_correction_formant_compensation,
8008 );
8009 }
8010 Action::Connect {
8011 ref from_track,
8012 from_port,
8013 ref to_track,
8014 to_port,
8015 kind,
8016 } => {
8017 match kind {
8018 Kind::Audio => {
8019 let from_audio_io = if from_track == "hw:in" {
8020 self.hw_input_audio_port(from_port)
8021 } else {
8022 self.state
8023 .lock()
8024 .tracks
8025 .get(from_track)
8026 .and_then(|t| t.lock().audio.outs.get(from_port).cloned())
8027 };
8028 let to_audio_io = if to_track == "hw:out" {
8029 self.hw_output_audio_port(to_port)
8030 } else {
8031 self.state
8032 .lock()
8033 .tracks
8034 .get(to_track)
8035 .and_then(|t| t.lock().audio.ins.get(to_port).cloned())
8036 };
8037 match (from_audio_io, to_audio_io) {
8038 (Some(source), Some(target)) => {
8039 if from_track != "hw:in"
8040 && to_track != "hw:out"
8041 && self.check_if_leads_to_kind(
8042 Kind::Audio,
8043 to_track,
8044 from_track,
8045 )
8046 {
8047 self.notify_clients(Err(
8048 "Circular routing is not allowed!".into()
8049 ))
8050 .await;
8051 return;
8052 }
8053 crate::audio::io::AudioIO::connect(&source, &target);
8054 }
8055 (None, _) => {
8056 self.notify_clients(Err(format!(
8057 "Source track '{}' not found",
8058 from_track
8059 )))
8060 .await;
8061 return;
8062 }
8063 (_, None) => {
8064 self.notify_clients(Err(format!(
8065 "Destination track '{}' not found",
8066 to_track
8067 )))
8068 .await;
8069 return;
8070 }
8071 }
8072 }
8073 Kind::MIDI => {
8074 let from_hw_in_device = Self::midi_hw_in_device(from_track);
8075 let to_hw_out_device = Self::midi_hw_out_device(to_track);
8076 let from_is_invalid_hw = Self::midi_hw_out_device(from_track).is_some();
8077 let to_is_invalid_hw = Self::midi_hw_in_device(to_track).is_some();
8078
8079 if from_is_invalid_hw || to_is_invalid_hw {
8080 self.notify_clients(Err(
8081 "Invalid MIDI hardware connection direction".to_string()
8082 ))
8083 .await;
8084 return;
8085 }
8086
8087 if from_hw_in_device.is_none()
8088 && to_hw_out_device.is_none()
8089 && self.check_if_leads_to_kind(Kind::MIDI, to_track, from_track)
8090 {
8091 self.notify_clients(Err("Circular routing is not allowed!".into()))
8092 .await;
8093 return;
8094 }
8095
8096 let state = self.state.lock();
8097 let from_track_handle = state.tracks.get(from_track);
8098 let to_track_handle = state.tracks.get(to_track);
8099
8100 if let (Some(from_device), Some(to_device)) =
8101 (from_hw_in_device, to_hw_out_device)
8102 {
8103 let route = MidiHwThruRoute {
8104 from_device: from_device.to_string(),
8105 to_device: to_device.to_string(),
8106 };
8107 if !self.midi_hw_thru_routes.iter().any(|r| r == &route) {
8108 self.midi_hw_thru_routes.push(route);
8109 }
8110 } else if let Some(device) = from_hw_in_device {
8111 if let Some(t_t) = to_track_handle {
8112 if t_t.lock().midi.ins.get(to_port).is_none() {
8113 self.notify_clients(Err(format!(
8114 "MIDI input port {} not found on track '{}'",
8115 to_port, to_track
8116 )))
8117 .await;
8118 return;
8119 }
8120 let route = MidiHwInRoute {
8121 device: device.to_string(),
8122 to_track: to_track.to_string(),
8123 to_port,
8124 };
8125 if !self.midi_hw_in_routes.iter().any(|r| r == &route) {
8126 self.midi_hw_in_routes.push(route);
8127 }
8128 } else {
8129 self.notify_clients(Err(format!(
8130 "MIDI destination track not found: {}",
8131 to_track
8132 )))
8133 .await;
8134 return;
8135 }
8136 } else if let Some(device) = to_hw_out_device {
8137 if let Some(f_t) = from_track_handle {
8138 if f_t.lock().midi.outs.get(from_port).is_none() {
8139 self.notify_clients(Err(format!(
8140 "MIDI output port {} not found on track '{}'",
8141 from_port, from_track
8142 )))
8143 .await;
8144 return;
8145 }
8146 let route = MidiHwOutRoute {
8147 from_track: from_track.to_string(),
8148 from_port,
8149 device: device.to_string(),
8150 };
8151 if !self.midi_hw_out_routes.iter().any(|r| r == &route) {
8152 self.midi_hw_out_routes.push(route);
8153 }
8154 } else {
8155 self.notify_clients(Err(format!(
8156 "MIDI source track not found: {}",
8157 from_track
8158 )))
8159 .await;
8160 return;
8161 }
8162 } else {
8163 match (from_track_handle, to_track_handle) {
8164 (Some(f_t), Some(t_t)) => {
8165 let to_in_res = t_t.lock().midi.ins.get(to_port).cloned();
8166 if let Some(to_in) = to_in_res {
8167 let from_track = f_t.lock();
8168 if let Err(e) =
8169 from_track.midi.connect_out(from_port, to_in)
8170 {
8171 self.notify_clients(Err(e)).await;
8172 return;
8173 }
8174 from_track.invalidate_midi_route_cache();
8175 } else {
8176 self.notify_clients(Err(format!(
8177 "MIDI input port {} not found on track '{}'",
8178 to_port, to_track
8179 )))
8180 .await;
8181 return;
8182 }
8183 }
8184 _ => {
8185 self.notify_clients(Err(format!(
8186 "MIDI tracks not found: {} or {}",
8187 from_track, to_track
8188 )))
8189 .await;
8190 return;
8191 }
8192 }
8193 }
8194 }
8195 };
8196 }
8197 Action::Disconnect {
8198 ref from_track,
8199 from_port,
8200 ref to_track,
8201 to_port,
8202 kind,
8203 } => {
8204 if kind == Kind::Audio {
8205 if let Err(e) = self.disconnect_audio_route_and_notify(a.clone()).await {
8206 self.notify_clients(Err(e)).await;
8207 }
8208 } else if kind == Kind::MIDI {
8209 let from_hw_in_device = Self::midi_hw_in_device(from_track);
8210 let to_hw_out_device = Self::midi_hw_out_device(to_track);
8211
8212 if let (Some(from_device), Some(to_device)) =
8213 (from_hw_in_device, to_hw_out_device)
8214 {
8215 let before = self.midi_hw_thru_routes.len();
8216 self.midi_hw_thru_routes.retain(|r| {
8217 !(r.from_device == from_device && r.to_device == to_device)
8218 });
8219 if self.midi_hw_thru_routes.len() < before {
8220 self.notify_clients(Ok(a.clone())).await;
8221 } else {
8222 self.notify_clients(Err(format!(
8223 "Disconnect failed: MIDI route not found ({} -> {})",
8224 from_track, to_track
8225 )))
8226 .await;
8227 }
8228 return;
8229 }
8230
8231 if let Some(device) = from_hw_in_device {
8232 let before = self.midi_hw_in_routes.len();
8233 self.midi_hw_in_routes.retain(|r| {
8234 !(r.device == device && r.to_track == *to_track && r.to_port == to_port)
8235 });
8236 if self.midi_hw_in_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) = to_hw_out_device {
8249 let before = self.midi_hw_out_routes.len();
8250 self.midi_hw_out_routes.retain(|r| {
8251 !(r.from_track == *from_track
8252 && r.from_port == from_port
8253 && r.device == device)
8254 });
8255 if self.midi_hw_out_routes.len() < before {
8256 self.notify_clients(Ok(a.clone())).await;
8257 } else {
8258 self.notify_clients(Err(format!(
8259 "Disconnect failed: MIDI route not found ({} -> {})",
8260 from_track, to_track
8261 )))
8262 .await;
8263 }
8264 return;
8265 }
8266
8267 let state = self.state.lock();
8268 if let (Some(f_t), Some(t_t)) =
8269 (state.tracks.get(from_track), state.tracks.get(to_track))
8270 && let Some(to_in) = t_t.lock().midi.ins.get(to_port).cloned()
8271 {
8272 let from_track = f_t.lock();
8273 if let Err(e) = from_track.midi.disconnect_out(from_port, &to_in) {
8274 self.notify_clients(Err(e)).await;
8275 } else {
8276 from_track.invalidate_midi_route_cache();
8277 self.notify_clients(Ok(a.clone())).await;
8278 }
8279 } else {
8280 self.notify_clients(Err(format!(
8281 "Disconnect failed: MIDI ports not found ({} -> {})",
8282 from_track, to_track
8283 )))
8284 .await;
8285 }
8286 }
8287 }
8288
8289 Action::OpenAudioDevice {
8290 ref device,
8291 ref input_device,
8292 sample_rate_hz,
8293 bits,
8294 exclusive,
8295 period_frames,
8296 nperiods,
8297 sync_mode,
8298 ..
8299 } => {
8300 #[cfg(unix)]
8301 {
8302 let request = AudioOpenRequest {
8303 device,
8304 input_device: input_device.as_deref(),
8305 sample_rate_hz,
8306 bits,
8307 exclusive,
8308 period_frames,
8309 nperiods,
8310 sync_mode,
8311 };
8312 if self.maybe_open_jack_runtime(request).await.is_some() {
8313 return;
8314 }
8315 }
8316 let hw_opts = Self::build_hw_options(exclusive, period_frames, nperiods, sync_mode);
8317 let open_result = self
8318 .open_non_jack_audio_device(
8319 device,
8320 input_device.as_deref(),
8321 sample_rate_hz,
8322 bits,
8323 hw_opts,
8324 )
8325 .await;
8326 match open_result {
8327 Ok(()) => {}
8328 Err(e) => {
8329 error!("Failed to open audio device: {e}");
8330 self.notify_clients(Err(e)).await;
8331 return;
8332 }
8333 }
8334 self.finalize_open_audio_device().await;
8335 if let Some(hw) = &self.hw_driver {
8336 let effective_action = {
8337 let hw = hw.lock();
8338 Action::OpenAudioDevice {
8339 device: device.clone(),
8340 input_device: input_device.clone(),
8341 sample_rate_hz: hw.sample_rate(),
8342 bits: hw.sample_bits(),
8343 exclusive,
8344 period_frames,
8345 nperiods,
8346 sync_mode,
8347 actual_period_frames: hw.cycle_samples(),
8348 input_channels: hw.input_channels(),
8349 output_channels: hw.output_channels(),
8350 bytes_per_frame: hw.frame_size_bytes(),
8351 }
8352 };
8353 action_to_process = effective_action;
8354 }
8355 }
8356 Action::JackAddAudioInputPort => {
8357 #[cfg(unix)]
8358 {
8359 if let Some(jack) = self.jack_runtime.clone() {
8360 let (input_channels, output_channels, rate) = {
8361 let jack = jack.lock();
8362 if let Err(e) = jack.add_audio_input_port() {
8363 self.notify_clients(Err(e)).await;
8364 return;
8365 }
8366 (
8367 jack.input_channels(),
8368 jack.output_channels(),
8369 jack.sample_rate,
8370 )
8371 };
8372 self.publish_hw_infos(input_channels, output_channels, rate)
8373 .await;
8374 self.notify_clients(Ok(a.clone())).await;
8375 } else {
8376 self.notify_clients(Err(
8377 "JACK runtime is not active; open the JACK backend first".to_string(),
8378 ))
8379 .await;
8380 }
8381 }
8382 #[cfg(not(unix))]
8383 {
8384 self.notify_clients(Err(
8385 "JACK backend is not available on this platform build".to_string(),
8386 ))
8387 .await;
8388 }
8389 }
8390 Action::JackRemoveAudioInputPort(_removed_port) => {
8391 #[cfg(unix)]
8392 {
8393 let removed_port = _removed_port;
8394 if let Some(jack) = self.jack_runtime.clone() {
8395 let (removed_port, removed_io) = {
8396 let jack = jack.lock();
8397 let removed_port = Some(removed_port);
8398 let removed_io =
8399 removed_port.and_then(|port| jack.input_audio_port(port));
8400 match (removed_port, removed_io) {
8401 (Some(port), Some(io)) => (port, io),
8402 _ => {
8403 self.notify_clients(Err(
8404 "JACK audio input port index is out of range".to_string(),
8405 ))
8406 .await;
8407 return;
8408 }
8409 }
8410 };
8411 let reindex_notifications =
8412 self.reindex_notifications_for_removed_hw_input(removed_port);
8413 for disconnect in
8414 self.disconnect_actions_for_removed_hw_input(removed_port, &removed_io)
8415 {
8416 if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await
8417 {
8418 self.notify_clients(Err(e)).await;
8419 return;
8420 }
8421 }
8422 let (input_channels, output_channels, rate) = {
8423 let jack = jack.lock();
8424 if let Err(e) = jack.remove_audio_input_port(removed_port) {
8425 self.notify_clients(Err(e)).await;
8426 return;
8427 }
8428 (
8429 jack.input_channels(),
8430 jack.output_channels(),
8431 jack.sample_rate,
8432 )
8433 };
8434 for action in reindex_notifications {
8435 self.notify_clients(Ok(action)).await;
8436 }
8437 self.publish_hw_infos(input_channels, output_channels, rate)
8438 .await;
8439 self.notify_clients(Ok(a.clone())).await;
8440 } else {
8441 self.notify_clients(Err(
8442 "JACK runtime is not active; open the JACK backend first".to_string(),
8443 ))
8444 .await;
8445 }
8446 }
8447 #[cfg(not(unix))]
8448 {
8449 self.notify_clients(Err(
8450 "JACK backend is not available on this platform build".to_string(),
8451 ))
8452 .await;
8453 }
8454 }
8455 Action::JackAddAudioOutputPort => {
8456 #[cfg(unix)]
8457 {
8458 if let Some(jack) = self.jack_runtime.clone() {
8459 let (input_channels, output_channels, rate) = {
8460 let jack = jack.lock();
8461 if let Err(e) = jack.add_audio_output_port() {
8462 self.notify_clients(Err(e)).await;
8463 return;
8464 }
8465 (
8466 jack.input_channels(),
8467 jack.output_channels(),
8468 jack.sample_rate,
8469 )
8470 };
8471 self.publish_hw_infos(input_channels, output_channels, rate)
8472 .await;
8473 self.notify_clients(Ok(a.clone())).await;
8474 } else {
8475 self.notify_clients(Err(
8476 "JACK runtime is not active; open the JACK backend first".to_string(),
8477 ))
8478 .await;
8479 }
8480 }
8481 #[cfg(not(unix))]
8482 {
8483 self.notify_clients(Err(
8484 "JACK backend is not available on this platform build".to_string(),
8485 ))
8486 .await;
8487 }
8488 }
8489 Action::JackRemoveAudioOutputPort(_removed_port) => {
8490 #[cfg(unix)]
8491 {
8492 let removed_port = _removed_port;
8493 if let Some(jack) = self.jack_runtime.clone() {
8494 let (removed_port, removed_io) = {
8495 let jack = jack.lock();
8496 let removed_port = Some(removed_port);
8497 let removed_io =
8498 removed_port.and_then(|port| jack.output_audio_port(port));
8499 match (removed_port, removed_io) {
8500 (Some(port), Some(io)) => (port, io),
8501 _ => {
8502 self.notify_clients(Err(
8503 "JACK audio output port index is out of range".to_string(),
8504 ))
8505 .await;
8506 return;
8507 }
8508 }
8509 };
8510 let reindex_notifications =
8511 self.reindex_notifications_for_removed_hw_output(removed_port);
8512 for disconnect in
8513 self.disconnect_actions_for_removed_hw_output(removed_port, &removed_io)
8514 {
8515 if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await
8516 {
8517 self.notify_clients(Err(e)).await;
8518 return;
8519 }
8520 }
8521 let (input_channels, output_channels, rate) = {
8522 let jack = jack.lock();
8523 if let Err(e) = jack.remove_audio_output_port(removed_port) {
8524 self.notify_clients(Err(e)).await;
8525 return;
8526 }
8527 (
8528 jack.input_channels(),
8529 jack.output_channels(),
8530 jack.sample_rate,
8531 )
8532 };
8533 for action in reindex_notifications {
8534 self.notify_clients(Ok(action)).await;
8535 }
8536 self.publish_hw_infos(input_channels, output_channels, rate)
8537 .await;
8538 self.notify_clients(Ok(a.clone())).await;
8539 } else {
8540 self.notify_clients(Err(
8541 "JACK runtime is not active; open the JACK backend first".to_string(),
8542 ))
8543 .await;
8544 }
8545 }
8546 #[cfg(not(unix))]
8547 {
8548 self.notify_clients(Err(
8549 "JACK backend is not available on this platform build".to_string(),
8550 ))
8551 .await;
8552 }
8553 }
8554 Action::OpenMidiInputDevice(ref device) => {
8555 let midi_hub = self.midi_hub.lock();
8556 if let Err(e) = midi_hub.open_input(device) {
8557 self.notify_clients(Err(e)).await;
8558 return;
8559 }
8560 }
8561 Action::OpenMidiOutputDevice(ref device) => {
8562 let midi_hub = self.midi_hub.lock();
8563 if let Err(e) = midi_hub.open_output(device) {
8564 self.notify_clients(Err(e)).await;
8565 return;
8566 }
8567 }
8568 Action::RequestSessionDiagnostics => {
8569 let (
8570 track_count,
8571 frozen_track_count,
8572 audio_clip_count,
8573 midi_clip_count,
8574 lv2_instance_count,
8575 vst3_instance_count,
8576 clap_instance_count,
8577 ) = {
8578 let tracks = &self.state.lock().tracks;
8579 let mut track_count = 0usize;
8580 let mut frozen_track_count = 0usize;
8581 let mut audio_clip_count = 0usize;
8582 let mut midi_clip_count = 0usize;
8583 #[cfg(all(unix, not(target_os = "macos")))]
8584 let mut lv2_instance_count = 0usize;
8585 #[cfg(not(all(unix, not(target_os = "macos"))))]
8586 let lv2_instance_count = 0usize;
8587 let mut vst3_instance_count = 0usize;
8588 let mut clap_instance_count = 0usize;
8589 for track in tracks.values() {
8590 let t = track.lock();
8591 track_count += 1;
8592 if t.frozen {
8593 frozen_track_count += 1;
8594 }
8595 audio_clip_count += t.audio.clips.len();
8596 midi_clip_count += t.midi.clips.len();
8597 #[cfg(all(unix, not(target_os = "macos")))]
8598 {
8599 lv2_instance_count += t.lv2_plugins.len();
8600 }
8601 vst3_instance_count += t.vst3_plugins.len();
8602 clap_instance_count += t.clap_plugins.len();
8603 }
8604 (
8605 track_count,
8606 frozen_track_count,
8607 audio_clip_count,
8608 midi_clip_count,
8609 lv2_instance_count,
8610 vst3_instance_count,
8611 clap_instance_count,
8612 )
8613 };
8614 #[cfg(not(all(unix, not(target_os = "macos"))))]
8615 let _lv2_instance_count = lv2_instance_count;
8616 let pending_hw_midi_events = self.pending_hw_midi_events.len()
8617 + self
8618 .pending_hw_midi_events_by_device
8619 .values()
8620 .map(std::vec::Vec::len)
8621 .sum::<usize>();
8622 let sample_rate_hz = if let Some(hw) = &self.hw_driver {
8623 hw.lock().sample_rate() as usize
8624 } else {
8625 #[cfg(unix)]
8626 {
8627 self.jack_runtime
8628 .as_ref()
8629 .map(|j| j.lock().sample_rate)
8630 .unwrap_or(0)
8631 }
8632 #[cfg(not(unix))]
8633 0
8634 };
8635 let cycle_samples = self.current_cycle_samples();
8636 self.notify_clients(Ok(Action::SessionDiagnosticsReport {
8637 track_count,
8638 frozen_track_count,
8639 audio_clip_count,
8640 midi_clip_count,
8641 #[cfg(all(unix, not(target_os = "macos")))]
8642 lv2_instance_count,
8643 vst3_instance_count,
8644 clap_instance_count,
8645 pending_requests: self.pending_requests.len(),
8646 workers_total: self.workers.len(),
8647 workers_ready: self.ready_workers.len(),
8648 pending_hw_midi_events,
8649 playing: self.playing,
8650 transport_sample: self.transport_sample,
8651 tempo_bpm: self.tempo_bpm,
8652 sample_rate_hz,
8653 cycle_samples,
8654 }))
8655 .await;
8656 }
8657 Action::RequestMidiLearnMappingsReport => {
8658 let mut lines = Vec::<String>::new();
8659 let fmt_binding = |b: &crate::message::MidiLearnBinding| {
8660 let device = b.device.as_deref().unwrap_or("*");
8661 format!("{device} CH{} CC{}", b.channel + 1, b.cc)
8662 };
8663 if let Some(b) = self.global_midi_learn_play_pause.as_ref() {
8664 lines.push(format!("Global PlayPause: {}", fmt_binding(b)));
8665 }
8666 if let Some(b) = self.global_midi_learn_stop.as_ref() {
8667 lines.push(format!("Global Stop: {}", fmt_binding(b)));
8668 }
8669 if let Some(b) = self.global_midi_learn_record_toggle.as_ref() {
8670 lines.push(format!("Global RecordToggle: {}", fmt_binding(b)));
8671 }
8672 for (track_name, track) in self.state.lock().tracks.iter() {
8673 let t = track.lock();
8674 if let Some(b) = t.midi_learn_volume.as_ref() {
8675 lines.push(format!("{} Volume: {}", track_name, fmt_binding(b)));
8676 }
8677 if let Some(b) = t.midi_learn_balance.as_ref() {
8678 lines.push(format!("{} Balance: {}", track_name, fmt_binding(b)));
8679 }
8680 if let Some(b) = t.midi_learn_mute.as_ref() {
8681 lines.push(format!("{} Mute: {}", track_name, fmt_binding(b)));
8682 }
8683 if let Some(b) = t.midi_learn_solo.as_ref() {
8684 lines.push(format!("{} Solo: {}", track_name, fmt_binding(b)));
8685 }
8686 if let Some(b) = t.midi_learn_arm.as_ref() {
8687 lines.push(format!("{} Arm: {}", track_name, fmt_binding(b)));
8688 }
8689 if let Some(b) = t.midi_learn_input_monitor.as_ref() {
8690 lines.push(format!("{} InputMonitor: {}", track_name, fmt_binding(b)));
8691 }
8692 if let Some(b) = t.midi_learn_disk_monitor.as_ref() {
8693 lines.push(format!("{} DiskMonitor: {}", track_name, fmt_binding(b)));
8694 }
8695 }
8696 if lines.is_empty() {
8697 lines.push("No MIDI learn mappings configured".to_string());
8698 }
8699 self.notify_clients(Ok(Action::MidiLearnMappingsReport { lines }))
8700 .await;
8701 }
8702 Action::ClearAllMidiLearnBindings => {
8703 self.pending_midi_learn = None;
8704 self.pending_global_midi_learn = None;
8705 self.global_midi_learn_play_pause = None;
8706 self.global_midi_learn_stop = None;
8707 self.global_midi_learn_record_toggle = None;
8708 self.midi_cc_gate.clear();
8709 for track in self.state.lock().tracks.values() {
8710 let t = track.lock();
8711 t.midi_learn_volume = None;
8712 t.midi_learn_balance = None;
8713 t.midi_learn_mute = None;
8714 t.midi_learn_solo = None;
8715 t.midi_learn_arm = None;
8716 t.midi_learn_input_monitor = None;
8717 t.midi_learn_disk_monitor = None;
8718 }
8719 }
8720 #[cfg(all(unix, not(target_os = "macos")))]
8721 Action::TrackLv2PluginControls { .. } => {}
8722 #[cfg(all(unix, not(target_os = "macos")))]
8723 Action::ClipLv2PluginControls { .. } => {}
8724 #[cfg(all(unix, not(target_os = "macos")))]
8725 Action::TrackLv2Midnam { .. } => {}
8726 Action::TrackClapNoteNames { .. } => {}
8727 Action::SessionDiagnosticsReport { .. } => {}
8728 Action::MidiLearnMappingsReport { .. } => {}
8729 Action::HWInfo { .. } => {}
8730 Action::HistoryState { .. } => {}
8731 Action::Undo => {}
8732 Action::Redo => {}
8733 Action::ApplyGroupedActions(_) => {}
8734 _ => {}
8735 }
8736
8737 if let Some(inverse) = inverse_actions {
8738 if let Some(group) = self.history_group.as_mut() {
8739 group.forward_actions.push(action_to_process.clone());
8740 group.inverse_actions.splice(0..0, inverse);
8741 } else {
8742 self.history.record(UndoEntry {
8743 forward_actions: vec![action_to_process.clone()],
8744 inverse_actions: inverse,
8745 });
8746 }
8747 }
8748
8749 self.notify_clients(Ok(action_to_process)).await;
8750 }
8751
8752 pub async fn work(&mut self) {
8753 while let Some(message) = self.rx.recv().await {
8754 match message {
8755 Message::Ready(id) => self.push_ready_worker(id),
8756 Message::Finished {
8757 worker_id,
8758 task,
8759 output_linear,
8760 process_epoch,
8761 parameter_updates,
8762 } => {
8763 tracing::debug!(
8764 "engine received Finished from worker {} for task {:?} (epoch {} vs {})",
8765 worker_id,
8766 task,
8767 process_epoch,
8768 self.track_process_epoch
8769 );
8770 self.push_ready_worker(worker_id);
8771 let task_key = Self::task_key(&task);
8772 self.task_processing_started_at.remove(&task_key);
8773 if process_epoch != self.track_process_epoch {
8774 if let Some(track) = self
8775 .state
8776 .lock()
8777 .tracks
8778 .get(&Self::task_track_name(&task))
8779 .cloned()
8780 {
8781 let t = track.lock();
8782 t.audio.finished = false;
8783 t.audio.processing = false;
8784 }
8785 continue;
8786 }
8787 self.cycle_tasks_running
8788 .retain(|t| Self::task_key(t) != task_key);
8789 self.cycle_tasks_finished.push(task.clone());
8790 let track_name = Self::task_track_name(&task);
8791 self.track_meter_linear_by_track
8792 .insert(track_name.clone(), output_linear);
8793 for action in parameter_updates {
8794 self.notify_clients(Ok(action)).await;
8795 }
8796 self.force_stalled_task_completions();
8797 let all_finished = self.send_tasks().await;
8798 tracing::debug!(
8799 "engine after Finished for {}: all_finished={}",
8800 track_name,
8801 all_finished
8802 );
8803 if all_finished {
8804 self.on_all_tracks_finished().await;
8805 }
8806 }
8807 Message::Channel(s) => {
8808 self.clients.push(s);
8809 }
8810
8811 Message::Request(a) => {
8812 match a {
8813 Action::TrackOfflineBounceCancel { track_name } => {
8814 if let Some(job) = self.offline_bounce_jobs.get(&track_name) {
8815 job.cancel.store(true, Ordering::Relaxed);
8816 }
8817 }
8818 Action::TrackOfflineBounceCancelAll => {
8819 for job in self.offline_bounce_jobs.values() {
8820 job.cancel.store(true, Ordering::Relaxed);
8821 }
8822 }
8823 _ if !self.offline_bounce_jobs.is_empty() => {
8824 self.pending_requests.push_back(a);
8825 }
8826 Action::OpenAudioDevice { .. }
8827 | Action::OpenMidiInputDevice(_)
8828 | Action::OpenMidiOutputDevice(_)
8829 | Action::RequestMeterSnapshot
8830 | Action::Quit
8831 | Action::Log { .. }
8832 | Action::Play
8833 | Action::Pause
8834 | Action::Stop
8835 | Action::TransportPosition(_)
8836 | Action::JumpToEnd
8837 | Action::SetLoopEnabled(_)
8838 | Action::SetLoopRange(_)
8839 | Action::SetPunchEnabled(_)
8840 | Action::SetPunchRange(_)
8841 | Action::SetMetronomeEnabled(_)
8842 | Action::SetTempo(_)
8843 | Action::SetTimeSignature { .. }
8844 | Action::SetOscEnabled(_)
8845 | Action::SetClipPlaybackEnabled(_)
8846 | Action::SetRecordEnabled(_)
8847 | Action::SetStepRecording(_)
8848 | Action::StepRecordMidiNote { .. }
8849 | Action::SetSessionPath(_)
8850 | Action::ClearHistory
8851 | Action::BeginSessionRestore
8852 | Action::PianoKey { .. }
8853 | Action::ModifyMidiNotes { .. }
8854 | Action::ModifyMidiControllers { .. }
8855 | Action::DeleteMidiControllers { .. }
8856 | Action::InsertMidiControllers { .. }
8857 | Action::DeleteMidiNotes { .. }
8858 | Action::InsertMidiNotes { .. }
8859 | Action::SetMidiSysExEvents { .. } => {
8860 self.handle_request(a).await;
8861 }
8862 #[cfg(all(unix, not(target_os = "macos")))]
8863 Action::ListLv2Plugins => {
8864 self.handle_request(a).await;
8865 }
8866 Action::ListVst3Plugins => {
8867 self.handle_request(a).await;
8868 }
8869 Action::ListClapPlugins => {
8870 self.handle_request(a).await;
8871 }
8872 Action::ListClapPluginsWithCapabilities => {
8873 self.handle_request(a).await;
8874 }
8875 _ => {
8876 self.pending_requests.push_back(a);
8877 if self.can_schedule_hw_cycle() {
8878 self.request_hw_cycle().await;
8879 } else {
8880 while let Some(next) = self.pending_requests.pop_front() {
8881 self.handle_request(next).await;
8882 }
8883 }
8884 }
8885 };
8886 self.publish_clap_state_dirty().await;
8887 }
8888 Message::OfflineBounceFinished { result } => {
8889 if let Ok(Action::TrackOfflineBounce { track_name, .. }) = &result {
8890 self.offline_bounce_jobs.remove(track_name);
8891 }
8892 self.notify_clients(result).await;
8893 if self.offline_bounce_jobs.is_empty() {
8894 while let Some(next) = self.pending_requests.pop_front() {
8895 self.handle_request(next).await;
8896 }
8897 }
8898 }
8899 Message::HWFinished => {
8900 if !self.awaiting_hwfinished {
8901 tracing::debug!("HWFinished ignored (not awaiting)");
8902 continue;
8903 }
8904 tracing::debug!("HWFinished handling; playing={}", self.playing);
8905 self.handling_hwfinished = true;
8906 self.awaiting_hwfinished = false;
8907 #[cfg(unix)]
8908 {
8909 if let Some(jack) = &self.jack_runtime {
8910 if !self.pending_hw_midi_out_events.is_empty() {
8911 let out_events =
8912 std::mem::take(&mut self.pending_hw_midi_out_events);
8913 jack.lock().write_events(&out_events);
8914 }
8915 let mut in_events = vec![];
8916 jack.lock().read_events_into(&mut in_events);
8917 if !in_events.is_empty() {
8918 self.pending_hw_midi_events.extend(in_events);
8919 }
8920 }
8921 }
8922 #[cfg(unix)]
8923 if self.jack_runtime.is_some() {
8924 self.sync_from_jack_transport().await;
8925 }
8926 while let Some(a) = self.pending_requests.pop_front() {
8927 self.handle_request(a).await;
8928 }
8929 self.apply_mute_solo_policy();
8930 self.append_recorded_cycle();
8931 self.flush_completed_recordings().await;
8932 let hw_in_routes = self.midi_hw_in_routes.clone();
8933 let pending_hw_in_by_device = self.pending_hw_midi_events_by_device.clone();
8934 let mut reconfigured_tracks = Vec::new();
8935 for (track_name, track) in self.state.lock().tracks.iter() {
8936 let track_lock = track.lock();
8937 if self.jack_runtime_is_some() {
8938 if !self.pending_hw_midi_events.is_empty() {
8939 track_lock.push_hw_midi_events(&self.pending_hw_midi_events);
8940 }
8941 } else {
8942 for route in hw_in_routes.iter().filter(|r| &r.to_track == track_name) {
8943 if let Some(events) = pending_hw_in_by_device.get(&route.device) {
8944 track_lock.push_hw_midi_events_to_port(route.to_port, events);
8945 }
8946 }
8947 }
8948 if track_lock.setup() {
8949 reconfigured_tracks.push(track_name.clone());
8950 }
8951 }
8952 self.publish_track_meters().await;
8953 self.publish_clap_state_dirty().await;
8954 for track_name in reconfigured_tracks {
8955 let track = self.state.lock().tracks.get(&track_name).cloned();
8956 if let Some(track) = track {
8957 let (plugins, connections, connectable_connections) = {
8958 let track_lock = track.lock();
8959 (
8960 track_lock.plugin_graph_plugins(),
8961 track_lock.plugin_graph_connections(),
8962 track_lock.connectable_connections(),
8963 )
8964 };
8965 self.notify_clients(Ok(Action::TrackPluginGraph {
8966 track_name: track_name.clone(),
8967 plugins,
8968 connections,
8969 connectable_connections,
8970 }))
8971 .await;
8972 }
8973 }
8974 self.pending_hw_midi_events.clear();
8975 self.pending_hw_midi_events_by_device.clear();
8976 if self.playing {
8977 if self.transport_panic_flush_pending {
8978 self.transport_panic_flush_pending = false;
8979 } else if self.transport_restart_pending {
8980 self.transport_restart_pending = false;
8981 } else {
8982 let next = self
8983 .transport_sample
8984 .saturating_add(self.current_cycle_samples());
8985 let normalized = self.normalize_transport_sample(next);
8986 let wrapped = normalized != next;
8987 self.transport_sample = normalized;
8988 if wrapped {
8989 if self.notified_loop_wrap_sample == Some(self.transport_sample) {
8990 self.notified_loop_wrap_sample = None;
8991 } else {
8992 self.notify_clients(Ok(Action::TransportPosition(
8993 self.transport_sample,
8994 )))
8995 .await;
8996 }
8997 }
8998 }
8999 }
9000 {
9001 let echoes = self.apply_modulators(self.transport_sample);
9002 for action in echoes {
9003 self.notify_clients(Ok(action)).await;
9004 }
9005 }
9006 self.invalidate_track_cycle_state();
9007 let all_finished = self.send_tasks().await;
9008 tracing::debug!(
9009 "HWFinished send_tasks finished={} hw_worker={}",
9010 all_finished,
9011 self.hw_worker.is_some()
9012 );
9013 if all_finished && self.hw_worker.is_some() {
9014 self.request_hw_cycle().await;
9015 }
9016 #[cfg(unix)]
9017 {
9018 if self.jack_runtime.is_some() {
9019 self.awaiting_hwfinished = true;
9020 }
9021 }
9022 self.handling_hwfinished = false;
9023 }
9024 Message::HWMidiEvents(events) => {
9025 for hw_event in events {
9026 let thru_targets: Vec<String> = self
9027 .midi_hw_thru_routes
9028 .iter()
9029 .filter(|route| route.from_device == hw_event.device)
9030 .map(|route| route.to_device.clone())
9031 .collect();
9032 for device in thru_targets {
9033 self.pending_hw_midi_out_events_by_device.push(HwMidiEvent {
9034 device,
9035 event: hw_event.event.clone(),
9036 });
9037 }
9038 if hw_event.event.data.len() >= 3 {
9039 let status = hw_event.event.data[0];
9040 if status & 0xF0 == 0xB0 {
9041 let channel = status & 0x0F;
9042 let cc = hw_event.event.data[1];
9043 let value = hw_event.event.data[2];
9044 self.handle_incoming_hw_cc(&hw_event.device, channel, cc, value)
9045 .await;
9046 }
9047 if self.step_recording_enabled && status & 0xF0 == 0x90 {
9048 let channel = status & 0x0F;
9049 let pitch = hw_event.event.data[1];
9050 let velocity = hw_event.event.data[2];
9051 if velocity > 0 {
9052 self.notify_clients(Ok(Action::StepRecordMidiNote {
9053 device: hw_event.device.clone(),
9054 channel,
9055 pitch,
9056 velocity,
9057 }))
9058 .await;
9059 }
9060 }
9061 }
9062 self.pending_hw_midi_events_by_device
9063 .entry(hw_event.device)
9064 .or_default()
9065 .push(hw_event.event);
9066 }
9067 }
9068 _ => {}
9069 }
9070 }
9071 }
9072
9073 fn collect_hw_midi_output_events(&self) -> Vec<MidiEvent> {
9074 let mut events = vec![];
9075 for track in self.state.lock().tracks.values() {
9076 events.extend(
9077 track
9078 .lock()
9079 .take_hw_midi_out_events()
9080 .into_iter()
9081 .map(|evt| evt.event),
9082 );
9083 }
9084 events.sort_by_key(|a| a.frame);
9085 events
9086 }
9087
9088 fn collect_hw_midi_output_events_by_device(&mut self) -> Vec<HwMidiEvent> {
9089 let mut events = Vec::<HwMidiEvent>::new();
9090 let routes = self.midi_hw_out_routes.clone();
9091 let mut events_by_track = HashMap::<String, Vec<crate::track::HwMidiOutEvent>>::new();
9092 {
9093 let state = self.state.lock();
9094 for route in &routes {
9095 if events_by_track.contains_key(&route.from_track) {
9096 continue;
9097 }
9098 let Some(track) = state.tracks.get(&route.from_track) else {
9099 continue;
9100 };
9101 events_by_track.insert(
9102 route.from_track.clone(),
9103 track.lock().take_hw_midi_out_events(),
9104 );
9105 }
9106 }
9107
9108 for route in routes {
9109 let Some(track_events) = events_by_track.get(&route.from_track) else {
9110 continue;
9111 };
9112 for hw_event in track_events
9113 .iter()
9114 .filter(|evt| evt.port == route.from_port)
9115 {
9116 self.update_active_hw_notes_for_track(
9117 &route.from_track,
9118 &route.device,
9119 &hw_event.event.data,
9120 );
9121 events.push(HwMidiEvent {
9122 device: route.device.clone(),
9123 event: hw_event.event.clone(),
9124 });
9125 }
9126 }
9127 events.sort_by(|a, b| {
9128 a.event
9129 .frame
9130 .cmp(&b.event.frame)
9131 .then_with(|| a.device.cmp(&b.device))
9132 });
9133 events
9134 }
9135}
9136
9137#[cfg(test)]
9138mod tests {
9139 use super::*;
9140 use crate::mutex::UnsafeMutex;
9141 use tokio::sync::mpsc::channel;
9142 use tokio::time::{Duration as TokioDuration, timeout};
9143
9144 #[test]
9145 #[cfg(unix)]
9146 fn jack_transport_sync_decision_starts_and_syncs_position_on_external_play() {
9147 let decision = Engine::jack_transport_sync_decision(false, 128, true, 256, 64);
9148
9149 assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Start));
9150 assert_eq!(decision.position_sync, Some(256));
9151 }
9152
9153 #[test]
9154 #[cfg(unix)]
9155 fn jack_transport_sync_decision_stops_and_syncs_position_on_external_stop() {
9156 let decision = Engine::jack_transport_sync_decision(true, 512, false, 96, 64);
9157
9158 assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Stop));
9159 assert_eq!(decision.position_sync, Some(96));
9160 }
9161
9162 #[test]
9163 #[cfg(unix)]
9164 fn jack_transport_sync_decision_ignores_small_rolling_drift() {
9165 let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1040, 64);
9166
9167 assert_eq!(decision.play_sync, None);
9168 assert_eq!(decision.position_sync, None);
9169 }
9170
9171 #[test]
9172 #[cfg(unix)]
9173 fn jack_transport_sync_decision_syncs_large_rolling_jump() {
9174 let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1200, 64);
9175
9176 assert_eq!(decision.play_sync, None);
9177 assert_eq!(decision.position_sync, Some(1200));
9178 }
9179
9180 #[test]
9181 #[cfg(unix)]
9182 fn jack_transport_sync_decision_syncs_locate_while_stopped() {
9183 let decision = Engine::jack_transport_sync_decision(false, 400, false, 900, 64);
9184
9185 assert_eq!(decision.play_sync, None);
9186 assert_eq!(decision.position_sync, Some(900));
9187 }
9188
9189 fn make_engine_with_client() -> (Engine, tokio::sync::mpsc::Receiver<Message>) {
9190 let (engine_tx, engine_rx) = channel(16);
9191 let mut engine = Engine::new(engine_rx, engine_tx);
9192 let (client_tx, client_rx) = channel(16);
9193 engine.clients.push(client_tx);
9194 (engine, client_rx)
9195 }
9196
9197 fn insert_track(engine: &mut Engine, track: Track) {
9198 engine.state.lock().tracks.insert(
9199 track.name.clone(),
9200 Arc::new(UnsafeMutex::new(Box::new(track))),
9201 );
9202 }
9203
9204 fn osc_packet(address: &str) -> Vec<u8> {
9205 fn push_padded_osc_string(packet: &mut Vec<u8>, value: &str) {
9206 packet.extend_from_slice(value.as_bytes());
9207 packet.push(0);
9208 while !packet.len().is_multiple_of(4) {
9209 packet.push(0);
9210 }
9211 }
9212
9213 let mut packet = Vec::new();
9214 push_padded_osc_string(&mut packet, address);
9215 push_padded_osc_string(&mut packet, ",");
9216 packet
9217 }
9218
9219 #[tokio::test]
9220 async fn set_osc_enabled_starts_and_stops_server() {
9221 let (mut engine, _client_rx) = make_engine_with_client();
9222
9223 engine
9224 .set_osc_enabled_with(true, |tx| OscServer::start_on_addr(tx, "127.0.0.1:0"))
9225 .expect("start osc server on ephemeral port");
9226 assert!(engine.osc_server.is_some());
9227
9228 engine
9229 .set_osc_enabled_with(false, OscServer::start)
9230 .expect("stop osc server");
9231 assert!(engine.osc_server.is_none());
9232 }
9233
9234 #[tokio::test]
9235 async fn osc_server_forwards_transport_packets_to_engine_channel() {
9236 let (tx, mut rx) = channel(4);
9237 let mut server =
9238 OscServer::start_on_addr(tx, "127.0.0.1:0").expect("start osc test server");
9239 let socket = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind sender socket");
9240 let packet = osc_packet("/transport/play");
9241 socket
9242 .send_to(&packet, server.listen_addr())
9243 .expect("send osc packet");
9244
9245 let message = timeout(TokioDuration::from_secs(1), rx.recv())
9246 .await
9247 .expect("packet delivery timeout")
9248 .expect("osc message");
9249 match message {
9250 Message::Request(Action::Play) => {}
9251 other => panic!("unexpected osc message: {other:?}"),
9252 }
9253
9254 server.stop();
9255 }
9256
9257 #[tokio::test]
9258 async fn track_offline_bounce_rejects_zero_length_requests() {
9259 let (mut engine, mut client_rx) = make_engine_with_client();
9260 insert_track(
9261 &mut engine,
9262 Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
9263 );
9264
9265 engine
9266 .handle_request(Action::TrackOfflineBounce {
9267 track_name: "track".to_string(),
9268 output_path: "/tmp/out.wav".to_string(),
9269 start_sample: 0,
9270 length_samples: 0,
9271 automation_lanes: vec![],
9272 apply_fader: false,
9273 })
9274 .await;
9275
9276 match client_rx.recv().await.expect("response") {
9277 Message::Response(Err(err)) => {
9278 assert!(err.contains("has no renderable content for offline bounce"));
9279 }
9280 other => panic!("unexpected message: {other:?}"),
9281 }
9282 }
9283
9284 #[tokio::test]
9285 async fn track_offline_bounce_rejects_when_same_track_is_active() {
9286 let (mut engine, mut client_rx) = make_engine_with_client();
9287 engine.offline_bounce_jobs.insert(
9288 "other".to_string(),
9289 OfflineBounceJob {
9290 cancel: Arc::new(AtomicBool::new(false)),
9291 },
9292 );
9293
9294 engine
9295 .handle_request(Action::TrackOfflineBounce {
9296 track_name: "other".to_string(),
9297 output_path: "/tmp/out.wav".to_string(),
9298 start_sample: 0,
9299 length_samples: 128,
9300 automation_lanes: vec![],
9301 apply_fader: false,
9302 })
9303 .await;
9304
9305 match client_rx.recv().await.expect("response") {
9306 Message::Response(Err(err)) => {
9307 assert!(err.contains("already in progress"));
9308 }
9309 other => panic!("unexpected message: {other:?}"),
9310 }
9311 }
9312
9313 #[tokio::test]
9314 async fn track_offline_bounce_allows_different_track_concurrently() {
9315 let (mut engine, _client_rx) = make_engine_with_client();
9316 insert_track(
9317 &mut engine,
9318 Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
9319 );
9320 engine.offline_bounce_jobs.insert(
9321 "other".to_string(),
9322 OfflineBounceJob {
9323 cancel: Arc::new(AtomicBool::new(false)),
9324 },
9325 );
9326
9327 engine
9328 .handle_request(Action::TrackOfflineBounce {
9329 track_name: "track".to_string(),
9330 output_path: "/tmp/out.wav".to_string(),
9331 start_sample: 0,
9332 length_samples: 128,
9333 automation_lanes: vec![],
9334 apply_fader: false,
9335 })
9336 .await;
9337
9338 assert!(engine.offline_bounce_jobs.contains_key("other"));
9339 assert_eq!(engine.pending_requests.len(), 1);
9340 }
9341
9342 #[tokio::test]
9343 async fn reject_if_track_frozen_sends_error_and_blocks_operation() {
9344 let (mut engine, mut client_rx) = make_engine_with_client();
9345 let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
9346 track.set_frozen(true);
9347 insert_track(&mut engine, track);
9348
9349 let rejected = engine
9350 .reject_if_track_frozen("track", "arming/disarming")
9351 .await;
9352
9353 assert!(rejected);
9354 match client_rx.recv().await.expect("response") {
9355 Message::Response(Err(err)) => {
9356 assert_eq!(err, "Track 'track' is frozen; arming/disarming is blocked");
9357 }
9358 other => panic!("unexpected message: {other:?}"),
9359 }
9360 }
9361
9362 #[tokio::test]
9363 async fn undo_restores_original_clip_bounds_after_stretch_style_group() {
9364 let (mut engine, _client_rx) = make_engine_with_client();
9365 let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
9366 let mut clip = AudioClip::new("audio/original.wav".to_string(), 100, 220);
9367 clip.offset = 12;
9368 clip.fade_in_samples = 20;
9369 clip.fade_out_samples = 30;
9370 track.audio.clips.push(clip);
9371 insert_track(&mut engine, track);
9372
9373 engine.handle_request(Action::BeginHistoryGroup).await;
9374 engine
9375 .handle_request(Action::SetClipBounds {
9376 track_name: "track".to_string(),
9377 clip_index: 0,
9378 kind: Kind::Audio,
9379 start: 120,
9380 length: 180,
9381 offset: 0,
9382 })
9383 .await;
9384 engine
9385 .handle_request(Action::SetClipSourceName {
9386 track_name: "track".to_string(),
9387 clip_index: 0,
9388 kind: Kind::Audio,
9389 name: "audio/stretched.wav".to_string(),
9390 })
9391 .await;
9392 engine
9393 .handle_request(Action::SetClipFade {
9394 track_name: "track".to_string(),
9395 clip_index: 0,
9396 kind: Kind::Audio,
9397 fade_enabled: true,
9398 fade_in_samples: 12,
9399 fade_out_samples: 12,
9400 })
9401 .await;
9402 engine.handle_request(Action::EndHistoryGroup).await;
9403
9404 engine.handle_request(Action::Undo).await;
9405
9406 let state = engine.state.lock();
9407 let track = state.tracks.get("track").expect("track exists").lock();
9408 let clip = track.audio.clips.first().expect("clip exists");
9409 assert_eq!(clip.name, "audio/original.wav");
9410 assert_eq!(clip.start, 100);
9411 assert_eq!(clip.end, 220);
9412 assert_eq!(clip.end.saturating_sub(clip.start), 120);
9413 assert_eq!(clip.offset, 12);
9414 }
9415
9416 #[tokio::test]
9417 async fn track_offline_bounce_queues_when_no_worker_is_ready() {
9418 let (mut engine, _client_rx) = make_engine_with_client();
9419 insert_track(
9420 &mut engine,
9421 Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
9422 );
9423
9424 engine
9425 .handle_request(Action::TrackOfflineBounce {
9426 track_name: "track".to_string(),
9427 output_path: "/tmp/out.wav".to_string(),
9428 start_sample: 0,
9429 length_samples: 128,
9430 automation_lanes: vec![],
9431 apply_fader: false,
9432 })
9433 .await;
9434
9435 assert!(engine.offline_bounce_jobs.is_empty());
9436 assert_eq!(engine.pending_requests.len(), 1);
9437 assert!(matches!(
9438 engine.pending_requests.front(),
9439 Some(Action::TrackOfflineBounce { track_name, length_samples, .. })
9440 if track_name == "track" && *length_samples == 128
9441 ));
9442 }
9443
9444 #[tokio::test]
9445 async fn track_offline_bounce_returns_missing_track_error() {
9446 let (mut engine, mut client_rx) = make_engine_with_client();
9447
9448 engine
9449 .handle_request(Action::TrackOfflineBounce {
9450 track_name: "missing".to_string(),
9451 output_path: "/tmp/out.wav".to_string(),
9452 start_sample: 0,
9453 length_samples: 128,
9454 automation_lanes: vec![],
9455 apply_fader: false,
9456 })
9457 .await;
9458
9459 match client_rx.recv().await.expect("response") {
9460 Message::Response(Err(err)) => {
9461 assert_eq!(err, "Track not found: missing");
9462 }
9463 other => panic!("unexpected message: {other:?}"),
9464 }
9465 }
9466
9467 #[tokio::test]
9468 async fn track_offline_bounce_clears_job_when_worker_send_fails() {
9469 let (mut engine, mut client_rx) = make_engine_with_client();
9470 insert_track(
9471 &mut engine,
9472 Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
9473 );
9474 let (worker_tx, worker_rx) = channel(1);
9475 drop(worker_rx);
9476 engine
9477 .workers
9478 .push(WorkerData::new(worker_tx, tokio::spawn(async {})));
9479 engine.ready_workers.push(0);
9480
9481 engine
9482 .handle_request(Action::TrackOfflineBounce {
9483 track_name: "track".to_string(),
9484 output_path: "/tmp/out.wav".to_string(),
9485 start_sample: 0,
9486 length_samples: 128,
9487 automation_lanes: vec![],
9488 apply_fader: false,
9489 })
9490 .await;
9491
9492 assert!(engine.offline_bounce_jobs.is_empty());
9493 match client_rx.recv().await.expect("response") {
9494 Message::Response(Err(err)) => {
9495 assert!(err.contains("Failed to schedule offline bounce"));
9496 }
9497 other => panic!("unexpected message: {other:?}"),
9498 }
9499 }
9500
9501 #[tokio::test]
9502 async fn play_stop_play_keeps_clip_output_audible() {
9503 use crate::audio::clip::AudioClip;
9504 use crate::audio_codec::write_wav_f32;
9505
9506 let (engine_tx, engine_rx) = channel(16);
9507 let mut engine = Engine::new(engine_rx, engine_tx);
9508 let state = engine.state();
9509 let (client_tx, mut client_rx) = channel(16);
9510 engine.clients.push(client_tx);
9511 engine.init().await;
9512
9513 let tmp_dir = std::env::temp_dir().join("maolan_play_stop_play_test");
9514 let _ = std::fs::create_dir_all(&tmp_dir);
9515 let wav_path = tmp_dir.join("tone.wav");
9516 let sample_rate = 48_000u32;
9517 let clip_samples = sample_rate as usize;
9518 let mut samples = Vec::with_capacity(clip_samples);
9519 for i in 0..clip_samples {
9520 let phase = i as f32 / sample_rate as f32 * 2.0 * std::f32::consts::PI * 440.0;
9521 samples.push(phase.sin() * 0.5);
9522 }
9523 write_wav_f32(&wav_path, &samples, 1, sample_rate).expect("write wav");
9524
9525 let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 1024, sample_rate as f64);
9526 let mut clip = AudioClip::new(wav_path.to_string_lossy().to_string(), 0, clip_samples);
9527 clip.fade_enabled = false;
9528 track.audio.clips.push(clip);
9529 track.session_base_dir = Some(tmp_dir.clone());
9530 insert_track(&mut engine, track);
9531
9532 let tx = engine.tx.clone();
9533 let work_handle = tokio::spawn(async move {
9534 engine.work().await;
9535 });
9536
9537 tokio::time::sleep(TokioDuration::from_millis(100)).await;
9539
9540 async fn drain_responses(
9541 client_rx: &mut tokio::sync::mpsc::Receiver<Message>,
9542 count: usize,
9543 ) {
9544 for _ in 0..count {
9545 let _ = tokio::time::timeout(TokioDuration::from_secs(2), client_rx.recv()).await;
9546 }
9547 }
9548
9549 async fn wait_for_track_processed(
9550 client_rx: &mut tokio::sync::mpsc::Receiver<Message>,
9551 state: &Arc<UnsafeMutex<State>>,
9552 ) -> bool {
9553 let deadline = Instant::now() + Duration::from_secs(5);
9554 while Instant::now() < deadline {
9555 let msg =
9556 tokio::time::timeout(TokioDuration::from_millis(100), client_rx.recv()).await;
9557 if let Ok(Some(Message::Response(Ok(Action::TransportPosition(_)))))
9558 | Ok(Some(Message::Response(Ok(Action::Play)))) = msg
9559 {
9560 let track_deadline = Instant::now() + Duration::from_secs(5);
9561 while Instant::now() < track_deadline {
9562 if state
9563 .lock()
9564 .tracks
9565 .get("track")
9566 .map(|t| t.lock().audio.finished)
9567 .unwrap_or(false)
9568 {
9569 return true;
9570 }
9571 tokio::time::sleep(TokioDuration::from_millis(10)).await;
9572 }
9573 }
9574 }
9575 false
9576 }
9577
9578 tx.send(Message::Request(Action::SetClipPlaybackEnabled(true)))
9579 .await
9580 .unwrap();
9581 tx.send(Message::Request(Action::Play)).await.unwrap();
9582 assert!(
9583 wait_for_track_processed(&mut client_rx, &state).await,
9584 "track did not process on first play"
9585 );
9586 let first_peak = {
9587 let state = state.lock();
9588 let track = state.tracks.get("track").expect("track").lock();
9589 let input = track.audio.ins[0].buffer.lock();
9590 crate::simd::peak_abs(input)
9591 };
9592 assert!(
9593 first_peak > 0.001,
9594 "expected audible input on first play, got {first_peak}"
9595 );
9596
9597 tx.send(Message::Request(Action::SetClipPlaybackEnabled(true)))
9598 .await
9599 .unwrap();
9600 tx.send(Message::Request(Action::Stop)).await.unwrap();
9601 drain_responses(&mut client_rx, 2).await;
9602
9603 tx.send(Message::Request(Action::SetClipPlaybackEnabled(true)))
9604 .await
9605 .unwrap();
9606 tx.send(Message::Request(Action::Play)).await.unwrap();
9607 assert!(
9608 wait_for_track_processed(&mut client_rx, &state).await,
9609 "track did not process on second play"
9610 );
9611 let second_peak = {
9612 let state = state.lock();
9613 let track = state.tracks.get("track").expect("track").lock();
9614 let input = track.audio.ins[0].buffer.lock();
9615 crate::simd::peak_abs(input)
9616 };
9617 assert!(
9618 second_peak > 0.001,
9619 "expected audible input on second play after stop, got {second_peak}"
9620 );
9621
9622 let _ = tx.send(Message::Request(Action::Quit)).await;
9623 tokio::time::sleep(TokioDuration::from_millis(200)).await;
9624 work_handle.abort();
9625 let _ = std::fs::remove_dir_all(&tmp_dir);
9626 }
9627
9628 #[test]
9629 fn modulator_sets_track_volume() {
9630 let (mut engine, _client_rx) = make_engine_with_client();
9631 let track = Track::new("vol-track".to_string(), 0, 2, 0, 0, 128, 48_000.0);
9632 insert_track(&mut engine, track);
9633
9634 engine.modulators = vec![crate::modulator::Modulator {
9635 id: 1,
9636 name: "LFO".to_string(),
9637 shape: crate::modulator::ModulatorShape::Sine,
9638 rate_hz: 1.0,
9639 phase: 0.0,
9640 enabled: true,
9641 targets: vec![crate::modulator::ModulatorTarget::TrackVolume {
9642 track_name: "vol-track".to_string(),
9643 min: -90.0,
9644 max: 20.0,
9645 }],
9646 }];
9647
9648 let echoes = engine.apply_modulators(12_000);
9650 let track = engine.state.lock().tracks["vol-track"].lock();
9651 assert!(
9652 (track.level() - 20.0).abs() < 0.01,
9653 "expected 20 dB, got {}",
9654 track.level()
9655 );
9656 assert!(
9657 echoes
9658 .iter()
9659 .any(|a| matches!(a, Action::TrackAutomationLevel(name, _) if name == "vol-track"))
9660 );
9661 }
9662
9663 #[test]
9664 fn modulator_sets_track_balance() {
9665 let (mut engine, _client_rx) = make_engine_with_client();
9666 let track = Track::new("pan-track".to_string(), 0, 2, 0, 0, 128, 48_000.0);
9667 insert_track(&mut engine, track);
9668
9669 engine.modulators = vec![crate::modulator::Modulator {
9670 id: 1,
9671 name: "LFO".to_string(),
9672 shape: crate::modulator::ModulatorShape::Sine,
9673 rate_hz: 1.0,
9674 phase: 0.0,
9675 enabled: true,
9676 targets: vec![crate::modulator::ModulatorTarget::TrackBalance {
9677 track_name: "pan-track".to_string(),
9678 min: -1.0,
9679 max: 1.0,
9680 }],
9681 }];
9682
9683 let echoes = engine.apply_modulators(12_000);
9685 let track = engine.state.lock().tracks["pan-track"].lock();
9686 assert!(
9687 (track.balance - 1.0).abs() < 0.01,
9688 "expected balance 1.0, got {}",
9689 track.balance
9690 );
9691 assert!(
9692 echoes.iter().any(
9693 |a| matches!(a, Action::TrackAutomationBalance(name, _) if name == "pan-track")
9694 )
9695 );
9696 }
9697
9698 #[tokio::test]
9699 async fn track_set_parent_wires_folder_input_to_child_input_and_child_output_to_folder_output()
9700 {
9701 let (mut engine, mut client_rx) = make_engine_with_client();
9702 let folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9703 let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9704 insert_track(&mut engine, folder);
9705 insert_track(&mut engine, child);
9706
9707 engine
9708 .handle_request_inner(
9709 Action::TrackSetParent {
9710 track_name: "child".to_string(),
9711 parent_name: Some("folder".to_string()),
9712 },
9713 false,
9714 )
9715 .await;
9716
9717 while let Ok(Some(_)) =
9719 tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
9720 {}
9721
9722 let state = engine.state.lock();
9723 let folder = state.tracks.get("folder").unwrap().lock();
9724 let child = state.tracks.get("child").unwrap().lock();
9725
9726 assert!(folder.child_tracks.iter().any(|c| c.lock().name == "child"));
9727 assert_eq!(child.parent_track.as_deref(), Some("folder"));
9728
9729 for (i, (parent_in, child_in)) in folder.audio.ins.iter().zip(&child.audio.ins).enumerate()
9731 {
9732 assert!(
9733 child_in
9734 .connections
9735 .lock()
9736 .iter()
9737 .any(|c| Arc::ptr_eq(c, parent_in)),
9738 "folder input {i} is not routed to child input {i}"
9739 );
9740 assert!(
9741 !parent_in
9742 .connections
9743 .lock()
9744 .iter()
9745 .any(|c| Arc::ptr_eq(c, child_in)),
9746 "folder input {i} should not read from child input {i}"
9747 );
9748 }
9749
9750 for (i, (child_out, parent_out)) in
9752 child.audio.outs.iter().zip(&folder.audio.outs).enumerate()
9753 {
9754 assert!(
9755 parent_out
9756 .connections
9757 .lock()
9758 .iter()
9759 .any(|c| Arc::ptr_eq(c, child_out)),
9760 "child output {i} is not routed to folder output {i}"
9761 );
9762 }
9763
9764 for (i, child_out) in child.audio.outs.iter().enumerate() {
9766 assert!(
9767 child_out.connections.lock().iter().any(|c| {
9768 child
9769 .audio
9770 .ins
9771 .get(i)
9772 .is_some_and(|inp| Arc::ptr_eq(c, inp))
9773 }),
9774 "child output {i} is not connected to child input {i}"
9775 );
9776 }
9777 }
9778
9779 #[tokio::test]
9780 async fn track_set_parent_to_none_restores_root_passthrough() {
9781 let (mut engine, mut client_rx) = make_engine_with_client();
9782 let folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9783 let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9784 insert_track(&mut engine, folder);
9785 insert_track(&mut engine, child);
9786
9787 engine
9788 .handle_request_inner(
9789 Action::TrackSetParent {
9790 track_name: "child".to_string(),
9791 parent_name: Some("folder".to_string()),
9792 },
9793 false,
9794 )
9795 .await;
9796 engine
9797 .handle_request_inner(
9798 Action::TrackSetParent {
9799 track_name: "child".to_string(),
9800 parent_name: None,
9801 },
9802 false,
9803 )
9804 .await;
9805
9806 while let Ok(Some(_)) =
9807 tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
9808 {}
9809
9810 let state = engine.state.lock();
9811 let folder = state.tracks.get("folder").unwrap().lock();
9812 let child = state.tracks.get("child").unwrap().lock();
9813
9814 assert!(folder.child_tracks.is_empty());
9815 assert!(child.parent_track.is_none());
9816
9817 for (i, child_out) in child.audio.outs.iter().enumerate() {
9818 assert!(
9819 child_out.connections.lock().iter().any(|c| {
9820 child
9821 .audio
9822 .ins
9823 .get(i)
9824 .is_some_and(|inp| Arc::ptr_eq(c, inp))
9825 }),
9826 "child output {i} should be connected to child input {i} after moving to root"
9827 );
9828 }
9829 }
9830
9831 #[tokio::test]
9832 async fn track_set_parent_wires_folder_midi_to_child_midi() {
9833 let (mut engine, mut client_rx) = make_engine_with_client();
9834 let folder = Track::new_folder("folder".to_string(), 0, 0, 1, 1, 64, 48_000.0);
9835 let child = Track::new("child".to_string(), 0, 0, 1, 1, 64, 48_000.0);
9836 insert_track(&mut engine, folder);
9837 insert_track(&mut engine, child);
9838
9839 engine
9840 .handle_request_inner(
9841 Action::TrackSetParent {
9842 track_name: "child".to_string(),
9843 parent_name: Some("folder".to_string()),
9844 },
9845 false,
9846 )
9847 .await;
9848
9849 while let Ok(Some(_)) =
9850 tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
9851 {}
9852
9853 let state = engine.state.lock();
9854 let folder = state.tracks.get("folder").unwrap().lock();
9855 let child = state.tracks.get("child").unwrap().lock();
9856
9857 let folder_midi_in = &folder.midi.ins[0];
9858 let child_midi_in = &child.midi.ins[0];
9859 assert!(
9860 child_midi_in
9861 .lock()
9862 .connections
9863 .iter()
9864 .any(|c| Arc::ptr_eq(c, folder_midi_in)),
9865 "folder MIDI input should be routed to child MIDI input"
9866 );
9867
9868 let child_midi_out = &child.midi.outs[0];
9869 let folder_midi_out = &folder.midi.outs[0];
9870 assert!(
9871 child_midi_out
9872 .lock()
9873 .connections
9874 .iter()
9875 .any(|c| Arc::ptr_eq(c, folder_midi_out)),
9876 "child MIDI output should be routed to folder MIDI output"
9877 );
9878 }
9879
9880 #[test]
9881 fn nested_folder_expands_in_task_graph() {
9882 let (mut engine, _client_rx) = make_engine_with_client();
9883 let outer = Track::new_folder("outer".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9884 let inner = Track::new_folder("inner".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9885 let leaf = Track::new("leaf".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9886 insert_track(&mut engine, outer);
9887 insert_track(&mut engine, inner);
9888 insert_track(&mut engine, leaf);
9889
9890 {
9891 let state = engine.state.lock();
9892 let outer = state.tracks.get("outer").unwrap().clone();
9893 let inner = state.tracks.get("inner").unwrap().clone();
9894 let leaf = state.tracks.get("leaf").unwrap().clone();
9895 outer.lock().child_tracks.push(inner.clone());
9896 inner.lock().child_tracks.push(leaf.clone());
9897 inner.lock().parent_track = Some("outer".to_string());
9898 leaf.lock().parent_track = Some("inner".to_string());
9899 }
9900
9901 let (tasks, deps) = engine.build_task_graph();
9902 let names: Vec<String> = tasks
9903 .iter()
9904 .map(|t| match t {
9905 ProcessTask::Track(t) => format!("track:{}", t.lock().name.clone()),
9906 ProcessTask::FolderInput(t) => format!("in:{}", t.lock().name.clone()),
9907 ProcessTask::FolderOutput(t) => format!("out:{}", t.lock().name.clone()),
9908 ProcessTask::Plugin { track, .. } => {
9909 format!("plugin:{}", track.lock().name.clone())
9910 }
9911 })
9912 .collect();
9913
9914 let expected = vec![
9915 "in:outer",
9916 "in:inner",
9917 "track:leaf",
9918 "out:inner",
9919 "out:outer",
9920 ];
9921 assert_eq!(names, expected, "task graph should expand nested folders");
9922
9923 for window in tasks.windows(2) {
9925 let prev = &window[0];
9926 let next = &window[1];
9927 let prev_key = Engine::task_key(prev);
9928 let next_key = Engine::task_key(next);
9929 assert!(
9930 deps.get(&next_key).is_some_and(|d| d.contains(&prev_key)),
9931 "{:?} should depend on {:?}",
9932 next,
9933 prev
9934 );
9935 }
9936 }
9937
9938 #[test]
9939 fn child_to_plugin_to_folder_output_task_graph_has_no_cycle() {
9940 use crate::message::ConnectableRef;
9941
9942 let plugin_path = Path::new(env!("CARGO_MANIFEST_DIR"))
9943 .parent()
9944 .unwrap()
9945 .join("daw")
9946 .join("plugin-host")
9947 .join("tests")
9948 .join("test_passthrough.clap");
9949 if !plugin_path.exists() {
9950 return;
9951 }
9952 if crate::plugins::ipc::find_plugin_host_binary().is_none() {
9953 return;
9954 }
9955
9956 let (mut engine, _client_rx) = make_engine_with_client();
9957 let mut folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9958 let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9959
9960 folder
9961 .load_clap_plugin(
9962 &format!("{}::com.maolan.test.passthrough", plugin_path.display()),
9963 None,
9964 )
9965 .expect("should load CLAP plugin on folder");
9966 folder.clap_plugins[0].processor.lock().setup_audio_ports();
9967 let plugin_id = folder.clap_plugins[0].id;
9968
9969 insert_track(&mut engine, folder);
9970 insert_track(&mut engine, child);
9971
9972 {
9973 let state = engine.state.lock();
9974 let folder = state.tracks.get("folder").unwrap().clone();
9975 let child = state.tracks.get("child").unwrap().clone();
9976 folder.lock().child_tracks.push(child.clone());
9977 child.lock().parent_track = Some("folder".to_string());
9978
9979 folder
9980 .lock()
9981 .connect_audio_connectable(
9982 ConnectableRef::ChildTrack("child".to_string()),
9983 0,
9984 ConnectableRef::ClapPlugin(plugin_id),
9985 0,
9986 )
9987 .expect("connect child L to plugin L");
9988 folder
9989 .lock()
9990 .connect_audio_connectable(
9991 ConnectableRef::ChildTrack("child".to_string()),
9992 1,
9993 ConnectableRef::ClapPlugin(plugin_id),
9994 1,
9995 )
9996 .expect("connect child R to plugin R");
9997 folder
9998 .lock()
9999 .connect_audio_connectable(
10000 ConnectableRef::ClapPlugin(plugin_id),
10001 0,
10002 ConnectableRef::TrackOutput,
10003 0,
10004 )
10005 .expect("connect plugin L to folder output L");
10006 folder
10007 .lock()
10008 .connect_audio_connectable(
10009 ConnectableRef::ClapPlugin(plugin_id),
10010 1,
10011 ConnectableRef::TrackOutput,
10012 1,
10013 )
10014 .expect("connect plugin R to folder output R");
10015 }
10016
10017 let (tasks, deps) = engine.build_task_graph();
10018
10019 let folder_in_key = tasks
10020 .iter()
10021 .find(|t| matches!(t, ProcessTask::FolderInput(t) if t.lock().name == "folder"))
10022 .map(Engine::task_key)
10023 .expect("folder input task");
10024 let child_key = tasks
10025 .iter()
10026 .find(|t| matches!(t, ProcessTask::Track(t) if t.lock().name == "child"))
10027 .map(Engine::task_key)
10028 .expect("child task");
10029 let plugin_key = tasks
10030 .iter()
10031 .find(|t| {
10032 matches!(
10033 t,
10034 ProcessTask::Plugin {
10035 track,
10036 kind: PluginKind::Clap,
10037 index: 0,
10038 } if track.lock().name == "folder"
10039 )
10040 })
10041 .map(Engine::task_key)
10042 .expect("plugin task");
10043 let folder_out_key = tasks
10044 .iter()
10045 .find(|t| matches!(t, ProcessTask::FolderOutput(t) if t.lock().name == "folder"))
10046 .map(Engine::task_key)
10047 .expect("folder output task");
10048
10049 assert!(
10050 deps.get(&child_key)
10051 .is_some_and(|d| d.contains(&folder_in_key)),
10052 "child task should depend on folder input"
10053 );
10054 assert!(
10055 deps.get(&plugin_key)
10056 .is_some_and(|d| d.contains(&folder_in_key) && d.contains(&child_key)),
10057 "plugin task should depend on folder input and child"
10058 );
10059 assert!(
10060 deps.get(&folder_out_key).is_some_and(|d| {
10061 d.contains(&folder_in_key) && d.contains(&plugin_key) && d.contains(&child_key)
10062 }),
10063 "folder output should depend on folder input, plugin, and child"
10064 );
10065
10066 fn has_cycle(deps: &HashMap<String, Vec<String>>) -> bool {
10067 let mut state: HashMap<String, u8> = HashMap::new();
10068 fn visit(
10069 node: &str,
10070 deps: &HashMap<String, Vec<String>>,
10071 state: &mut HashMap<String, u8>,
10072 ) -> bool {
10073 match state.get(node).copied() {
10074 Some(1) => return true,
10075 Some(2) => return false,
10076 _ => {}
10077 }
10078 state.insert(node.to_string(), 1);
10079 for next in deps.get(node).into_iter().flatten() {
10080 if visit(next, deps, state) {
10081 return true;
10082 }
10083 }
10084 state.insert(node.to_string(), 2);
10085 false
10086 }
10087 for node in deps.keys() {
10088 if visit(node, deps, &mut state) {
10089 return true;
10090 }
10091 }
10092 false
10093 }
10094
10095 assert!(
10096 !has_cycle(&deps),
10097 "task graph should not contain a cycle when a plugin reads from a child track"
10098 );
10099 }
10100
10101 #[tokio::test]
10102 async fn track_set_parent_wires_child_io_to_folder_even_after_addtrack() {
10103 let (mut engine, mut client_rx) = make_engine_with_client();
10104 let folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
10105 let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
10106 insert_track(&mut engine, folder);
10107 insert_track(&mut engine, child);
10108
10109 engine
10110 .handle_request_inner(
10111 Action::TrackSetParent {
10112 track_name: "child".to_string(),
10113 parent_name: Some("folder".to_string()),
10114 },
10115 false,
10116 )
10117 .await;
10118
10119 while let Ok(Some(_)) =
10120 tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
10121 {}
10122
10123 let state = engine.state.lock();
10124 let folder = state.tracks.get("folder").unwrap().lock();
10125 let child = state.tracks.get("child").unwrap().lock();
10126
10127 for (i, (parent_in, child_in)) in folder.audio.ins.iter().zip(&child.audio.ins).enumerate()
10129 {
10130 assert!(
10131 child_in
10132 .connections
10133 .lock()
10134 .iter()
10135 .any(|c| Arc::ptr_eq(c, parent_in)),
10136 "folder input {i} is not routed to child input {i}"
10137 );
10138 }
10139
10140 for (i, (child_out, parent_out)) in
10142 child.audio.outs.iter().zip(&folder.audio.outs).enumerate()
10143 {
10144 assert!(
10145 parent_out
10146 .connections
10147 .lock()
10148 .iter()
10149 .any(|c| Arc::ptr_eq(c, child_out)),
10150 "child output {i} is not routed to folder output {i}"
10151 );
10152 }
10153 }
10154
10155 #[tokio::test]
10156 async fn folder_child_audio_passes_through() {
10157 let (mut engine, mut client_rx) = make_engine_with_client();
10158 let folder = Track::new_folder("folder".to_string(), 1, 1, 0, 0, 64, 48_000.0);
10159 let child = Track::new("child".to_string(), 1, 1, 0, 0, 64, 48_000.0);
10160 insert_track(&mut engine, folder);
10161 insert_track(&mut engine, child);
10162
10163 engine
10164 .handle_request_inner(
10165 Action::TrackSetParent {
10166 track_name: "child".to_string(),
10167 parent_name: Some("folder".to_string()),
10168 },
10169 false,
10170 )
10171 .await;
10172 while let Ok(Some(_)) =
10173 tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
10174 {}
10175
10176 {
10177 let state = engine.state.lock();
10178 let folder = state.tracks.get("folder").unwrap().clone();
10179 let child = state.tracks.get("child").unwrap().clone();
10180
10181 folder.lock().input_monitor = vec![true];
10182 child.lock().input_monitor = vec![true];
10183
10184 let source = Arc::new(crate::audio::io::AudioIO::new(64));
10186 for sample in source.buffer.lock().iter_mut() {
10187 *sample = 0.75;
10188 }
10189 crate::audio::io::AudioIO::connect(&source, &folder.lock().audio.ins[0]);
10190
10191 folder.lock().process_folder_input();
10192 child.lock().process();
10193 folder.lock().process_folder_output();
10194
10195 let output = folder.lock().audio.outs[0].buffer.lock();
10196 assert!(
10197 output.iter().any(|s| (*s - 0.75).abs() < 1e-5),
10198 "folder output should contain the child-processed folder input signal, got {:?}",
10199 output.iter().take(8).collect::<Vec<_>>()
10200 );
10201 }
10202 }
10203
10204 #[tokio::test]
10205 async fn remove_folder_track_deletes_descendants_recursively() {
10206 let (mut engine, mut client_rx) = make_engine_with_client();
10207 let folder = Track::new_folder("folder".to_string(), 1, 1, 0, 0, 64, 48_000.0);
10208 let child = Track::new_folder("child".to_string(), 1, 1, 0, 0, 64, 48_000.0);
10209 let grandchild = Track::new("grandchild".to_string(), 1, 1, 0, 0, 64, 48_000.0);
10210 insert_track(&mut engine, folder);
10211 insert_track(&mut engine, child);
10212 insert_track(&mut engine, grandchild);
10213
10214 engine
10215 .handle_request(Action::TrackSetParent {
10216 track_name: "child".to_string(),
10217 parent_name: Some("folder".to_string()),
10218 })
10219 .await;
10220 engine
10221 .handle_request(Action::TrackSetParent {
10222 track_name: "grandchild".to_string(),
10223 parent_name: Some("child".to_string()),
10224 })
10225 .await;
10226
10227 while let Ok(Some(_)) =
10229 tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
10230 {}
10231
10232 engine
10233 .handle_request(Action::RemoveTrack("folder".to_string()))
10234 .await;
10235
10236 {
10237 let state = engine.state.lock();
10238 assert!(
10239 !state.tracks.contains_key("folder"),
10240 "folder should have been removed"
10241 );
10242 assert!(
10243 !state.tracks.contains_key("child"),
10244 "child should have been removed"
10245 );
10246 assert!(
10247 !state.tracks.contains_key("grandchild"),
10248 "grandchild should have been removed"
10249 );
10250 }
10251
10252 let mut removed_names = Vec::new();
10253 for _ in 0..3 {
10254 let msg = tokio::time::timeout(TokioDuration::from_millis(100), client_rx.recv()).await;
10255 if let Ok(Some(Message::Response(Ok(Action::RemoveTrack(name))))) = msg {
10256 removed_names.push(name);
10257 }
10258 }
10259 assert_eq!(
10260 removed_names,
10261 vec!["grandchild", "child", "folder"],
10262 "descendants should be removed before the folder and clients notified"
10263 );
10264 }
10265}