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 self.notify_clients(Err(e)).await;
6636 }
6637 }
6638 return;
6639 }
6640 #[cfg(all(unix, not(target_os = "macos")))]
6641 Action::Lv2Plugins(_) => {}
6642 Action::ListVst3Plugins => {
6643 match crate::plugins::scan_plugins::<crate::plugins::types::Vst3PluginInfo>("vst3")
6644 {
6645 Ok(plugins) => {
6646 self.notify_clients(Ok(Action::Vst3Plugins(plugins))).await;
6647 }
6648 Err(e) => {
6649 self.notify_clients(Err(e)).await;
6650 }
6651 }
6652 return;
6653 }
6654 Action::Vst3Plugins(_) => {}
6655 Action::ListClapPlugins => {
6656 match crate::plugins::scan_plugins::<crate::plugins::types::ClapPluginInfo>("clap")
6657 {
6658 Ok(plugins) => {
6659 self.notify_clients(Ok(Action::ClapPlugins(plugins))).await;
6660 }
6661 Err(e) => {
6662 self.notify_clients(Err(e)).await;
6663 }
6664 }
6665 return;
6666 }
6667 Action::ListClapPluginsWithCapabilities => {
6668 match crate::plugins::scan_plugins::<crate::plugins::types::ClapPluginInfo>("clap")
6669 {
6670 Ok(plugins) => {
6671 self.notify_clients(Ok(Action::ClapPlugins(plugins))).await;
6672 }
6673 Err(e) => {
6674 self.notify_clients(Err(e)).await;
6675 }
6676 }
6677 return;
6678 }
6679 Action::ClapPlugins(_) => {}
6680 Action::TrackLoadClapPlugin {
6681 ref track_name,
6682 ref plugin_path,
6683 instance_id,
6684 } => {
6685 if self
6686 .reject_if_track_frozen(track_name, "CLAP plugin loading")
6687 .await
6688 {
6689 return;
6690 }
6691 let track = match self.track_handle_or_err(track_name) {
6692 Ok(track) => track,
6693 Err(e) => {
6694 self.notify_clients(Err(e)).await;
6695 return;
6696 }
6697 };
6698 let track = track.lock();
6699 if track.audio.processing {
6700 self.notify_clients(Err(format!(
6701 "Track '{}' is currently processing audio; stop playback before loading CLAP plugins",
6702 track_name
6703 )))
6704 .await;
6705 return;
6706 }
6707 if let Err(e) = track.load_clap_plugin(plugin_path, instance_id) {
6708 self.notify_clients(Err(e)).await;
6709 return;
6710 }
6711 self.notify_clients(Ok(Action::Log {
6712 source: "engine".to_string(),
6713 message: format!("CLAP plugin loaded on track '{track_name}': {plugin_path}"),
6714 }))
6715 .await;
6716 if let Some(instance) = track.clap_plugins.last()
6717 && let Some(stderr) = instance.processor.lock().take_stderr()
6718 {
6719 let source = format!("clap:{plugin_path}");
6720 self.spawn_plugin_host_stderr_reader(stderr, source);
6721 self.notify_clients(Ok(Action::Log {
6722 source: "engine".to_string(),
6723 message: format!(
6724 "Attached stderr reader for CLAP plugin on track '{track_name}'"
6725 ),
6726 }))
6727 .await;
6728 }
6729 }
6730 Action::TrackUnloadClapPlugin {
6731 ref track_name,
6732 ref plugin_path,
6733 } => {
6734 if self
6735 .reject_if_track_frozen(track_name, "CLAP plugin unloading")
6736 .await
6737 {
6738 return;
6739 }
6740 let track = match self.track_handle_or_err(track_name) {
6741 Ok(track) => track,
6742 Err(e) => {
6743 self.notify_clients(Err(e)).await;
6744 return;
6745 }
6746 };
6747 let track = track.lock();
6748 if track.audio.processing {
6749 self.notify_clients(Err(format!(
6750 "Track '{}' is currently processing audio; stop playback before unloading CLAP plugins",
6751 track_name
6752 )))
6753 .await;
6754 return;
6755 }
6756 if let Err(e) = track.unload_clap_plugin(plugin_path) {
6757 self.notify_clients(Err(e)).await;
6758 return;
6759 }
6760 }
6761 Action::TrackUnloadClapPluginInstance {
6762 ref track_name,
6763 instance_id,
6764 } => {
6765 if self
6766 .reject_if_track_frozen(track_name, "CLAP plugin unloading")
6767 .await
6768 {
6769 return;
6770 }
6771 let track = match self.track_handle_or_err(track_name) {
6772 Ok(track) => track,
6773 Err(e) => {
6774 self.notify_clients(Err(e)).await;
6775 return;
6776 }
6777 };
6778 let track = track.lock();
6779 if track.audio.processing {
6780 self.notify_clients(Err(format!(
6781 "Track '{}' is currently processing audio; stop playback before unloading CLAP plugins",
6782 track_name
6783 )))
6784 .await;
6785 return;
6786 }
6787 if let Err(e) = track.unload_clap_plugin_instance(instance_id) {
6788 self.notify_clients(Err(e)).await;
6789 return;
6790 }
6791 }
6792 Action::TrackShowClapGui {
6793 ref track_name,
6794 instance_id,
6795 } => {
6796 let track = match self.track_handle_or_err(track_name) {
6797 Ok(track) => track,
6798 Err(e) => {
6799 self.notify_clients(Err(e)).await;
6800 return;
6801 }
6802 };
6803 if let Err(e) = track.lock().show_clap_gui(instance_id) {
6804 self.notify_clients(Err(e)).await;
6805 return;
6806 }
6807 }
6808 Action::TrackLoadVst3Plugin {
6809 ref track_name,
6810 ref plugin_path,
6811 instance_id,
6812 } => {
6813 if self
6814 .reject_if_track_frozen(track_name, "VST3 plugin loading")
6815 .await
6816 {
6817 return;
6818 }
6819 let track = match self.track_handle_or_err(track_name) {
6820 Ok(track) => track,
6821 Err(e) => {
6822 self.notify_clients(Err(e)).await;
6823 return;
6824 }
6825 };
6826 let track = track.lock();
6827 if track.audio.processing {
6828 self.notify_clients(Err(format!(
6829 "Track '{}' is currently processing audio; stop playback before loading VST3 plugins",
6830 track_name
6831 )))
6832 .await;
6833 return;
6834 }
6835 if let Err(e) = track.load_vst3_plugin(plugin_path, instance_id) {
6836 self.notify_clients(Err(e)).await;
6837 return;
6838 }
6839 if let Some(instance) = track.vst3_plugins.last()
6840 && let Some(stderr) = instance.processor.lock().take_stderr()
6841 {
6842 let source = format!("vst3:{plugin_path}");
6843 self.spawn_plugin_host_stderr_reader(stderr, source);
6844 }
6845 }
6846 Action::TrackUnloadVst3Plugin {
6847 ref track_name,
6848 ref plugin_path,
6849 } => {
6850 if self
6851 .reject_if_track_frozen(track_name, "VST3 plugin unloading")
6852 .await
6853 {
6854 return;
6855 }
6856 let track = match self.track_handle_or_err(track_name) {
6857 Ok(track) => track,
6858 Err(e) => {
6859 self.notify_clients(Err(e)).await;
6860 return;
6861 }
6862 };
6863 let track = track.lock();
6864 if track.audio.processing {
6865 self.notify_clients(Err(format!(
6866 "Track '{}' is currently processing audio; stop playback before unloading VST3 plugins",
6867 track_name
6868 )))
6869 .await;
6870 return;
6871 }
6872 if let Err(e) = track.unload_vst3_plugin(plugin_path) {
6873 self.notify_clients(Err(e)).await;
6874 return;
6875 }
6876 }
6877 Action::TrackUnloadVst3PluginInstance {
6878 ref track_name,
6879 instance_id,
6880 } => {
6881 if self
6882 .reject_if_track_frozen(track_name, "VST3 plugin unloading")
6883 .await
6884 {
6885 return;
6886 }
6887 let track = match self.track_handle_or_err(track_name) {
6888 Ok(track) => track,
6889 Err(e) => {
6890 self.notify_clients(Err(e)).await;
6891 return;
6892 }
6893 };
6894 let track = track.lock();
6895 if track.audio.processing {
6896 self.notify_clients(Err(format!(
6897 "Track '{}' is currently processing audio; stop playback before unloading VST3 plugins",
6898 track_name
6899 )))
6900 .await;
6901 return;
6902 }
6903 if let Err(e) = track.unload_vst3_plugin_instance(instance_id) {
6904 self.notify_clients(Err(e)).await;
6905 return;
6906 }
6907 }
6908 Action::TrackShowVst3Gui {
6909 ref track_name,
6910 instance_id,
6911 } => {
6912 let track = match self.track_handle_or_err(track_name) {
6913 Ok(track) => track,
6914 Err(e) => {
6915 self.notify_clients(Err(e)).await;
6916 return;
6917 }
6918 };
6919 if let Err(e) = track.lock().show_vst3_gui(instance_id) {
6920 self.notify_clients(Err(e)).await;
6921 return;
6922 }
6923 }
6924 #[cfg(all(unix, not(target_os = "macos")))]
6925 Action::TrackLoadLv2Plugin {
6926 ref track_name,
6927 ref plugin_uri,
6928 instance_id,
6929 } => {
6930 if self
6931 .reject_if_track_frozen(track_name, "LV2 plugin loading")
6932 .await
6933 {
6934 return;
6935 }
6936 let track = match self.track_handle_or_err(track_name) {
6937 Ok(track) => track,
6938 Err(e) => {
6939 self.notify_clients(Err(e)).await;
6940 return;
6941 }
6942 };
6943 let track = track.lock();
6944 if track.audio.processing {
6945 self.notify_clients(Err(format!(
6946 "Track '{}' is currently processing audio; stop playback before loading LV2 plugins",
6947 track_name
6948 )))
6949 .await;
6950 return;
6951 }
6952 if let Err(e) = track.load_lv2_plugin(plugin_uri, instance_id) {
6953 self.notify_clients(Err(e)).await;
6954 return;
6955 }
6956 if let Some(instance) = track.lv2_plugins.last()
6957 && let Some(stderr) = instance.processor.lock().take_stderr()
6958 {
6959 let source = format!("lv2:{plugin_uri}");
6960 self.spawn_plugin_host_stderr_reader(stderr, source);
6961 }
6962 }
6963 #[cfg(all(unix, not(target_os = "macos")))]
6964 Action::TrackUnloadLv2Plugin {
6965 ref track_name,
6966 ref plugin_uri,
6967 } => {
6968 if self
6969 .reject_if_track_frozen(track_name, "LV2 plugin unloading")
6970 .await
6971 {
6972 return;
6973 }
6974 let track = match self.track_handle_or_err(track_name) {
6975 Ok(track) => track,
6976 Err(e) => {
6977 self.notify_clients(Err(e)).await;
6978 return;
6979 }
6980 };
6981 let track = track.lock();
6982 if track.audio.processing {
6983 self.notify_clients(Err(format!(
6984 "Track '{}' is currently processing audio; stop playback before unloading LV2 plugins",
6985 track_name
6986 )))
6987 .await;
6988 return;
6989 }
6990 if let Err(e) = track.unload_lv2_plugin(plugin_uri) {
6991 self.notify_clients(Err(e)).await;
6992 return;
6993 }
6994 }
6995 #[cfg(all(unix, not(target_os = "macos")))]
6996 Action::TrackUnloadLv2PluginInstance {
6997 ref track_name,
6998 instance_id,
6999 } => {
7000 if self
7001 .reject_if_track_frozen(track_name, "LV2 plugin unloading")
7002 .await
7003 {
7004 return;
7005 }
7006 let track = match self.track_handle_or_err(track_name) {
7007 Ok(track) => track,
7008 Err(e) => {
7009 self.notify_clients(Err(e)).await;
7010 return;
7011 }
7012 };
7013 let track = track.lock();
7014 if track.audio.processing {
7015 self.notify_clients(Err(format!(
7016 "Track '{}' is currently processing audio; stop playback before unloading LV2 plugins",
7017 track_name
7018 )))
7019 .await;
7020 return;
7021 }
7022 if let Err(e) = track.unload_lv2_plugin_instance(instance_id) {
7023 self.notify_clients(Err(e)).await;
7024 return;
7025 }
7026 }
7027 #[cfg(all(unix, not(target_os = "macos")))]
7028 Action::TrackShowLv2Gui {
7029 ref track_name,
7030 instance_id,
7031 } => {
7032 let track = match self.track_handle_or_err(track_name) {
7033 Ok(track) => track,
7034 Err(e) => {
7035 self.notify_clients(Err(e)).await;
7036 return;
7037 }
7038 };
7039 if let Err(e) = track.lock().show_lv2_gui(instance_id) {
7040 self.notify_clients(Err(e)).await;
7041 return;
7042 }
7043 }
7044 Action::TrackSetPluginResourceDir {
7045 ref track_name,
7046 instance_id,
7047 ref format,
7048 ref directory,
7049 } => {
7050 let track = match self.track_handle_or_err(track_name) {
7051 Ok(track) => track,
7052 Err(e) => {
7053 self.notify_clients(Err(e)).await;
7054 return;
7055 }
7056 };
7057 let dir = std::path::Path::new(directory);
7058 let result = if format.eq_ignore_ascii_case("CLAP") {
7059 track.lock().set_clap_plugin_resource_dir(instance_id, dir)
7060 } else if format.eq_ignore_ascii_case("LV2") {
7061 #[cfg(all(unix, not(target_os = "macos")))]
7062 {
7063 track.lock().set_lv2_plugin_resource_dir(instance_id, dir)
7064 }
7065 #[cfg(not(all(unix, not(target_os = "macos"))))]
7066 Err("LV2 is not supported on this platform".to_string())
7067 } else {
7068 Err(format!(
7069 "Unsupported plugin format for resource dir: {format}"
7070 ))
7071 };
7072 if let Err(e) = result {
7073 self.notify_clients(Err(e)).await;
7074 return;
7075 }
7076 }
7077 Action::TrackClapFileReferences {
7078 ref track_name,
7079 instance_id,
7080 refs: _,
7081 } => match self.track_handle_or_err(track_name) {
7082 Ok(track) => {
7083 let refs = track.lock().clap_file_references(instance_id).unwrap_or_else(|e| {
7084 tracing::warn!(track_name = %track_name, instance_id, error = %e, "Failed to enumerate CLAP file references");
7085 Vec::new()
7086 });
7087 self.notify_clients(Ok(Action::TrackClapFileReferences {
7088 track_name: track_name.clone(),
7089 instance_id,
7090 refs,
7091 }))
7092 .await;
7093 }
7094 Err(e) => {
7095 self.notify_clients(Err(e)).await;
7096 }
7097 },
7098 Action::TrackUpdateClapFileReference {
7099 ref track_name,
7100 instance_id,
7101 index,
7102 ref path,
7103 } => {
7104 let track = match self.track_handle_or_err(track_name) {
7105 Ok(track) => track,
7106 Err(e) => {
7107 self.notify_clients(Err(e)).await;
7108 return;
7109 }
7110 };
7111 if let Err(e) = track
7112 .lock()
7113 .update_clap_file_reference(instance_id, index, path)
7114 {
7115 self.notify_clients(Err(e)).await;
7116 return;
7117 }
7118 }
7119 Action::ClipSetPluginResourceDir {
7120 ref track_name,
7121 clip_idx,
7122 instance_id,
7123 ref format,
7124 ref directory,
7125 } => {
7126 let track = match self.track_handle_or_err(track_name) {
7127 Ok(track) => track,
7128 Err(e) => {
7129 self.notify_clients(Err(e)).await;
7130 return;
7131 }
7132 };
7133 let dir = std::path::Path::new(directory);
7134 let track = track.lock();
7135 let result = if format.eq_ignore_ascii_case("CLAP") {
7136 track.clip_set_clap_plugin_resource_dir(clip_idx, instance_id, dir)
7137 } else if format.eq_ignore_ascii_case("LV2") {
7138 #[cfg(all(unix, not(target_os = "macos")))]
7139 {
7140 track.clip_set_lv2_plugin_resource_dir(clip_idx, instance_id, dir)
7141 }
7142 #[cfg(not(all(unix, not(target_os = "macos"))))]
7143 Err("LV2 is not supported on this platform".to_string())
7144 } else {
7145 Err(format!(
7146 "Unsupported plugin format for resource dir: {format}"
7147 ))
7148 };
7149 if let Err(e) = result {
7150 self.notify_clients(Err(e)).await;
7151 return;
7152 }
7153 }
7154 Action::ClipClapFileReferences {
7155 ref track_name,
7156 clip_idx,
7157 instance_id,
7158 refs: _,
7159 } => match self.track_handle_or_err(track_name) {
7160 Ok(track) => {
7161 let track = track.lock();
7162 let refs = track
7163 .clip_clap_file_references(clip_idx, instance_id)
7164 .unwrap_or_else(|e| {
7165 tracing::warn!(
7166 track_name = %track_name,
7167 clip_idx,
7168 instance_id,
7169 error = %e,
7170 "Failed to enumerate clip CLAP file references"
7171 );
7172 Vec::new()
7173 });
7174 self.notify_clients(Ok(Action::ClipClapFileReferences {
7175 track_name: track_name.clone(),
7176 clip_idx,
7177 instance_id,
7178 refs,
7179 }))
7180 .await;
7181 }
7182 Err(e) => {
7183 self.notify_clients(Err(e)).await;
7184 }
7185 },
7186 Action::ClipUpdateClapFileReference {
7187 ref track_name,
7188 clip_idx,
7189 instance_id,
7190 index,
7191 ref path,
7192 } => {
7193 let track = match self.track_handle_or_err(track_name) {
7194 Ok(track) => track,
7195 Err(e) => {
7196 self.notify_clients(Err(e)).await;
7197 return;
7198 }
7199 };
7200 if let Err(e) =
7201 track
7202 .lock()
7203 .clip_update_clap_file_reference(clip_idx, instance_id, index, path)
7204 {
7205 self.notify_clients(Err(e)).await;
7206 return;
7207 }
7208 }
7209 Action::TrackSetClapParameter {
7210 ref track_name,
7211 instance_id,
7212 param_id,
7213 value,
7214 } => {
7215 if self
7216 .reject_if_track_frozen(track_name, "CLAP parameter changes")
7217 .await
7218 {
7219 return;
7220 }
7221 match self.track_handle_or_err(track_name) {
7222 Ok(track) => {
7223 if let Err(e) =
7224 track
7225 .lock()
7226 .set_clap_parameter(instance_id, param_id, value)
7227 {
7228 self.notify_clients(Err(e)).await;
7229 return;
7230 }
7231 self.notify_clients(Ok(a.clone())).await;
7232 }
7233 Err(e) => {
7234 self.notify_clients(Err(e)).await;
7235 }
7236 }
7237 }
7238 Action::ClipSetClapParameter {
7239 ref track_name,
7240 clip_idx,
7241 instance_id,
7242 param_id,
7243 value,
7244 } => {
7245 if self
7246 .reject_if_track_frozen(track_name, "CLAP parameter changes")
7247 .await
7248 {
7249 return;
7250 }
7251 match self.track_handle_or_err(track_name) {
7252 Ok(track) => {
7253 if let Err(e) = track.lock().clip_set_clap_parameter(
7254 clip_idx,
7255 instance_id,
7256 param_id,
7257 value,
7258 ) {
7259 self.notify_clients(Err(e)).await;
7260 return;
7261 }
7262 self.notify_clients(Ok(a.clone())).await;
7263 }
7264 Err(e) => {
7265 self.notify_clients(Err(e)).await;
7266 }
7267 }
7268 }
7269 Action::TrackSetClapParameterAt {
7270 ref track_name,
7271 instance_id,
7272 param_id,
7273 value,
7274 frame,
7275 } => {
7276 if self
7277 .reject_if_track_frozen(track_name, "CLAP parameter changes")
7278 .await
7279 {
7280 return;
7281 }
7282 match self.track_handle_or_err(track_name) {
7283 Ok(track) => {
7284 if let Err(e) =
7285 track
7286 .lock()
7287 .set_clap_parameter_at(instance_id, param_id, value, frame)
7288 {
7289 self.notify_clients(Err(e)).await;
7290 return;
7291 }
7292 self.notify_clients(Ok(a.clone())).await;
7293 }
7294 Err(e) => {
7295 self.notify_clients(Err(e)).await;
7296 }
7297 }
7298 }
7299 Action::TrackBeginClapParameterEdit {
7300 ref track_name,
7301 instance_id,
7302 param_id,
7303 frame,
7304 } => {
7305 if self
7306 .reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
7307 .await
7308 {
7309 return;
7310 }
7311 match self.track_handle_or_err(track_name) {
7312 Ok(track) => {
7313 if let Err(e) =
7314 track
7315 .lock()
7316 .begin_clap_parameter_edit(instance_id, param_id, frame)
7317 {
7318 self.notify_clients(Err(e)).await;
7319 return;
7320 }
7321 self.notify_clients(Ok(a.clone())).await;
7322 }
7323 Err(e) => {
7324 self.notify_clients(Err(e)).await;
7325 }
7326 }
7327 }
7328 Action::TrackEndClapParameterEdit {
7329 ref track_name,
7330 instance_id,
7331 param_id,
7332 frame,
7333 } => {
7334 if self
7335 .reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
7336 .await
7337 {
7338 return;
7339 }
7340 match self.track_handle_or_err(track_name) {
7341 Ok(track) => {
7342 if let Err(e) =
7343 track
7344 .lock()
7345 .end_clap_parameter_edit(instance_id, param_id, frame)
7346 {
7347 self.notify_clients(Err(e)).await;
7348 return;
7349 }
7350 self.notify_clients(Ok(a.clone())).await;
7351 }
7352 Err(e) => {
7353 self.notify_clients(Err(e)).await;
7354 }
7355 }
7356 }
7357 Action::TrackGetClapParameters {
7358 ref track_name,
7359 instance_id,
7360 } => match self.track_handle_or_err(track_name) {
7361 Ok(track) => match track.lock().get_clap_parameters(instance_id) {
7362 Ok(parameters) => {
7363 self.notify_clients(Ok(Action::TrackClapParameters {
7364 track_name: track_name.clone(),
7365 instance_id,
7366 parameters,
7367 }))
7368 .await;
7369 }
7370 Err(e) => {
7371 self.notify_clients(Err(e)).await;
7372 }
7373 },
7374 Err(e) => {
7375 self.notify_clients(Err(e)).await;
7376 }
7377 },
7378 Action::TrackClapParameters { .. } => {}
7379 Action::TrackClapSnapshotState {
7380 ref track_name,
7381 instance_id,
7382 } => match self.track_handle_or_err(track_name) {
7383 Ok(track) => {
7384 let plugin_path = track
7385 .lock()
7386 .clap_plugins
7387 .iter()
7388 .find(|instance| instance.id == instance_id)
7389 .map(|instance| instance.processor.lock().path().to_string())
7390 .unwrap_or_default();
7391 match track.lock().clap_snapshot_state(instance_id) {
7392 Ok(state) => {
7393 self.notify_clients(Ok(Action::TrackClapStateSnapshot {
7394 track_name: track_name.clone(),
7395 instance_id,
7396 plugin_path,
7397 state,
7398 }))
7399 .await;
7400 }
7401 Err(e) => {
7402 self.notify_clients(Err(e)).await;
7403 }
7404 }
7405 }
7406 Err(e) => {
7407 self.notify_clients(Err(e)).await;
7408 }
7409 },
7410 Action::ClipClapSnapshotState {
7411 ref track_name,
7412 clip_idx,
7413 instance_id,
7414 } => match self.track_handle_or_err(track_name) {
7415 Ok(track) => match track.lock().clip_clap_snapshot_state(clip_idx, instance_id) {
7416 Ok((plugin_path, state)) => {
7417 self.notify_clients(Ok(Action::ClipClapStateSnapshot {
7418 track_name: track_name.clone(),
7419 clip_idx,
7420 instance_id,
7421 plugin_path,
7422 state,
7423 }))
7424 .await;
7425 }
7426 Err(e) => {
7427 self.notify_clients(Err(e)).await;
7428 }
7429 },
7430 Err(e) => {
7431 self.notify_clients(Err(e)).await;
7432 }
7433 },
7434 Action::TrackClapStateSnapshot { .. } => {}
7435 Action::ClipClapStateSnapshot { .. } => {}
7436 Action::TrackClapStateDirty { .. } => {}
7437 Action::ClipClapStateDirty { .. } => {}
7438 Action::TrackClapRestoreState {
7439 ref track_name,
7440 instance_id,
7441 ref state,
7442 } => {
7443 if self
7444 .reject_if_track_frozen(track_name, "CLAP state restore")
7445 .await
7446 {
7447 return;
7448 }
7449 let track = match self.track_handle_or_err(track_name) {
7450 Ok(track) => track,
7451 Err(e) => {
7452 self.notify_clients(Err(e)).await;
7453 return;
7454 }
7455 };
7456 let track = track.lock();
7457 if track.audio.processing {
7458 self.notify_clients(Err(format!(
7459 "Track '{}' is currently processing audio; stop playback before restoring CLAP state",
7460 track_name
7461 )))
7462 .await;
7463 return;
7464 }
7465 if let Err(e) = track.clap_restore_state(instance_id, state) {
7466 self.notify_clients(Err(e)).await;
7467 return;
7468 }
7469 }
7470 Action::ClipClapRestoreState {
7471 ref track_name,
7472 clip_idx,
7473 instance_id,
7474 ref state,
7475 } => {
7476 if self
7477 .reject_if_track_frozen(track_name, "CLAP state restore")
7478 .await
7479 {
7480 return;
7481 }
7482 let track = match self.track_handle_or_err(track_name) {
7483 Ok(track) => track,
7484 Err(e) => {
7485 self.notify_clients(Err(e)).await;
7486 return;
7487 }
7488 };
7489 let track = track.lock();
7490 if track.audio.processing {
7491 self.notify_clients(Err(format!(
7492 "Track '{}' is currently processing audio; stop playback before restoring CLAP state",
7493 track_name
7494 )))
7495 .await;
7496 return;
7497 }
7498 if let Err(e) = track.clip_clap_restore_state(clip_idx, instance_id, state) {
7499 self.notify_clients(Err(e)).await;
7500 return;
7501 }
7502 }
7503 Action::TrackSnapshotAllClapStates { ref track_name } => {
7504 let track = match self.track_handle_or_err(track_name) {
7505 Ok(track) => track,
7506 Err(e) => {
7507 self.notify_clients(Err(e)).await;
7508 return;
7509 }
7510 };
7511 let instances: Vec<_> = {
7512 let locked = track.lock();
7513 locked
7514 .clap_plugins
7515 .iter()
7516 .map(|i| (i.id, i.processor.lock().path().to_string()))
7517 .collect()
7518 };
7519 for (instance_id, plugin_path) in instances {
7520 match track.lock().clap_snapshot_state(instance_id) {
7521 Ok(state) => {
7522 self.notify_clients(Ok(Action::TrackClapStateSnapshot {
7523 track_name: track_name.clone(),
7524 instance_id,
7525 plugin_path,
7526 state,
7527 }))
7528 .await;
7529 }
7530 Err(_e) => {}
7531 }
7532 }
7533 self.notify_clients(Ok(Action::TrackSnapshotAllClapStatesDone {
7534 track_name: track_name.clone(),
7535 }))
7536 .await;
7537 }
7538 Action::TrackSnapshotAllClapStatesDone { .. } => {}
7539 Action::TrackGetVst3Graph { ref track_name } => {
7540 match self.track_handle_or_err(track_name) {
7541 Ok(track) => {
7542 let t = track.lock();
7543 let plugins = t.vst3_graph_plugins();
7544 let connections = t.vst3_graph_connections();
7545 self.notify_clients(Ok(Action::TrackVst3Graph {
7546 track_name: track_name.clone(),
7547 plugins,
7548 connections,
7549 }))
7550 .await;
7551 }
7552 Err(e) => {
7553 self.notify_clients(Err(e)).await;
7554 }
7555 }
7556 }
7557 Action::TrackVst3Graph { .. } => {}
7558 Action::TrackSetVst3Parameter {
7559 ref track_name,
7560 instance_id,
7561 param_id,
7562 value,
7563 } => {
7564 if self
7565 .reject_if_track_frozen(track_name, "VST3 parameter changes")
7566 .await
7567 {
7568 return;
7569 }
7570 match self.track_handle_or_err(track_name) {
7571 Ok(track) => {
7572 if let Err(e) =
7573 track
7574 .lock()
7575 .set_vst3_parameter(instance_id, param_id, value)
7576 {
7577 self.notify_clients(Err(e)).await;
7578 return;
7579 }
7580 self.notify_clients(Ok(a.clone())).await;
7581 }
7582 Err(e) => {
7583 self.notify_clients(Err(e)).await;
7584 }
7585 }
7586 }
7587 Action::TrackSetPluginBypassed {
7588 ref track_name,
7589 instance_id,
7590 ref format,
7591 bypassed,
7592 } => match self.track_handle_or_err(track_name) {
7593 Ok(track) => {
7594 let result = match format.as_str() {
7595 "CLAP" => track.lock().set_clap_plugin_bypassed(instance_id, bypassed),
7596 "VST3" => track.lock().set_vst3_plugin_bypassed(instance_id, bypassed),
7597 #[cfg(all(unix, not(target_os = "macos")))]
7598 "LV2" => track.lock().set_lv2_plugin_bypassed(instance_id, bypassed),
7599 _ => Err(format!("Unknown plugin format for bypass: {format}")),
7600 };
7601 if let Err(e) = result {
7602 self.notify_clients(Err(e)).await;
7603 return;
7604 }
7605 self.notify_clients(Ok(a.clone())).await;
7606 }
7607 Err(e) => {
7608 self.notify_clients(Err(e)).await;
7609 }
7610 },
7611 Action::TrackGetVst3Parameters {
7612 ref track_name,
7613 instance_id,
7614 } => match self.track_handle_or_err(track_name) {
7615 Ok(track) => match track.lock().get_vst3_parameters(instance_id) {
7616 Ok(parameters) => {
7617 self.notify_clients(Ok(Action::TrackVst3Parameters {
7618 track_name: track_name.clone(),
7619 instance_id,
7620 parameters,
7621 }))
7622 .await;
7623 }
7624 Err(e) => {
7625 self.notify_clients(Err(e)).await;
7626 }
7627 },
7628 Err(e) => {
7629 self.notify_clients(Err(e)).await;
7630 }
7631 },
7632 Action::TrackVst3Parameters { .. } => {}
7633 Action::TrackVst3SnapshotState {
7634 ref track_name,
7635 instance_id,
7636 } => match self.track_handle_or_err(track_name) {
7637 Ok(track) => match track.lock().vst3_snapshot_state(instance_id) {
7638 Ok(state) => {
7639 self.notify_clients(Ok(Action::TrackVst3StateSnapshot {
7640 track_name: track_name.clone(),
7641 instance_id,
7642 state,
7643 }))
7644 .await;
7645 }
7646 Err(e) => {
7647 self.notify_clients(Err(e)).await;
7648 }
7649 },
7650 Err(e) => {
7651 self.notify_clients(Err(e)).await;
7652 }
7653 },
7654 Action::ClipVst3SnapshotState {
7655 ref track_name,
7656 clip_idx,
7657 instance_id,
7658 } => match self.track_handle_or_err(track_name) {
7659 Ok(track) => match track.lock().clip_vst3_snapshot_state(clip_idx, instance_id) {
7660 Ok(state) => {
7661 self.notify_clients(Ok(Action::ClipVst3StateSnapshot {
7662 track_name: track_name.clone(),
7663 clip_idx,
7664 instance_id,
7665 state,
7666 }))
7667 .await;
7668 }
7669 Err(e) => {
7670 self.notify_clients(Err(e)).await;
7671 }
7672 },
7673 Err(e) => {
7674 self.notify_clients(Err(e)).await;
7675 }
7676 },
7677 Action::TrackVst3StateSnapshot { .. } => {}
7678 Action::ClipVst3StateSnapshot { .. } => {}
7679 Action::TrackVst3RestoreState {
7680 ref track_name,
7681 instance_id,
7682 ref state,
7683 } => match self.track_handle_or_err(track_name) {
7684 Ok(track) => {
7685 if let Err(e) = track.lock().vst3_restore_state(instance_id, state) {
7686 self.notify_clients(Err(e)).await;
7687 return;
7688 }
7689 self.notify_clients(Ok(a.clone())).await;
7690 }
7691 Err(e) => {
7692 self.notify_clients(Err(e)).await;
7693 }
7694 },
7695 Action::TrackConnectVst3Audio {
7696 ref track_name,
7697 ref from_node,
7698 from_port,
7699 ref to_node,
7700 to_port,
7701 } => {
7702 if self
7703 .reject_if_track_frozen(track_name, "VST3 routing changes")
7704 .await
7705 {
7706 return;
7707 }
7708 match self.track_handle_or_err(track_name) {
7709 Ok(track) => {
7710 if let Err(e) = track
7711 .lock()
7712 .connect_vst3_audio(from_node, from_port, to_node, to_port)
7713 {
7714 self.notify_clients(Err(e)).await;
7715 return;
7716 }
7717 self.notify_clients(Ok(a.clone())).await;
7718 }
7719 Err(e) => {
7720 self.notify_clients(Err(e)).await;
7721 }
7722 }
7723 }
7724 Action::TrackDisconnectVst3Audio {
7725 ref track_name,
7726 ref from_node,
7727 from_port,
7728 ref to_node,
7729 to_port,
7730 } => {
7731 if self
7732 .reject_if_track_frozen(track_name, "VST3 routing changes")
7733 .await
7734 {
7735 return;
7736 }
7737 match self.track_handle_or_err(track_name) {
7738 Ok(track) => {
7739 if let Err(e) = track
7740 .lock()
7741 .disconnect_vst3_audio(from_node, from_port, to_node, to_port)
7742 {
7743 self.notify_clients(Err(e)).await;
7744 return;
7745 }
7746 self.notify_clients(Ok(a.clone())).await;
7747 }
7748 Err(e) => {
7749 self.notify_clients(Err(e)).await;
7750 }
7751 }
7752 }
7753 Action::ClipMove {
7754 ref kind,
7755 ref from,
7756 ref to,
7757 copy,
7758 } => {
7759 if let Some(from_track_handle) = self.state.lock().tracks.get(&from.track_name)
7760 && let Some(to_track_handle) = self.state.lock().tracks.get(&to.track_name)
7761 {
7762 let from_track = from_track_handle.lock();
7763 let to_track = to_track_handle.lock();
7764 match kind {
7765 Kind::Audio => {
7766 if from.clip_index >= from_track.audio.clips.len() {
7767 self.notify_clients(Err(format!(
7768 "Clip index {} is too high, as track {} has only {} clips!",
7769 from.clip_index,
7770 from_track.name.clone(),
7771 from_track.audio.clips.len(),
7772 )))
7773 .await;
7774 return;
7775 }
7776 if from_track.audio.ins.len() != to_track.audio.ins.len() {
7777 self.notify_clients(Err(format!(
7778 "Cannot move/copy audio clip from '{}' ({} inputs) to '{}' ({} inputs)",
7779 from_track.name,
7780 from_track.audio.ins.len(),
7781 to_track.name,
7782 to_track.audio.ins.len()
7783 )))
7784 .await;
7785 return;
7786 }
7787 let clip_copy = from_track.audio.clips[from.clip_index].clone();
7788 if !copy {
7789 from_track.audio.clips.remove(from.clip_index);
7790 }
7791 let mut clip_copy = clip_copy;
7792 clip_copy.start = to.sample_offset;
7793 let max_lane = to_track.audio.ins.len().saturating_sub(1);
7794 clip_copy.input_channel = to.input_channel.min(max_lane);
7795 to_track.audio.clips.push(clip_copy);
7796 }
7797 Kind::MIDI => {
7798 if from.clip_index >= from_track.midi.clips.len() {
7799 self.notify_clients(Err(format!(
7800 "Clip index {} is too high, as track {} has only {} clips!",
7801 from.clip_index,
7802 from_track.name.clone(),
7803 from_track.midi.clips.len(),
7804 )))
7805 .await;
7806 return;
7807 }
7808 let clip_copy = from_track.midi.clips[from.clip_index].clone();
7809 if !copy {
7810 from_track.midi.clips.remove(from.clip_index);
7811 }
7812 let mut clip_copy = clip_copy;
7813 clip_copy.start = to.sample_offset;
7814 let max_lane = to_track.midi.ins.len().saturating_sub(1);
7815 clip_copy.input_channel = to.input_channel.min(max_lane);
7816 to_track.midi.clips.push(clip_copy);
7817 }
7818 }
7819 }
7820 }
7821 Action::AddClip {
7822 ref name,
7823 ref track_name,
7824 start,
7825 length,
7826 offset,
7827 input_channel,
7828 muted,
7829 ref peaks_file,
7830 kind,
7831 fade_enabled,
7832 fade_in_samples,
7833 fade_out_samples,
7834 ref source_name,
7835 source_offset,
7836 source_length,
7837 ref preview_name,
7838 ref pitch_correction_points,
7839 pitch_correction_frame_likeness,
7840 pitch_correction_inertia_ms,
7841 pitch_correction_formant_compensation,
7842 ref plugin_graph_json,
7843 } => {
7844 self.add_clip_to_track(ClipAddRequest {
7845 name,
7846 track_name,
7847 start,
7848 length,
7849 offset,
7850 input_channel,
7851 muted,
7852 peaks_file: peaks_file.clone(),
7853 kind,
7854 fade_enabled,
7855 fade_in_samples,
7856 fade_out_samples,
7857 source_name: source_name.clone(),
7858 source_offset,
7859 source_length,
7860 preview_name: preview_name.clone(),
7861 pitch_correction_points: pitch_correction_points.clone(),
7862 pitch_correction_frame_likeness,
7863 pitch_correction_inertia_ms,
7864 pitch_correction_formant_compensation,
7865 plugin_graph_json: plugin_graph_json.clone(),
7866 });
7867 if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
7868 let track_name = track_name.clone();
7869 tokio::task::spawn_blocking(move || {
7870 track.lock().preload_clips();
7871 tracing::debug!("Preloaded clips for track '{}' after AddClip", track_name);
7872 });
7873 }
7874 }
7875 Action::AddGroupedClip {
7876 ref track_name,
7877 kind,
7878 ref audio_clip,
7879 ref midi_clip,
7880 } => {
7881 self.add_grouped_clip_to_track(
7882 track_name,
7883 kind,
7884 audio_clip.clone(),
7885 midi_clip.clone(),
7886 );
7887 if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
7888 let track_name = track_name.clone();
7889 tokio::task::spawn_blocking(move || {
7890 track.lock().preload_clips();
7891 tracing::debug!(
7892 "Preloaded clips for track '{}' after AddGroupedClip",
7893 track_name
7894 );
7895 });
7896 }
7897 }
7898 Action::RemoveClip {
7899 ref track_name,
7900 kind,
7901 ref clip_indices,
7902 } => {
7903 self.remove_clips_from_track(track_name, kind, clip_indices);
7904 }
7905 Action::RenameClip {
7906 ref track_name,
7907 kind,
7908 clip_index,
7909 ref new_name,
7910 } => {
7911 self.rename_clip_references(track_name, kind, clip_index, new_name);
7912 }
7913 Action::SetClipSourceName {
7914 ref track_name,
7915 kind,
7916 clip_index,
7917 ref name,
7918 } => {
7919 self.set_clip_source_name(track_name, clip_index, kind, name.clone());
7920 }
7921 Action::SetClipFade {
7922 ref track_name,
7923 clip_index,
7924 kind,
7925 fade_enabled,
7926 fade_in_samples,
7927 fade_out_samples,
7928 } => {
7929 self.set_clip_fade(
7930 track_name,
7931 clip_index,
7932 kind,
7933 fade_enabled,
7934 fade_in_samples,
7935 fade_out_samples,
7936 );
7937 }
7938 Action::SetClipBounds {
7939 ref track_name,
7940 clip_index,
7941 kind,
7942 start,
7943 length,
7944 offset,
7945 } => {
7946 self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
7947 }
7948 Action::SyncClipBounds {
7949 ref track_name,
7950 clip_index,
7951 kind,
7952 start,
7953 length,
7954 offset,
7955 } => {
7956 self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
7957 }
7958 Action::SetClipMuted {
7959 ref track_name,
7960 clip_index,
7961 kind,
7962 muted,
7963 } => {
7964 self.set_clip_muted(track_name, clip_index, kind, muted);
7965 }
7966 Action::SetClipPluginGraphJson {
7967 ref track_name,
7968 clip_index,
7969 ref plugin_graph_json,
7970 } => {
7971 self.set_clip_plugin_graph_json(track_name, clip_index, plugin_graph_json.clone());
7972 }
7973 Action::SetClipPitchCorrection {
7974 ref track_name,
7975 clip_index,
7976 ref preview_name,
7977 ref source_name,
7978 source_offset,
7979 source_length,
7980 ref pitch_correction_points,
7981 pitch_correction_frame_likeness,
7982 pitch_correction_inertia_ms,
7983 pitch_correction_formant_compensation,
7984 } => {
7985 self.set_clip_pitch_correction(
7986 track_name,
7987 clip_index,
7988 preview_name.clone(),
7989 source_name.clone(),
7990 source_offset,
7991 source_length,
7992 pitch_correction_points.clone(),
7993 pitch_correction_frame_likeness,
7994 pitch_correction_inertia_ms,
7995 pitch_correction_formant_compensation,
7996 );
7997 }
7998 Action::Connect {
7999 ref from_track,
8000 from_port,
8001 ref to_track,
8002 to_port,
8003 kind,
8004 } => {
8005 match kind {
8006 Kind::Audio => {
8007 let from_audio_io = if from_track == "hw:in" {
8008 self.hw_input_audio_port(from_port)
8009 } else {
8010 self.state
8011 .lock()
8012 .tracks
8013 .get(from_track)
8014 .and_then(|t| t.lock().audio.outs.get(from_port).cloned())
8015 };
8016 let to_audio_io = if to_track == "hw:out" {
8017 self.hw_output_audio_port(to_port)
8018 } else {
8019 self.state
8020 .lock()
8021 .tracks
8022 .get(to_track)
8023 .and_then(|t| t.lock().audio.ins.get(to_port).cloned())
8024 };
8025 match (from_audio_io, to_audio_io) {
8026 (Some(source), Some(target)) => {
8027 if from_track != "hw:in"
8028 && to_track != "hw:out"
8029 && self.check_if_leads_to_kind(
8030 Kind::Audio,
8031 to_track,
8032 from_track,
8033 )
8034 {
8035 self.notify_clients(Err(
8036 "Circular routing is not allowed!".into()
8037 ))
8038 .await;
8039 return;
8040 }
8041 crate::audio::io::AudioIO::connect(&source, &target);
8042 }
8043 (None, _) => {
8044 self.notify_clients(Err(format!(
8045 "Source track '{}' not found",
8046 from_track
8047 )))
8048 .await;
8049 return;
8050 }
8051 (_, None) => {
8052 self.notify_clients(Err(format!(
8053 "Destination track '{}' not found",
8054 to_track
8055 )))
8056 .await;
8057 return;
8058 }
8059 }
8060 }
8061 Kind::MIDI => {
8062 let from_hw_in_device = Self::midi_hw_in_device(from_track);
8063 let to_hw_out_device = Self::midi_hw_out_device(to_track);
8064 let from_is_invalid_hw = Self::midi_hw_out_device(from_track).is_some();
8065 let to_is_invalid_hw = Self::midi_hw_in_device(to_track).is_some();
8066
8067 if from_is_invalid_hw || to_is_invalid_hw {
8068 self.notify_clients(Err(
8069 "Invalid MIDI hardware connection direction".to_string()
8070 ))
8071 .await;
8072 return;
8073 }
8074
8075 if from_hw_in_device.is_none()
8076 && to_hw_out_device.is_none()
8077 && self.check_if_leads_to_kind(Kind::MIDI, to_track, from_track)
8078 {
8079 self.notify_clients(Err("Circular routing is not allowed!".into()))
8080 .await;
8081 return;
8082 }
8083
8084 let state = self.state.lock();
8085 let from_track_handle = state.tracks.get(from_track);
8086 let to_track_handle = state.tracks.get(to_track);
8087
8088 if let (Some(from_device), Some(to_device)) =
8089 (from_hw_in_device, to_hw_out_device)
8090 {
8091 let route = MidiHwThruRoute {
8092 from_device: from_device.to_string(),
8093 to_device: to_device.to_string(),
8094 };
8095 if !self.midi_hw_thru_routes.iter().any(|r| r == &route) {
8096 self.midi_hw_thru_routes.push(route);
8097 }
8098 } else if let Some(device) = from_hw_in_device {
8099 if let Some(t_t) = to_track_handle {
8100 if t_t.lock().midi.ins.get(to_port).is_none() {
8101 self.notify_clients(Err(format!(
8102 "MIDI input port {} not found on track '{}'",
8103 to_port, to_track
8104 )))
8105 .await;
8106 return;
8107 }
8108 let route = MidiHwInRoute {
8109 device: device.to_string(),
8110 to_track: to_track.to_string(),
8111 to_port,
8112 };
8113 if !self.midi_hw_in_routes.iter().any(|r| r == &route) {
8114 self.midi_hw_in_routes.push(route);
8115 }
8116 } else {
8117 self.notify_clients(Err(format!(
8118 "MIDI destination track not found: {}",
8119 to_track
8120 )))
8121 .await;
8122 return;
8123 }
8124 } else if let Some(device) = to_hw_out_device {
8125 if let Some(f_t) = from_track_handle {
8126 if f_t.lock().midi.outs.get(from_port).is_none() {
8127 self.notify_clients(Err(format!(
8128 "MIDI output port {} not found on track '{}'",
8129 from_port, from_track
8130 )))
8131 .await;
8132 return;
8133 }
8134 let route = MidiHwOutRoute {
8135 from_track: from_track.to_string(),
8136 from_port,
8137 device: device.to_string(),
8138 };
8139 if !self.midi_hw_out_routes.iter().any(|r| r == &route) {
8140 self.midi_hw_out_routes.push(route);
8141 }
8142 } else {
8143 self.notify_clients(Err(format!(
8144 "MIDI source track not found: {}",
8145 from_track
8146 )))
8147 .await;
8148 return;
8149 }
8150 } else {
8151 match (from_track_handle, to_track_handle) {
8152 (Some(f_t), Some(t_t)) => {
8153 let to_in_res = t_t.lock().midi.ins.get(to_port).cloned();
8154 if let Some(to_in) = to_in_res {
8155 let from_track = f_t.lock();
8156 if let Err(e) =
8157 from_track.midi.connect_out(from_port, to_in)
8158 {
8159 self.notify_clients(Err(e)).await;
8160 return;
8161 }
8162 from_track.invalidate_midi_route_cache();
8163 } else {
8164 self.notify_clients(Err(format!(
8165 "MIDI input port {} not found on track '{}'",
8166 to_port, to_track
8167 )))
8168 .await;
8169 return;
8170 }
8171 }
8172 _ => {
8173 self.notify_clients(Err(format!(
8174 "MIDI tracks not found: {} or {}",
8175 from_track, to_track
8176 )))
8177 .await;
8178 return;
8179 }
8180 }
8181 }
8182 }
8183 };
8184 }
8185 Action::Disconnect {
8186 ref from_track,
8187 from_port,
8188 ref to_track,
8189 to_port,
8190 kind,
8191 } => {
8192 if kind == Kind::Audio {
8193 if let Err(e) = self.disconnect_audio_route_and_notify(a.clone()).await {
8194 self.notify_clients(Err(e)).await;
8195 }
8196 } else if kind == Kind::MIDI {
8197 let from_hw_in_device = Self::midi_hw_in_device(from_track);
8198 let to_hw_out_device = Self::midi_hw_out_device(to_track);
8199
8200 if let (Some(from_device), Some(to_device)) =
8201 (from_hw_in_device, to_hw_out_device)
8202 {
8203 let before = self.midi_hw_thru_routes.len();
8204 self.midi_hw_thru_routes.retain(|r| {
8205 !(r.from_device == from_device && r.to_device == to_device)
8206 });
8207 if self.midi_hw_thru_routes.len() < before {
8208 self.notify_clients(Ok(a.clone())).await;
8209 } else {
8210 self.notify_clients(Err(format!(
8211 "Disconnect failed: MIDI route not found ({} -> {})",
8212 from_track, to_track
8213 )))
8214 .await;
8215 }
8216 return;
8217 }
8218
8219 if let Some(device) = from_hw_in_device {
8220 let before = self.midi_hw_in_routes.len();
8221 self.midi_hw_in_routes.retain(|r| {
8222 !(r.device == device && r.to_track == *to_track && r.to_port == to_port)
8223 });
8224 if self.midi_hw_in_routes.len() < before {
8225 self.notify_clients(Ok(a.clone())).await;
8226 } else {
8227 self.notify_clients(Err(format!(
8228 "Disconnect failed: MIDI route not found ({} -> {})",
8229 from_track, to_track
8230 )))
8231 .await;
8232 }
8233 return;
8234 }
8235
8236 if let Some(device) = to_hw_out_device {
8237 let before = self.midi_hw_out_routes.len();
8238 self.midi_hw_out_routes.retain(|r| {
8239 !(r.from_track == *from_track
8240 && r.from_port == from_port
8241 && r.device == device)
8242 });
8243 if self.midi_hw_out_routes.len() < before {
8244 self.notify_clients(Ok(a.clone())).await;
8245 } else {
8246 self.notify_clients(Err(format!(
8247 "Disconnect failed: MIDI route not found ({} -> {})",
8248 from_track, to_track
8249 )))
8250 .await;
8251 }
8252 return;
8253 }
8254
8255 let state = self.state.lock();
8256 if let (Some(f_t), Some(t_t)) =
8257 (state.tracks.get(from_track), state.tracks.get(to_track))
8258 && let Some(to_in) = t_t.lock().midi.ins.get(to_port).cloned()
8259 {
8260 let from_track = f_t.lock();
8261 if let Err(e) = from_track.midi.disconnect_out(from_port, &to_in) {
8262 self.notify_clients(Err(e)).await;
8263 } else {
8264 from_track.invalidate_midi_route_cache();
8265 self.notify_clients(Ok(a.clone())).await;
8266 }
8267 } else {
8268 self.notify_clients(Err(format!(
8269 "Disconnect failed: MIDI ports not found ({} -> {})",
8270 from_track, to_track
8271 )))
8272 .await;
8273 }
8274 }
8275 }
8276
8277 Action::OpenAudioDevice {
8278 ref device,
8279 ref input_device,
8280 sample_rate_hz,
8281 bits,
8282 exclusive,
8283 period_frames,
8284 nperiods,
8285 sync_mode,
8286 ..
8287 } => {
8288 #[cfg(unix)]
8289 {
8290 let request = AudioOpenRequest {
8291 device,
8292 input_device: input_device.as_deref(),
8293 sample_rate_hz,
8294 bits,
8295 exclusive,
8296 period_frames,
8297 nperiods,
8298 sync_mode,
8299 };
8300 if self.maybe_open_jack_runtime(request).await.is_some() {
8301 return;
8302 }
8303 }
8304 let hw_opts = Self::build_hw_options(exclusive, period_frames, nperiods, sync_mode);
8305 let open_result = self
8306 .open_non_jack_audio_device(
8307 device,
8308 input_device.as_deref(),
8309 sample_rate_hz,
8310 bits,
8311 hw_opts,
8312 )
8313 .await;
8314 match open_result {
8315 Ok(()) => {}
8316 Err(e) => {
8317 error!("Failed to open audio device: {e}");
8318 self.notify_clients(Err(e)).await;
8319 return;
8320 }
8321 }
8322 self.finalize_open_audio_device().await;
8323 if let Some(hw) = &self.hw_driver {
8324 let effective_action = {
8325 let hw = hw.lock();
8326 Action::OpenAudioDevice {
8327 device: device.clone(),
8328 input_device: input_device.clone(),
8329 sample_rate_hz: hw.sample_rate(),
8330 bits: hw.sample_bits(),
8331 exclusive,
8332 period_frames,
8333 nperiods,
8334 sync_mode,
8335 actual_period_frames: hw.cycle_samples(),
8336 input_channels: hw.input_channels(),
8337 output_channels: hw.output_channels(),
8338 bytes_per_frame: hw.frame_size_bytes(),
8339 }
8340 };
8341 action_to_process = effective_action;
8342 }
8343 }
8344 Action::JackAddAudioInputPort => {
8345 #[cfg(unix)]
8346 {
8347 if let Some(jack) = self.jack_runtime.clone() {
8348 let (input_channels, output_channels, rate) = {
8349 let jack = jack.lock();
8350 if let Err(e) = jack.add_audio_input_port() {
8351 self.notify_clients(Err(e)).await;
8352 return;
8353 }
8354 (
8355 jack.input_channels(),
8356 jack.output_channels(),
8357 jack.sample_rate,
8358 )
8359 };
8360 self.publish_hw_infos(input_channels, output_channels, rate)
8361 .await;
8362 self.notify_clients(Ok(a.clone())).await;
8363 } else {
8364 self.notify_clients(Err(
8365 "JACK runtime is not active; open the JACK backend first".to_string(),
8366 ))
8367 .await;
8368 }
8369 }
8370 #[cfg(not(unix))]
8371 {
8372 self.notify_clients(Err(
8373 "JACK backend is not available on this platform build".to_string(),
8374 ))
8375 .await;
8376 }
8377 }
8378 Action::JackRemoveAudioInputPort(_removed_port) => {
8379 #[cfg(unix)]
8380 {
8381 let removed_port = _removed_port;
8382 if let Some(jack) = self.jack_runtime.clone() {
8383 let (removed_port, removed_io) = {
8384 let jack = jack.lock();
8385 let removed_port = Some(removed_port);
8386 let removed_io =
8387 removed_port.and_then(|port| jack.input_audio_port(port));
8388 match (removed_port, removed_io) {
8389 (Some(port), Some(io)) => (port, io),
8390 _ => {
8391 self.notify_clients(Err(
8392 "JACK audio input port index is out of range".to_string(),
8393 ))
8394 .await;
8395 return;
8396 }
8397 }
8398 };
8399 let reindex_notifications =
8400 self.reindex_notifications_for_removed_hw_input(removed_port);
8401 for disconnect in
8402 self.disconnect_actions_for_removed_hw_input(removed_port, &removed_io)
8403 {
8404 if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await
8405 {
8406 self.notify_clients(Err(e)).await;
8407 return;
8408 }
8409 }
8410 let (input_channels, output_channels, rate) = {
8411 let jack = jack.lock();
8412 if let Err(e) = jack.remove_audio_input_port(removed_port) {
8413 self.notify_clients(Err(e)).await;
8414 return;
8415 }
8416 (
8417 jack.input_channels(),
8418 jack.output_channels(),
8419 jack.sample_rate,
8420 )
8421 };
8422 for action in reindex_notifications {
8423 self.notify_clients(Ok(action)).await;
8424 }
8425 self.publish_hw_infos(input_channels, output_channels, rate)
8426 .await;
8427 self.notify_clients(Ok(a.clone())).await;
8428 } else {
8429 self.notify_clients(Err(
8430 "JACK runtime is not active; open the JACK backend first".to_string(),
8431 ))
8432 .await;
8433 }
8434 }
8435 #[cfg(not(unix))]
8436 {
8437 self.notify_clients(Err(
8438 "JACK backend is not available on this platform build".to_string(),
8439 ))
8440 .await;
8441 }
8442 }
8443 Action::JackAddAudioOutputPort => {
8444 #[cfg(unix)]
8445 {
8446 if let Some(jack) = self.jack_runtime.clone() {
8447 let (input_channels, output_channels, rate) = {
8448 let jack = jack.lock();
8449 if let Err(e) = jack.add_audio_output_port() {
8450 self.notify_clients(Err(e)).await;
8451 return;
8452 }
8453 (
8454 jack.input_channels(),
8455 jack.output_channels(),
8456 jack.sample_rate,
8457 )
8458 };
8459 self.publish_hw_infos(input_channels, output_channels, rate)
8460 .await;
8461 self.notify_clients(Ok(a.clone())).await;
8462 } else {
8463 self.notify_clients(Err(
8464 "JACK runtime is not active; open the JACK backend first".to_string(),
8465 ))
8466 .await;
8467 }
8468 }
8469 #[cfg(not(unix))]
8470 {
8471 self.notify_clients(Err(
8472 "JACK backend is not available on this platform build".to_string(),
8473 ))
8474 .await;
8475 }
8476 }
8477 Action::JackRemoveAudioOutputPort(_removed_port) => {
8478 #[cfg(unix)]
8479 {
8480 let removed_port = _removed_port;
8481 if let Some(jack) = self.jack_runtime.clone() {
8482 let (removed_port, removed_io) = {
8483 let jack = jack.lock();
8484 let removed_port = Some(removed_port);
8485 let removed_io =
8486 removed_port.and_then(|port| jack.output_audio_port(port));
8487 match (removed_port, removed_io) {
8488 (Some(port), Some(io)) => (port, io),
8489 _ => {
8490 self.notify_clients(Err(
8491 "JACK audio output port index is out of range".to_string(),
8492 ))
8493 .await;
8494 return;
8495 }
8496 }
8497 };
8498 let reindex_notifications =
8499 self.reindex_notifications_for_removed_hw_output(removed_port);
8500 for disconnect in
8501 self.disconnect_actions_for_removed_hw_output(removed_port, &removed_io)
8502 {
8503 if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await
8504 {
8505 self.notify_clients(Err(e)).await;
8506 return;
8507 }
8508 }
8509 let (input_channels, output_channels, rate) = {
8510 let jack = jack.lock();
8511 if let Err(e) = jack.remove_audio_output_port(removed_port) {
8512 self.notify_clients(Err(e)).await;
8513 return;
8514 }
8515 (
8516 jack.input_channels(),
8517 jack.output_channels(),
8518 jack.sample_rate,
8519 )
8520 };
8521 for action in reindex_notifications {
8522 self.notify_clients(Ok(action)).await;
8523 }
8524 self.publish_hw_infos(input_channels, output_channels, rate)
8525 .await;
8526 self.notify_clients(Ok(a.clone())).await;
8527 } else {
8528 self.notify_clients(Err(
8529 "JACK runtime is not active; open the JACK backend first".to_string(),
8530 ))
8531 .await;
8532 }
8533 }
8534 #[cfg(not(unix))]
8535 {
8536 self.notify_clients(Err(
8537 "JACK backend is not available on this platform build".to_string(),
8538 ))
8539 .await;
8540 }
8541 }
8542 Action::OpenMidiInputDevice(ref device) => {
8543 let midi_hub = self.midi_hub.lock();
8544 if let Err(e) = midi_hub.open_input(device) {
8545 self.notify_clients(Err(e)).await;
8546 return;
8547 }
8548 }
8549 Action::OpenMidiOutputDevice(ref device) => {
8550 let midi_hub = self.midi_hub.lock();
8551 if let Err(e) = midi_hub.open_output(device) {
8552 self.notify_clients(Err(e)).await;
8553 return;
8554 }
8555 }
8556 Action::RequestSessionDiagnostics => {
8557 let (
8558 track_count,
8559 frozen_track_count,
8560 audio_clip_count,
8561 midi_clip_count,
8562 lv2_instance_count,
8563 vst3_instance_count,
8564 clap_instance_count,
8565 ) = {
8566 let tracks = &self.state.lock().tracks;
8567 let mut track_count = 0usize;
8568 let mut frozen_track_count = 0usize;
8569 let mut audio_clip_count = 0usize;
8570 let mut midi_clip_count = 0usize;
8571 #[cfg(all(unix, not(target_os = "macos")))]
8572 let mut lv2_instance_count = 0usize;
8573 #[cfg(not(all(unix, not(target_os = "macos"))))]
8574 let lv2_instance_count = 0usize;
8575 let mut vst3_instance_count = 0usize;
8576 let mut clap_instance_count = 0usize;
8577 for track in tracks.values() {
8578 let t = track.lock();
8579 track_count += 1;
8580 if t.frozen {
8581 frozen_track_count += 1;
8582 }
8583 audio_clip_count += t.audio.clips.len();
8584 midi_clip_count += t.midi.clips.len();
8585 #[cfg(all(unix, not(target_os = "macos")))]
8586 {
8587 lv2_instance_count += t.lv2_plugins.len();
8588 }
8589 vst3_instance_count += t.vst3_plugins.len();
8590 clap_instance_count += t.clap_plugins.len();
8591 }
8592 (
8593 track_count,
8594 frozen_track_count,
8595 audio_clip_count,
8596 midi_clip_count,
8597 lv2_instance_count,
8598 vst3_instance_count,
8599 clap_instance_count,
8600 )
8601 };
8602 #[cfg(not(all(unix, not(target_os = "macos"))))]
8603 let _lv2_instance_count = lv2_instance_count;
8604 let pending_hw_midi_events = self.pending_hw_midi_events.len()
8605 + self
8606 .pending_hw_midi_events_by_device
8607 .values()
8608 .map(std::vec::Vec::len)
8609 .sum::<usize>();
8610 let sample_rate_hz = if let Some(hw) = &self.hw_driver {
8611 hw.lock().sample_rate() as usize
8612 } else {
8613 #[cfg(unix)]
8614 {
8615 self.jack_runtime
8616 .as_ref()
8617 .map(|j| j.lock().sample_rate)
8618 .unwrap_or(0)
8619 }
8620 #[cfg(not(unix))]
8621 0
8622 };
8623 let cycle_samples = self.current_cycle_samples();
8624 self.notify_clients(Ok(Action::SessionDiagnosticsReport {
8625 track_count,
8626 frozen_track_count,
8627 audio_clip_count,
8628 midi_clip_count,
8629 #[cfg(all(unix, not(target_os = "macos")))]
8630 lv2_instance_count,
8631 vst3_instance_count,
8632 clap_instance_count,
8633 pending_requests: self.pending_requests.len(),
8634 workers_total: self.workers.len(),
8635 workers_ready: self.ready_workers.len(),
8636 pending_hw_midi_events,
8637 playing: self.playing,
8638 transport_sample: self.transport_sample,
8639 tempo_bpm: self.tempo_bpm,
8640 sample_rate_hz,
8641 cycle_samples,
8642 }))
8643 .await;
8644 }
8645 Action::RequestMidiLearnMappingsReport => {
8646 let mut lines = Vec::<String>::new();
8647 let fmt_binding = |b: &crate::message::MidiLearnBinding| {
8648 let device = b.device.as_deref().unwrap_or("*");
8649 format!("{device} CH{} CC{}", b.channel + 1, b.cc)
8650 };
8651 if let Some(b) = self.global_midi_learn_play_pause.as_ref() {
8652 lines.push(format!("Global PlayPause: {}", fmt_binding(b)));
8653 }
8654 if let Some(b) = self.global_midi_learn_stop.as_ref() {
8655 lines.push(format!("Global Stop: {}", fmt_binding(b)));
8656 }
8657 if let Some(b) = self.global_midi_learn_record_toggle.as_ref() {
8658 lines.push(format!("Global RecordToggle: {}", fmt_binding(b)));
8659 }
8660 for (track_name, track) in self.state.lock().tracks.iter() {
8661 let t = track.lock();
8662 if let Some(b) = t.midi_learn_volume.as_ref() {
8663 lines.push(format!("{} Volume: {}", track_name, fmt_binding(b)));
8664 }
8665 if let Some(b) = t.midi_learn_balance.as_ref() {
8666 lines.push(format!("{} Balance: {}", track_name, fmt_binding(b)));
8667 }
8668 if let Some(b) = t.midi_learn_mute.as_ref() {
8669 lines.push(format!("{} Mute: {}", track_name, fmt_binding(b)));
8670 }
8671 if let Some(b) = t.midi_learn_solo.as_ref() {
8672 lines.push(format!("{} Solo: {}", track_name, fmt_binding(b)));
8673 }
8674 if let Some(b) = t.midi_learn_arm.as_ref() {
8675 lines.push(format!("{} Arm: {}", track_name, fmt_binding(b)));
8676 }
8677 if let Some(b) = t.midi_learn_input_monitor.as_ref() {
8678 lines.push(format!("{} InputMonitor: {}", track_name, fmt_binding(b)));
8679 }
8680 if let Some(b) = t.midi_learn_disk_monitor.as_ref() {
8681 lines.push(format!("{} DiskMonitor: {}", track_name, fmt_binding(b)));
8682 }
8683 }
8684 if lines.is_empty() {
8685 lines.push("No MIDI learn mappings configured".to_string());
8686 }
8687 self.notify_clients(Ok(Action::MidiLearnMappingsReport { lines }))
8688 .await;
8689 }
8690 Action::ClearAllMidiLearnBindings => {
8691 self.pending_midi_learn = None;
8692 self.pending_global_midi_learn = None;
8693 self.global_midi_learn_play_pause = None;
8694 self.global_midi_learn_stop = None;
8695 self.global_midi_learn_record_toggle = None;
8696 self.midi_cc_gate.clear();
8697 for track in self.state.lock().tracks.values() {
8698 let t = track.lock();
8699 t.midi_learn_volume = None;
8700 t.midi_learn_balance = None;
8701 t.midi_learn_mute = None;
8702 t.midi_learn_solo = None;
8703 t.midi_learn_arm = None;
8704 t.midi_learn_input_monitor = None;
8705 t.midi_learn_disk_monitor = None;
8706 }
8707 }
8708 #[cfg(all(unix, not(target_os = "macos")))]
8709 Action::TrackLv2PluginControls { .. } => {}
8710 #[cfg(all(unix, not(target_os = "macos")))]
8711 Action::ClipLv2PluginControls { .. } => {}
8712 #[cfg(all(unix, not(target_os = "macos")))]
8713 Action::TrackLv2Midnam { .. } => {}
8714 Action::TrackClapNoteNames { .. } => {}
8715 Action::SessionDiagnosticsReport { .. } => {}
8716 Action::MidiLearnMappingsReport { .. } => {}
8717 Action::HWInfo { .. } => {}
8718 Action::HistoryState { .. } => {}
8719 Action::Undo => {}
8720 Action::Redo => {}
8721 Action::ApplyGroupedActions(_) => {}
8722 _ => {}
8723 }
8724
8725 if let Some(inverse) = inverse_actions {
8726 if let Some(group) = self.history_group.as_mut() {
8727 group.forward_actions.push(action_to_process.clone());
8728 group.inverse_actions.splice(0..0, inverse);
8729 } else {
8730 self.history.record(UndoEntry {
8731 forward_actions: vec![action_to_process.clone()],
8732 inverse_actions: inverse,
8733 });
8734 }
8735 }
8736
8737 self.notify_clients(Ok(action_to_process)).await;
8738 }
8739
8740 pub async fn work(&mut self) {
8741 while let Some(message) = self.rx.recv().await {
8742 match message {
8743 Message::Ready(id) => self.push_ready_worker(id),
8744 Message::Finished {
8745 worker_id,
8746 task,
8747 output_linear,
8748 process_epoch,
8749 parameter_updates,
8750 } => {
8751 tracing::debug!(
8752 "engine received Finished from worker {} for task {:?} (epoch {} vs {})",
8753 worker_id,
8754 task,
8755 process_epoch,
8756 self.track_process_epoch
8757 );
8758 self.push_ready_worker(worker_id);
8759 let task_key = Self::task_key(&task);
8760 self.task_processing_started_at.remove(&task_key);
8761 if process_epoch != self.track_process_epoch {
8762 if let Some(track) = self
8763 .state
8764 .lock()
8765 .tracks
8766 .get(&Self::task_track_name(&task))
8767 .cloned()
8768 {
8769 let t = track.lock();
8770 t.audio.finished = false;
8771 t.audio.processing = false;
8772 }
8773 continue;
8774 }
8775 self.cycle_tasks_running
8776 .retain(|t| Self::task_key(t) != task_key);
8777 self.cycle_tasks_finished.push(task.clone());
8778 let track_name = Self::task_track_name(&task);
8779 self.track_meter_linear_by_track
8780 .insert(track_name.clone(), output_linear);
8781 for action in parameter_updates {
8782 self.notify_clients(Ok(action)).await;
8783 }
8784 self.force_stalled_task_completions();
8785 let all_finished = self.send_tasks().await;
8786 tracing::debug!(
8787 "engine after Finished for {}: all_finished={}",
8788 track_name,
8789 all_finished
8790 );
8791 if all_finished {
8792 self.on_all_tracks_finished().await;
8793 }
8794 }
8795 Message::Channel(s) => {
8796 self.clients.push(s);
8797 }
8798
8799 Message::Request(a) => {
8800 match a {
8801 Action::TrackOfflineBounceCancel { track_name } => {
8802 if let Some(job) = self.offline_bounce_jobs.get(&track_name) {
8803 job.cancel.store(true, Ordering::Relaxed);
8804 }
8805 }
8806 Action::TrackOfflineBounceCancelAll => {
8807 for job in self.offline_bounce_jobs.values() {
8808 job.cancel.store(true, Ordering::Relaxed);
8809 }
8810 }
8811 _ if !self.offline_bounce_jobs.is_empty() => {
8812 self.pending_requests.push_back(a);
8813 }
8814 Action::OpenAudioDevice { .. }
8815 | Action::OpenMidiInputDevice(_)
8816 | Action::OpenMidiOutputDevice(_)
8817 | Action::RequestMeterSnapshot
8818 | Action::Quit
8819 | Action::Log { .. }
8820 | Action::Play
8821 | Action::Pause
8822 | Action::Stop
8823 | Action::TransportPosition(_)
8824 | Action::JumpToEnd
8825 | Action::SetLoopEnabled(_)
8826 | Action::SetLoopRange(_)
8827 | Action::SetPunchEnabled(_)
8828 | Action::SetPunchRange(_)
8829 | Action::SetMetronomeEnabled(_)
8830 | Action::SetTempo(_)
8831 | Action::SetTimeSignature { .. }
8832 | Action::SetOscEnabled(_)
8833 | Action::SetClipPlaybackEnabled(_)
8834 | Action::SetRecordEnabled(_)
8835 | Action::SetStepRecording(_)
8836 | Action::StepRecordMidiNote { .. }
8837 | Action::SetSessionPath(_)
8838 | Action::ClearHistory
8839 | Action::BeginSessionRestore
8840 | Action::PianoKey { .. }
8841 | Action::ModifyMidiNotes { .. }
8842 | Action::ModifyMidiControllers { .. }
8843 | Action::DeleteMidiControllers { .. }
8844 | Action::InsertMidiControllers { .. }
8845 | Action::DeleteMidiNotes { .. }
8846 | Action::InsertMidiNotes { .. }
8847 | Action::SetMidiSysExEvents { .. } => {
8848 self.handle_request(a).await;
8849 }
8850 #[cfg(all(unix, not(target_os = "macos")))]
8851 Action::ListLv2Plugins => {
8852 self.handle_request(a).await;
8853 }
8854 Action::ListVst3Plugins => {
8855 self.handle_request(a).await;
8856 }
8857 Action::ListClapPlugins => {
8858 self.handle_request(a).await;
8859 }
8860 Action::ListClapPluginsWithCapabilities => {
8861 self.handle_request(a).await;
8862 }
8863 _ => {
8864 self.pending_requests.push_back(a);
8865 if self.can_schedule_hw_cycle() {
8866 self.request_hw_cycle().await;
8867 } else {
8868 while let Some(next) = self.pending_requests.pop_front() {
8869 self.handle_request(next).await;
8870 }
8871 }
8872 }
8873 };
8874 self.publish_clap_state_dirty().await;
8875 }
8876 Message::OfflineBounceFinished { result } => {
8877 if let Ok(Action::TrackOfflineBounce { track_name, .. }) = &result {
8878 self.offline_bounce_jobs.remove(track_name);
8879 }
8880 self.notify_clients(result).await;
8881 if self.offline_bounce_jobs.is_empty() {
8882 while let Some(next) = self.pending_requests.pop_front() {
8883 self.handle_request(next).await;
8884 }
8885 }
8886 }
8887 Message::HWFinished => {
8888 if !self.awaiting_hwfinished {
8889 tracing::debug!("HWFinished ignored (not awaiting)");
8890 continue;
8891 }
8892 tracing::debug!("HWFinished handling; playing={}", self.playing);
8893 self.handling_hwfinished = true;
8894 self.awaiting_hwfinished = false;
8895 #[cfg(unix)]
8896 {
8897 if let Some(jack) = &self.jack_runtime {
8898 if !self.pending_hw_midi_out_events.is_empty() {
8899 let out_events =
8900 std::mem::take(&mut self.pending_hw_midi_out_events);
8901 jack.lock().write_events(&out_events);
8902 }
8903 let mut in_events = vec![];
8904 jack.lock().read_events_into(&mut in_events);
8905 if !in_events.is_empty() {
8906 self.pending_hw_midi_events.extend(in_events);
8907 }
8908 }
8909 }
8910 #[cfg(unix)]
8911 if self.jack_runtime.is_some() {
8912 self.sync_from_jack_transport().await;
8913 }
8914 while let Some(a) = self.pending_requests.pop_front() {
8915 self.handle_request(a).await;
8916 }
8917 self.apply_mute_solo_policy();
8918 self.append_recorded_cycle();
8919 self.flush_completed_recordings().await;
8920 let hw_in_routes = self.midi_hw_in_routes.clone();
8921 let pending_hw_in_by_device = self.pending_hw_midi_events_by_device.clone();
8922 let mut reconfigured_tracks = Vec::new();
8923 for (track_name, track) in self.state.lock().tracks.iter() {
8924 let track_lock = track.lock();
8925 if self.jack_runtime_is_some() {
8926 if !self.pending_hw_midi_events.is_empty() {
8927 track_lock.push_hw_midi_events(&self.pending_hw_midi_events);
8928 }
8929 } else {
8930 for route in hw_in_routes.iter().filter(|r| &r.to_track == track_name) {
8931 if let Some(events) = pending_hw_in_by_device.get(&route.device) {
8932 track_lock.push_hw_midi_events_to_port(route.to_port, events);
8933 }
8934 }
8935 }
8936 if track_lock.setup() {
8937 reconfigured_tracks.push(track_name.clone());
8938 }
8939 }
8940 self.publish_track_meters().await;
8941 self.publish_clap_state_dirty().await;
8942 for track_name in reconfigured_tracks {
8943 let track = self.state.lock().tracks.get(&track_name).cloned();
8944 if let Some(track) = track {
8945 let (plugins, connections, connectable_connections) = {
8946 let track_lock = track.lock();
8947 (
8948 track_lock.plugin_graph_plugins(),
8949 track_lock.plugin_graph_connections(),
8950 track_lock.connectable_connections(),
8951 )
8952 };
8953 self.notify_clients(Ok(Action::TrackPluginGraph {
8954 track_name: track_name.clone(),
8955 plugins,
8956 connections,
8957 connectable_connections,
8958 }))
8959 .await;
8960 }
8961 }
8962 self.pending_hw_midi_events.clear();
8963 self.pending_hw_midi_events_by_device.clear();
8964 if self.playing {
8965 if self.transport_panic_flush_pending {
8966 self.transport_panic_flush_pending = false;
8967 } else if self.transport_restart_pending {
8968 self.transport_restart_pending = false;
8969 } else {
8970 let next = self
8971 .transport_sample
8972 .saturating_add(self.current_cycle_samples());
8973 let normalized = self.normalize_transport_sample(next);
8974 let wrapped = normalized != next;
8975 self.transport_sample = normalized;
8976 if wrapped {
8977 if self.notified_loop_wrap_sample == Some(self.transport_sample) {
8978 self.notified_loop_wrap_sample = None;
8979 } else {
8980 self.notify_clients(Ok(Action::TransportPosition(
8981 self.transport_sample,
8982 )))
8983 .await;
8984 }
8985 }
8986 }
8987 }
8988 {
8989 let echoes = self.apply_modulators(self.transport_sample);
8990 for action in echoes {
8991 self.notify_clients(Ok(action)).await;
8992 }
8993 }
8994 self.invalidate_track_cycle_state();
8995 let all_finished = self.send_tasks().await;
8996 tracing::debug!(
8997 "HWFinished send_tasks finished={} hw_worker={}",
8998 all_finished,
8999 self.hw_worker.is_some()
9000 );
9001 if all_finished && self.hw_worker.is_some() {
9002 self.request_hw_cycle().await;
9003 }
9004 #[cfg(unix)]
9005 {
9006 if self.jack_runtime.is_some() {
9007 self.awaiting_hwfinished = true;
9008 }
9009 }
9010 self.handling_hwfinished = false;
9011 }
9012 Message::HWMidiEvents(events) => {
9013 for hw_event in events {
9014 let thru_targets: Vec<String> = self
9015 .midi_hw_thru_routes
9016 .iter()
9017 .filter(|route| route.from_device == hw_event.device)
9018 .map(|route| route.to_device.clone())
9019 .collect();
9020 for device in thru_targets {
9021 self.pending_hw_midi_out_events_by_device.push(HwMidiEvent {
9022 device,
9023 event: hw_event.event.clone(),
9024 });
9025 }
9026 if hw_event.event.data.len() >= 3 {
9027 let status = hw_event.event.data[0];
9028 if status & 0xF0 == 0xB0 {
9029 let channel = status & 0x0F;
9030 let cc = hw_event.event.data[1];
9031 let value = hw_event.event.data[2];
9032 self.handle_incoming_hw_cc(&hw_event.device, channel, cc, value)
9033 .await;
9034 }
9035 if self.step_recording_enabled && status & 0xF0 == 0x90 {
9036 let channel = status & 0x0F;
9037 let pitch = hw_event.event.data[1];
9038 let velocity = hw_event.event.data[2];
9039 if velocity > 0 {
9040 self.notify_clients(Ok(Action::StepRecordMidiNote {
9041 device: hw_event.device.clone(),
9042 channel,
9043 pitch,
9044 velocity,
9045 }))
9046 .await;
9047 }
9048 }
9049 }
9050 self.pending_hw_midi_events_by_device
9051 .entry(hw_event.device)
9052 .or_default()
9053 .push(hw_event.event);
9054 }
9055 }
9056 _ => {}
9057 }
9058 }
9059 }
9060
9061 fn collect_hw_midi_output_events(&self) -> Vec<MidiEvent> {
9062 let mut events = vec![];
9063 for track in self.state.lock().tracks.values() {
9064 events.extend(
9065 track
9066 .lock()
9067 .take_hw_midi_out_events()
9068 .into_iter()
9069 .map(|evt| evt.event),
9070 );
9071 }
9072 events.sort_by_key(|a| a.frame);
9073 events
9074 }
9075
9076 fn collect_hw_midi_output_events_by_device(&mut self) -> Vec<HwMidiEvent> {
9077 let mut events = Vec::<HwMidiEvent>::new();
9078 let routes = self.midi_hw_out_routes.clone();
9079 let mut events_by_track = HashMap::<String, Vec<crate::track::HwMidiOutEvent>>::new();
9080 {
9081 let state = self.state.lock();
9082 for route in &routes {
9083 if events_by_track.contains_key(&route.from_track) {
9084 continue;
9085 }
9086 let Some(track) = state.tracks.get(&route.from_track) else {
9087 continue;
9088 };
9089 events_by_track.insert(
9090 route.from_track.clone(),
9091 track.lock().take_hw_midi_out_events(),
9092 );
9093 }
9094 }
9095
9096 for route in routes {
9097 let Some(track_events) = events_by_track.get(&route.from_track) else {
9098 continue;
9099 };
9100 for hw_event in track_events
9101 .iter()
9102 .filter(|evt| evt.port == route.from_port)
9103 {
9104 self.update_active_hw_notes_for_track(
9105 &route.from_track,
9106 &route.device,
9107 &hw_event.event.data,
9108 );
9109 events.push(HwMidiEvent {
9110 device: route.device.clone(),
9111 event: hw_event.event.clone(),
9112 });
9113 }
9114 }
9115 events.sort_by(|a, b| {
9116 a.event
9117 .frame
9118 .cmp(&b.event.frame)
9119 .then_with(|| a.device.cmp(&b.device))
9120 });
9121 events
9122 }
9123}
9124
9125#[cfg(test)]
9126mod tests {
9127 use super::*;
9128 use crate::mutex::UnsafeMutex;
9129 use tokio::sync::mpsc::channel;
9130 use tokio::time::{Duration as TokioDuration, timeout};
9131
9132 #[test]
9133 #[cfg(unix)]
9134 fn jack_transport_sync_decision_starts_and_syncs_position_on_external_play() {
9135 let decision = Engine::jack_transport_sync_decision(false, 128, true, 256, 64);
9136
9137 assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Start));
9138 assert_eq!(decision.position_sync, Some(256));
9139 }
9140
9141 #[test]
9142 #[cfg(unix)]
9143 fn jack_transport_sync_decision_stops_and_syncs_position_on_external_stop() {
9144 let decision = Engine::jack_transport_sync_decision(true, 512, false, 96, 64);
9145
9146 assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Stop));
9147 assert_eq!(decision.position_sync, Some(96));
9148 }
9149
9150 #[test]
9151 #[cfg(unix)]
9152 fn jack_transport_sync_decision_ignores_small_rolling_drift() {
9153 let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1040, 64);
9154
9155 assert_eq!(decision.play_sync, None);
9156 assert_eq!(decision.position_sync, None);
9157 }
9158
9159 #[test]
9160 #[cfg(unix)]
9161 fn jack_transport_sync_decision_syncs_large_rolling_jump() {
9162 let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1200, 64);
9163
9164 assert_eq!(decision.play_sync, None);
9165 assert_eq!(decision.position_sync, Some(1200));
9166 }
9167
9168 #[test]
9169 #[cfg(unix)]
9170 fn jack_transport_sync_decision_syncs_locate_while_stopped() {
9171 let decision = Engine::jack_transport_sync_decision(false, 400, false, 900, 64);
9172
9173 assert_eq!(decision.play_sync, None);
9174 assert_eq!(decision.position_sync, Some(900));
9175 }
9176
9177 fn make_engine_with_client() -> (Engine, tokio::sync::mpsc::Receiver<Message>) {
9178 let (engine_tx, engine_rx) = channel(16);
9179 let mut engine = Engine::new(engine_rx, engine_tx);
9180 let (client_tx, client_rx) = channel(16);
9181 engine.clients.push(client_tx);
9182 (engine, client_rx)
9183 }
9184
9185 fn insert_track(engine: &mut Engine, track: Track) {
9186 engine.state.lock().tracks.insert(
9187 track.name.clone(),
9188 Arc::new(UnsafeMutex::new(Box::new(track))),
9189 );
9190 }
9191
9192 fn osc_packet(address: &str) -> Vec<u8> {
9193 fn push_padded_osc_string(packet: &mut Vec<u8>, value: &str) {
9194 packet.extend_from_slice(value.as_bytes());
9195 packet.push(0);
9196 while !packet.len().is_multiple_of(4) {
9197 packet.push(0);
9198 }
9199 }
9200
9201 let mut packet = Vec::new();
9202 push_padded_osc_string(&mut packet, address);
9203 push_padded_osc_string(&mut packet, ",");
9204 packet
9205 }
9206
9207 #[tokio::test]
9208 async fn set_osc_enabled_starts_and_stops_server() {
9209 let (mut engine, _client_rx) = make_engine_with_client();
9210
9211 engine
9212 .set_osc_enabled_with(true, |tx| OscServer::start_on_addr(tx, "127.0.0.1:0"))
9213 .expect("start osc server on ephemeral port");
9214 assert!(engine.osc_server.is_some());
9215
9216 engine
9217 .set_osc_enabled_with(false, OscServer::start)
9218 .expect("stop osc server");
9219 assert!(engine.osc_server.is_none());
9220 }
9221
9222 #[tokio::test]
9223 async fn osc_server_forwards_transport_packets_to_engine_channel() {
9224 let (tx, mut rx) = channel(4);
9225 let mut server =
9226 OscServer::start_on_addr(tx, "127.0.0.1:0").expect("start osc test server");
9227 let socket = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind sender socket");
9228 let packet = osc_packet("/transport/play");
9229 socket
9230 .send_to(&packet, server.listen_addr())
9231 .expect("send osc packet");
9232
9233 let message = timeout(TokioDuration::from_secs(1), rx.recv())
9234 .await
9235 .expect("packet delivery timeout")
9236 .expect("osc message");
9237 match message {
9238 Message::Request(Action::Play) => {}
9239 other => panic!("unexpected osc message: {other:?}"),
9240 }
9241
9242 server.stop();
9243 }
9244
9245 #[tokio::test]
9246 async fn track_offline_bounce_rejects_zero_length_requests() {
9247 let (mut engine, mut client_rx) = make_engine_with_client();
9248 insert_track(
9249 &mut engine,
9250 Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
9251 );
9252
9253 engine
9254 .handle_request(Action::TrackOfflineBounce {
9255 track_name: "track".to_string(),
9256 output_path: "/tmp/out.wav".to_string(),
9257 start_sample: 0,
9258 length_samples: 0,
9259 automation_lanes: vec![],
9260 apply_fader: false,
9261 })
9262 .await;
9263
9264 match client_rx.recv().await.expect("response") {
9265 Message::Response(Err(err)) => {
9266 assert!(err.contains("has no renderable content for offline bounce"));
9267 }
9268 other => panic!("unexpected message: {other:?}"),
9269 }
9270 }
9271
9272 #[tokio::test]
9273 async fn track_offline_bounce_rejects_when_same_track_is_active() {
9274 let (mut engine, mut client_rx) = make_engine_with_client();
9275 engine.offline_bounce_jobs.insert(
9276 "other".to_string(),
9277 OfflineBounceJob {
9278 cancel: Arc::new(AtomicBool::new(false)),
9279 },
9280 );
9281
9282 engine
9283 .handle_request(Action::TrackOfflineBounce {
9284 track_name: "other".to_string(),
9285 output_path: "/tmp/out.wav".to_string(),
9286 start_sample: 0,
9287 length_samples: 128,
9288 automation_lanes: vec![],
9289 apply_fader: false,
9290 })
9291 .await;
9292
9293 match client_rx.recv().await.expect("response") {
9294 Message::Response(Err(err)) => {
9295 assert!(err.contains("already in progress"));
9296 }
9297 other => panic!("unexpected message: {other:?}"),
9298 }
9299 }
9300
9301 #[tokio::test]
9302 async fn track_offline_bounce_allows_different_track_concurrently() {
9303 let (mut engine, _client_rx) = make_engine_with_client();
9304 insert_track(
9305 &mut engine,
9306 Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
9307 );
9308 engine.offline_bounce_jobs.insert(
9309 "other".to_string(),
9310 OfflineBounceJob {
9311 cancel: Arc::new(AtomicBool::new(false)),
9312 },
9313 );
9314
9315 engine
9316 .handle_request(Action::TrackOfflineBounce {
9317 track_name: "track".to_string(),
9318 output_path: "/tmp/out.wav".to_string(),
9319 start_sample: 0,
9320 length_samples: 128,
9321 automation_lanes: vec![],
9322 apply_fader: false,
9323 })
9324 .await;
9325
9326 assert!(engine.offline_bounce_jobs.contains_key("other"));
9327 assert_eq!(engine.pending_requests.len(), 1);
9328 }
9329
9330 #[tokio::test]
9331 async fn reject_if_track_frozen_sends_error_and_blocks_operation() {
9332 let (mut engine, mut client_rx) = make_engine_with_client();
9333 let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
9334 track.set_frozen(true);
9335 insert_track(&mut engine, track);
9336
9337 let rejected = engine
9338 .reject_if_track_frozen("track", "arming/disarming")
9339 .await;
9340
9341 assert!(rejected);
9342 match client_rx.recv().await.expect("response") {
9343 Message::Response(Err(err)) => {
9344 assert_eq!(err, "Track 'track' is frozen; arming/disarming is blocked");
9345 }
9346 other => panic!("unexpected message: {other:?}"),
9347 }
9348 }
9349
9350 #[tokio::test]
9351 async fn undo_restores_original_clip_bounds_after_stretch_style_group() {
9352 let (mut engine, _client_rx) = make_engine_with_client();
9353 let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
9354 let mut clip = AudioClip::new("audio/original.wav".to_string(), 100, 220);
9355 clip.offset = 12;
9356 clip.fade_in_samples = 20;
9357 clip.fade_out_samples = 30;
9358 track.audio.clips.push(clip);
9359 insert_track(&mut engine, track);
9360
9361 engine.handle_request(Action::BeginHistoryGroup).await;
9362 engine
9363 .handle_request(Action::SetClipBounds {
9364 track_name: "track".to_string(),
9365 clip_index: 0,
9366 kind: Kind::Audio,
9367 start: 120,
9368 length: 180,
9369 offset: 0,
9370 })
9371 .await;
9372 engine
9373 .handle_request(Action::SetClipSourceName {
9374 track_name: "track".to_string(),
9375 clip_index: 0,
9376 kind: Kind::Audio,
9377 name: "audio/stretched.wav".to_string(),
9378 })
9379 .await;
9380 engine
9381 .handle_request(Action::SetClipFade {
9382 track_name: "track".to_string(),
9383 clip_index: 0,
9384 kind: Kind::Audio,
9385 fade_enabled: true,
9386 fade_in_samples: 12,
9387 fade_out_samples: 12,
9388 })
9389 .await;
9390 engine.handle_request(Action::EndHistoryGroup).await;
9391
9392 engine.handle_request(Action::Undo).await;
9393
9394 let state = engine.state.lock();
9395 let track = state.tracks.get("track").expect("track exists").lock();
9396 let clip = track.audio.clips.first().expect("clip exists");
9397 assert_eq!(clip.name, "audio/original.wav");
9398 assert_eq!(clip.start, 100);
9399 assert_eq!(clip.end, 220);
9400 assert_eq!(clip.end.saturating_sub(clip.start), 120);
9401 assert_eq!(clip.offset, 12);
9402 }
9403
9404 #[tokio::test]
9405 async fn track_offline_bounce_queues_when_no_worker_is_ready() {
9406 let (mut engine, _client_rx) = make_engine_with_client();
9407 insert_track(
9408 &mut engine,
9409 Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
9410 );
9411
9412 engine
9413 .handle_request(Action::TrackOfflineBounce {
9414 track_name: "track".to_string(),
9415 output_path: "/tmp/out.wav".to_string(),
9416 start_sample: 0,
9417 length_samples: 128,
9418 automation_lanes: vec![],
9419 apply_fader: false,
9420 })
9421 .await;
9422
9423 assert!(engine.offline_bounce_jobs.is_empty());
9424 assert_eq!(engine.pending_requests.len(), 1);
9425 assert!(matches!(
9426 engine.pending_requests.front(),
9427 Some(Action::TrackOfflineBounce { track_name, length_samples, .. })
9428 if track_name == "track" && *length_samples == 128
9429 ));
9430 }
9431
9432 #[tokio::test]
9433 async fn track_offline_bounce_returns_missing_track_error() {
9434 let (mut engine, mut client_rx) = make_engine_with_client();
9435
9436 engine
9437 .handle_request(Action::TrackOfflineBounce {
9438 track_name: "missing".to_string(),
9439 output_path: "/tmp/out.wav".to_string(),
9440 start_sample: 0,
9441 length_samples: 128,
9442 automation_lanes: vec![],
9443 apply_fader: false,
9444 })
9445 .await;
9446
9447 match client_rx.recv().await.expect("response") {
9448 Message::Response(Err(err)) => {
9449 assert_eq!(err, "Track not found: missing");
9450 }
9451 other => panic!("unexpected message: {other:?}"),
9452 }
9453 }
9454
9455 #[tokio::test]
9456 async fn track_offline_bounce_clears_job_when_worker_send_fails() {
9457 let (mut engine, mut client_rx) = make_engine_with_client();
9458 insert_track(
9459 &mut engine,
9460 Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
9461 );
9462 let (worker_tx, worker_rx) = channel(1);
9463 drop(worker_rx);
9464 engine
9465 .workers
9466 .push(WorkerData::new(worker_tx, tokio::spawn(async {})));
9467 engine.ready_workers.push(0);
9468
9469 engine
9470 .handle_request(Action::TrackOfflineBounce {
9471 track_name: "track".to_string(),
9472 output_path: "/tmp/out.wav".to_string(),
9473 start_sample: 0,
9474 length_samples: 128,
9475 automation_lanes: vec![],
9476 apply_fader: false,
9477 })
9478 .await;
9479
9480 assert!(engine.offline_bounce_jobs.is_empty());
9481 match client_rx.recv().await.expect("response") {
9482 Message::Response(Err(err)) => {
9483 assert!(err.contains("Failed to schedule offline bounce"));
9484 }
9485 other => panic!("unexpected message: {other:?}"),
9486 }
9487 }
9488
9489 #[tokio::test]
9490 async fn play_stop_play_keeps_clip_output_audible() {
9491 use crate::audio::clip::AudioClip;
9492 use crate::audio_codec::write_wav_f32;
9493
9494 let (engine_tx, engine_rx) = channel(16);
9495 let mut engine = Engine::new(engine_rx, engine_tx);
9496 let state = engine.state();
9497 let (client_tx, mut client_rx) = channel(16);
9498 engine.clients.push(client_tx);
9499 engine.init().await;
9500
9501 let tmp_dir = std::env::temp_dir().join("maolan_play_stop_play_test");
9502 let _ = std::fs::create_dir_all(&tmp_dir);
9503 let wav_path = tmp_dir.join("tone.wav");
9504 let sample_rate = 48_000u32;
9505 let clip_samples = sample_rate as usize;
9506 let mut samples = Vec::with_capacity(clip_samples);
9507 for i in 0..clip_samples {
9508 let phase = i as f32 / sample_rate as f32 * 2.0 * std::f32::consts::PI * 440.0;
9509 samples.push(phase.sin() * 0.5);
9510 }
9511 write_wav_f32(&wav_path, &samples, 1, sample_rate).expect("write wav");
9512
9513 let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 1024, sample_rate as f64);
9514 let mut clip = AudioClip::new(wav_path.to_string_lossy().to_string(), 0, clip_samples);
9515 clip.fade_enabled = false;
9516 track.audio.clips.push(clip);
9517 track.session_base_dir = Some(tmp_dir.clone());
9518 insert_track(&mut engine, track);
9519
9520 let tx = engine.tx.clone();
9521 let work_handle = tokio::spawn(async move {
9522 engine.work().await;
9523 });
9524
9525 tokio::time::sleep(TokioDuration::from_millis(100)).await;
9527
9528 async fn drain_responses(
9529 client_rx: &mut tokio::sync::mpsc::Receiver<Message>,
9530 count: usize,
9531 ) {
9532 for _ in 0..count {
9533 let _ = tokio::time::timeout(TokioDuration::from_secs(2), client_rx.recv()).await;
9534 }
9535 }
9536
9537 async fn wait_for_track_processed(
9538 client_rx: &mut tokio::sync::mpsc::Receiver<Message>,
9539 state: &Arc<UnsafeMutex<State>>,
9540 ) -> bool {
9541 let deadline = Instant::now() + Duration::from_secs(5);
9542 while Instant::now() < deadline {
9543 let msg =
9544 tokio::time::timeout(TokioDuration::from_millis(100), client_rx.recv()).await;
9545 if let Ok(Some(Message::Response(Ok(Action::TransportPosition(_)))))
9546 | Ok(Some(Message::Response(Ok(Action::Play)))) = msg
9547 {
9548 let track_deadline = Instant::now() + Duration::from_secs(5);
9549 while Instant::now() < track_deadline {
9550 if state
9551 .lock()
9552 .tracks
9553 .get("track")
9554 .map(|t| t.lock().audio.finished)
9555 .unwrap_or(false)
9556 {
9557 return true;
9558 }
9559 tokio::time::sleep(TokioDuration::from_millis(10)).await;
9560 }
9561 }
9562 }
9563 false
9564 }
9565
9566 tx.send(Message::Request(Action::SetClipPlaybackEnabled(true)))
9567 .await
9568 .unwrap();
9569 tx.send(Message::Request(Action::Play)).await.unwrap();
9570 assert!(
9571 wait_for_track_processed(&mut client_rx, &state).await,
9572 "track did not process on first play"
9573 );
9574 let first_peak = {
9575 let state = state.lock();
9576 let track = state.tracks.get("track").expect("track").lock();
9577 let input = track.audio.ins[0].buffer.lock();
9578 crate::simd::peak_abs(input)
9579 };
9580 assert!(
9581 first_peak > 0.001,
9582 "expected audible input on first play, got {first_peak}"
9583 );
9584
9585 tx.send(Message::Request(Action::SetClipPlaybackEnabled(true)))
9586 .await
9587 .unwrap();
9588 tx.send(Message::Request(Action::Stop)).await.unwrap();
9589 drain_responses(&mut client_rx, 2).await;
9590
9591 tx.send(Message::Request(Action::SetClipPlaybackEnabled(true)))
9592 .await
9593 .unwrap();
9594 tx.send(Message::Request(Action::Play)).await.unwrap();
9595 assert!(
9596 wait_for_track_processed(&mut client_rx, &state).await,
9597 "track did not process on second play"
9598 );
9599 let second_peak = {
9600 let state = state.lock();
9601 let track = state.tracks.get("track").expect("track").lock();
9602 let input = track.audio.ins[0].buffer.lock();
9603 crate::simd::peak_abs(input)
9604 };
9605 assert!(
9606 second_peak > 0.001,
9607 "expected audible input on second play after stop, got {second_peak}"
9608 );
9609
9610 let _ = tx.send(Message::Request(Action::Quit)).await;
9611 tokio::time::sleep(TokioDuration::from_millis(200)).await;
9612 work_handle.abort();
9613 let _ = std::fs::remove_dir_all(&tmp_dir);
9614 }
9615
9616 #[test]
9617 fn modulator_sets_track_volume() {
9618 let (mut engine, _client_rx) = make_engine_with_client();
9619 let track = Track::new("vol-track".to_string(), 0, 2, 0, 0, 128, 48_000.0);
9620 insert_track(&mut engine, track);
9621
9622 engine.modulators = vec![crate::modulator::Modulator {
9623 id: 1,
9624 name: "LFO".to_string(),
9625 shape: crate::modulator::ModulatorShape::Sine,
9626 rate_hz: 1.0,
9627 phase: 0.0,
9628 enabled: true,
9629 targets: vec![crate::modulator::ModulatorTarget::TrackVolume {
9630 track_name: "vol-track".to_string(),
9631 min: -90.0,
9632 max: 20.0,
9633 }],
9634 }];
9635
9636 let echoes = engine.apply_modulators(12_000);
9638 let track = engine.state.lock().tracks["vol-track"].lock();
9639 assert!(
9640 (track.level() - 20.0).abs() < 0.01,
9641 "expected 20 dB, got {}",
9642 track.level()
9643 );
9644 assert!(
9645 echoes
9646 .iter()
9647 .any(|a| matches!(a, Action::TrackAutomationLevel(name, _) if name == "vol-track"))
9648 );
9649 }
9650
9651 #[test]
9652 fn modulator_sets_track_balance() {
9653 let (mut engine, _client_rx) = make_engine_with_client();
9654 let track = Track::new("pan-track".to_string(), 0, 2, 0, 0, 128, 48_000.0);
9655 insert_track(&mut engine, track);
9656
9657 engine.modulators = vec![crate::modulator::Modulator {
9658 id: 1,
9659 name: "LFO".to_string(),
9660 shape: crate::modulator::ModulatorShape::Sine,
9661 rate_hz: 1.0,
9662 phase: 0.0,
9663 enabled: true,
9664 targets: vec![crate::modulator::ModulatorTarget::TrackBalance {
9665 track_name: "pan-track".to_string(),
9666 min: -1.0,
9667 max: 1.0,
9668 }],
9669 }];
9670
9671 let echoes = engine.apply_modulators(12_000);
9673 let track = engine.state.lock().tracks["pan-track"].lock();
9674 assert!(
9675 (track.balance - 1.0).abs() < 0.01,
9676 "expected balance 1.0, got {}",
9677 track.balance
9678 );
9679 assert!(
9680 echoes.iter().any(
9681 |a| matches!(a, Action::TrackAutomationBalance(name, _) if name == "pan-track")
9682 )
9683 );
9684 }
9685
9686 #[tokio::test]
9687 async fn track_set_parent_wires_folder_input_to_child_input_and_child_output_to_folder_output()
9688 {
9689 let (mut engine, mut client_rx) = make_engine_with_client();
9690 let folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9691 let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9692 insert_track(&mut engine, folder);
9693 insert_track(&mut engine, child);
9694
9695 engine
9696 .handle_request_inner(
9697 Action::TrackSetParent {
9698 track_name: "child".to_string(),
9699 parent_name: Some("folder".to_string()),
9700 },
9701 false,
9702 )
9703 .await;
9704
9705 while let Ok(Some(_)) =
9707 tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
9708 {}
9709
9710 let state = engine.state.lock();
9711 let folder = state.tracks.get("folder").unwrap().lock();
9712 let child = state.tracks.get("child").unwrap().lock();
9713
9714 assert!(folder.child_tracks.iter().any(|c| c.lock().name == "child"));
9715 assert_eq!(child.parent_track.as_deref(), Some("folder"));
9716
9717 for (i, (parent_in, child_in)) in folder.audio.ins.iter().zip(&child.audio.ins).enumerate()
9719 {
9720 assert!(
9721 child_in
9722 .connections
9723 .lock()
9724 .iter()
9725 .any(|c| Arc::ptr_eq(c, parent_in)),
9726 "folder input {i} is not routed to child input {i}"
9727 );
9728 assert!(
9729 !parent_in
9730 .connections
9731 .lock()
9732 .iter()
9733 .any(|c| Arc::ptr_eq(c, child_in)),
9734 "folder input {i} should not read from child input {i}"
9735 );
9736 }
9737
9738 for (i, (child_out, parent_out)) in
9740 child.audio.outs.iter().zip(&folder.audio.outs).enumerate()
9741 {
9742 assert!(
9743 parent_out
9744 .connections
9745 .lock()
9746 .iter()
9747 .any(|c| Arc::ptr_eq(c, child_out)),
9748 "child output {i} is not routed to folder output {i}"
9749 );
9750 }
9751
9752 for (i, child_out) in child.audio.outs.iter().enumerate() {
9754 assert!(
9755 child_out.connections.lock().iter().any(|c| {
9756 child
9757 .audio
9758 .ins
9759 .get(i)
9760 .is_some_and(|inp| Arc::ptr_eq(c, inp))
9761 }),
9762 "child output {i} is not connected to child input {i}"
9763 );
9764 }
9765 }
9766
9767 #[tokio::test]
9768 async fn track_set_parent_to_none_restores_root_passthrough() {
9769 let (mut engine, mut client_rx) = make_engine_with_client();
9770 let folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9771 let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9772 insert_track(&mut engine, folder);
9773 insert_track(&mut engine, child);
9774
9775 engine
9776 .handle_request_inner(
9777 Action::TrackSetParent {
9778 track_name: "child".to_string(),
9779 parent_name: Some("folder".to_string()),
9780 },
9781 false,
9782 )
9783 .await;
9784 engine
9785 .handle_request_inner(
9786 Action::TrackSetParent {
9787 track_name: "child".to_string(),
9788 parent_name: None,
9789 },
9790 false,
9791 )
9792 .await;
9793
9794 while let Ok(Some(_)) =
9795 tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
9796 {}
9797
9798 let state = engine.state.lock();
9799 let folder = state.tracks.get("folder").unwrap().lock();
9800 let child = state.tracks.get("child").unwrap().lock();
9801
9802 assert!(folder.child_tracks.is_empty());
9803 assert!(child.parent_track.is_none());
9804
9805 for (i, child_out) in child.audio.outs.iter().enumerate() {
9806 assert!(
9807 child_out.connections.lock().iter().any(|c| {
9808 child
9809 .audio
9810 .ins
9811 .get(i)
9812 .is_some_and(|inp| Arc::ptr_eq(c, inp))
9813 }),
9814 "child output {i} should be connected to child input {i} after moving to root"
9815 );
9816 }
9817 }
9818
9819 #[tokio::test]
9820 async fn track_set_parent_wires_folder_midi_to_child_midi() {
9821 let (mut engine, mut client_rx) = make_engine_with_client();
9822 let folder = Track::new_folder("folder".to_string(), 0, 0, 1, 1, 64, 48_000.0);
9823 let child = Track::new("child".to_string(), 0, 0, 1, 1, 64, 48_000.0);
9824 insert_track(&mut engine, folder);
9825 insert_track(&mut engine, child);
9826
9827 engine
9828 .handle_request_inner(
9829 Action::TrackSetParent {
9830 track_name: "child".to_string(),
9831 parent_name: Some("folder".to_string()),
9832 },
9833 false,
9834 )
9835 .await;
9836
9837 while let Ok(Some(_)) =
9838 tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
9839 {}
9840
9841 let state = engine.state.lock();
9842 let folder = state.tracks.get("folder").unwrap().lock();
9843 let child = state.tracks.get("child").unwrap().lock();
9844
9845 let folder_midi_in = &folder.midi.ins[0];
9846 let child_midi_in = &child.midi.ins[0];
9847 assert!(
9848 child_midi_in
9849 .lock()
9850 .connections
9851 .iter()
9852 .any(|c| Arc::ptr_eq(c, folder_midi_in)),
9853 "folder MIDI input should be routed to child MIDI input"
9854 );
9855
9856 let child_midi_out = &child.midi.outs[0];
9857 let folder_midi_out = &folder.midi.outs[0];
9858 assert!(
9859 child_midi_out
9860 .lock()
9861 .connections
9862 .iter()
9863 .any(|c| Arc::ptr_eq(c, folder_midi_out)),
9864 "child MIDI output should be routed to folder MIDI output"
9865 );
9866 }
9867
9868 #[test]
9869 fn nested_folder_expands_in_task_graph() {
9870 let (mut engine, _client_rx) = make_engine_with_client();
9871 let outer = Track::new_folder("outer".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9872 let inner = Track::new_folder("inner".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9873 let leaf = Track::new("leaf".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9874 insert_track(&mut engine, outer);
9875 insert_track(&mut engine, inner);
9876 insert_track(&mut engine, leaf);
9877
9878 {
9879 let state = engine.state.lock();
9880 let outer = state.tracks.get("outer").unwrap().clone();
9881 let inner = state.tracks.get("inner").unwrap().clone();
9882 let leaf = state.tracks.get("leaf").unwrap().clone();
9883 outer.lock().child_tracks.push(inner.clone());
9884 inner.lock().child_tracks.push(leaf.clone());
9885 inner.lock().parent_track = Some("outer".to_string());
9886 leaf.lock().parent_track = Some("inner".to_string());
9887 }
9888
9889 let (tasks, deps) = engine.build_task_graph();
9890 let names: Vec<String> = tasks
9891 .iter()
9892 .map(|t| match t {
9893 ProcessTask::Track(t) => format!("track:{}", t.lock().name.clone()),
9894 ProcessTask::FolderInput(t) => format!("in:{}", t.lock().name.clone()),
9895 ProcessTask::FolderOutput(t) => format!("out:{}", t.lock().name.clone()),
9896 ProcessTask::Plugin { track, .. } => {
9897 format!("plugin:{}", track.lock().name.clone())
9898 }
9899 })
9900 .collect();
9901
9902 let expected = vec![
9903 "in:outer",
9904 "in:inner",
9905 "track:leaf",
9906 "out:inner",
9907 "out:outer",
9908 ];
9909 assert_eq!(names, expected, "task graph should expand nested folders");
9910
9911 for window in tasks.windows(2) {
9913 let prev = &window[0];
9914 let next = &window[1];
9915 let prev_key = Engine::task_key(prev);
9916 let next_key = Engine::task_key(next);
9917 assert!(
9918 deps.get(&next_key).is_some_and(|d| d.contains(&prev_key)),
9919 "{:?} should depend on {:?}",
9920 next,
9921 prev
9922 );
9923 }
9924 }
9925
9926 #[test]
9927 fn child_to_plugin_to_folder_output_task_graph_has_no_cycle() {
9928 use crate::message::ConnectableRef;
9929
9930 let plugin_path = Path::new(env!("CARGO_MANIFEST_DIR"))
9931 .parent()
9932 .unwrap()
9933 .join("daw")
9934 .join("plugin-host")
9935 .join("tests")
9936 .join("test_passthrough.clap");
9937 if !plugin_path.exists() {
9938 return;
9939 }
9940 if crate::plugins::ipc::find_plugin_host_binary().is_none() {
9941 return;
9942 }
9943
9944 let (mut engine, _client_rx) = make_engine_with_client();
9945 let mut folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9946 let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
9947
9948 folder
9949 .load_clap_plugin(
9950 &format!("{}::com.maolan.test.passthrough", plugin_path.display()),
9951 None,
9952 )
9953 .expect("should load CLAP plugin on folder");
9954 folder.clap_plugins[0].processor.lock().setup_audio_ports();
9955 let plugin_id = folder.clap_plugins[0].id;
9956
9957 insert_track(&mut engine, folder);
9958 insert_track(&mut engine, child);
9959
9960 {
9961 let state = engine.state.lock();
9962 let folder = state.tracks.get("folder").unwrap().clone();
9963 let child = state.tracks.get("child").unwrap().clone();
9964 folder.lock().child_tracks.push(child.clone());
9965 child.lock().parent_track = Some("folder".to_string());
9966
9967 folder
9968 .lock()
9969 .connect_audio_connectable(
9970 ConnectableRef::ChildTrack("child".to_string()),
9971 0,
9972 ConnectableRef::ClapPlugin(plugin_id),
9973 0,
9974 )
9975 .expect("connect child L to plugin L");
9976 folder
9977 .lock()
9978 .connect_audio_connectable(
9979 ConnectableRef::ChildTrack("child".to_string()),
9980 1,
9981 ConnectableRef::ClapPlugin(plugin_id),
9982 1,
9983 )
9984 .expect("connect child R to plugin R");
9985 folder
9986 .lock()
9987 .connect_audio_connectable(
9988 ConnectableRef::ClapPlugin(plugin_id),
9989 0,
9990 ConnectableRef::TrackOutput,
9991 0,
9992 )
9993 .expect("connect plugin L to folder output L");
9994 folder
9995 .lock()
9996 .connect_audio_connectable(
9997 ConnectableRef::ClapPlugin(plugin_id),
9998 1,
9999 ConnectableRef::TrackOutput,
10000 1,
10001 )
10002 .expect("connect plugin R to folder output R");
10003 }
10004
10005 let (tasks, deps) = engine.build_task_graph();
10006
10007 let folder_in_key = tasks
10008 .iter()
10009 .find(|t| matches!(t, ProcessTask::FolderInput(t) if t.lock().name == "folder"))
10010 .map(Engine::task_key)
10011 .expect("folder input task");
10012 let child_key = tasks
10013 .iter()
10014 .find(|t| matches!(t, ProcessTask::Track(t) if t.lock().name == "child"))
10015 .map(Engine::task_key)
10016 .expect("child task");
10017 let plugin_key = tasks
10018 .iter()
10019 .find(|t| {
10020 matches!(
10021 t,
10022 ProcessTask::Plugin {
10023 track,
10024 kind: PluginKind::Clap,
10025 index: 0,
10026 } if track.lock().name == "folder"
10027 )
10028 })
10029 .map(Engine::task_key)
10030 .expect("plugin task");
10031 let folder_out_key = tasks
10032 .iter()
10033 .find(|t| matches!(t, ProcessTask::FolderOutput(t) if t.lock().name == "folder"))
10034 .map(Engine::task_key)
10035 .expect("folder output task");
10036
10037 assert!(
10038 deps.get(&child_key)
10039 .is_some_and(|d| d.contains(&folder_in_key)),
10040 "child task should depend on folder input"
10041 );
10042 assert!(
10043 deps.get(&plugin_key)
10044 .is_some_and(|d| d.contains(&folder_in_key) && d.contains(&child_key)),
10045 "plugin task should depend on folder input and child"
10046 );
10047 assert!(
10048 deps.get(&folder_out_key).is_some_and(|d| {
10049 d.contains(&folder_in_key) && d.contains(&plugin_key) && d.contains(&child_key)
10050 }),
10051 "folder output should depend on folder input, plugin, and child"
10052 );
10053
10054 fn has_cycle(deps: &HashMap<String, Vec<String>>) -> bool {
10055 let mut state: HashMap<String, u8> = HashMap::new();
10056 fn visit(
10057 node: &str,
10058 deps: &HashMap<String, Vec<String>>,
10059 state: &mut HashMap<String, u8>,
10060 ) -> bool {
10061 match state.get(node).copied() {
10062 Some(1) => return true,
10063 Some(2) => return false,
10064 _ => {}
10065 }
10066 state.insert(node.to_string(), 1);
10067 for next in deps.get(node).into_iter().flatten() {
10068 if visit(next, deps, state) {
10069 return true;
10070 }
10071 }
10072 state.insert(node.to_string(), 2);
10073 false
10074 }
10075 for node in deps.keys() {
10076 if visit(node, deps, &mut state) {
10077 return true;
10078 }
10079 }
10080 false
10081 }
10082
10083 assert!(
10084 !has_cycle(&deps),
10085 "task graph should not contain a cycle when a plugin reads from a child track"
10086 );
10087 }
10088
10089 #[tokio::test]
10090 async fn track_set_parent_wires_child_io_to_folder_even_after_addtrack() {
10091 let (mut engine, mut client_rx) = make_engine_with_client();
10092 let folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
10093 let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
10094 insert_track(&mut engine, folder);
10095 insert_track(&mut engine, child);
10096
10097 engine
10098 .handle_request_inner(
10099 Action::TrackSetParent {
10100 track_name: "child".to_string(),
10101 parent_name: Some("folder".to_string()),
10102 },
10103 false,
10104 )
10105 .await;
10106
10107 while let Ok(Some(_)) =
10108 tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
10109 {}
10110
10111 let state = engine.state.lock();
10112 let folder = state.tracks.get("folder").unwrap().lock();
10113 let child = state.tracks.get("child").unwrap().lock();
10114
10115 for (i, (parent_in, child_in)) in folder.audio.ins.iter().zip(&child.audio.ins).enumerate()
10117 {
10118 assert!(
10119 child_in
10120 .connections
10121 .lock()
10122 .iter()
10123 .any(|c| Arc::ptr_eq(c, parent_in)),
10124 "folder input {i} is not routed to child input {i}"
10125 );
10126 }
10127
10128 for (i, (child_out, parent_out)) in
10130 child.audio.outs.iter().zip(&folder.audio.outs).enumerate()
10131 {
10132 assert!(
10133 parent_out
10134 .connections
10135 .lock()
10136 .iter()
10137 .any(|c| Arc::ptr_eq(c, child_out)),
10138 "child output {i} is not routed to folder output {i}"
10139 );
10140 }
10141 }
10142
10143 #[tokio::test]
10144 async fn folder_child_audio_passes_through() {
10145 let (mut engine, mut client_rx) = make_engine_with_client();
10146 let folder = Track::new_folder("folder".to_string(), 1, 1, 0, 0, 64, 48_000.0);
10147 let child = Track::new("child".to_string(), 1, 1, 0, 0, 64, 48_000.0);
10148 insert_track(&mut engine, folder);
10149 insert_track(&mut engine, child);
10150
10151 engine
10152 .handle_request_inner(
10153 Action::TrackSetParent {
10154 track_name: "child".to_string(),
10155 parent_name: Some("folder".to_string()),
10156 },
10157 false,
10158 )
10159 .await;
10160 while let Ok(Some(_)) =
10161 tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
10162 {}
10163
10164 {
10165 let state = engine.state.lock();
10166 let folder = state.tracks.get("folder").unwrap().clone();
10167 let child = state.tracks.get("child").unwrap().clone();
10168
10169 folder.lock().input_monitor = vec![true];
10170 child.lock().input_monitor = vec![true];
10171
10172 let source = Arc::new(crate::audio::io::AudioIO::new(64));
10174 for sample in source.buffer.lock().iter_mut() {
10175 *sample = 0.75;
10176 }
10177 crate::audio::io::AudioIO::connect(&source, &folder.lock().audio.ins[0]);
10178
10179 folder.lock().process_folder_input();
10180 child.lock().process();
10181 folder.lock().process_folder_output();
10182
10183 let output = folder.lock().audio.outs[0].buffer.lock();
10184 assert!(
10185 output.iter().any(|s| (*s - 0.75).abs() < 1e-5),
10186 "folder output should contain the child-processed folder input signal, got {:?}",
10187 output.iter().take(8).collect::<Vec<_>>()
10188 );
10189 }
10190 }
10191
10192 #[tokio::test]
10193 async fn remove_folder_track_deletes_descendants_recursively() {
10194 let (mut engine, mut client_rx) = make_engine_with_client();
10195 let folder = Track::new_folder("folder".to_string(), 1, 1, 0, 0, 64, 48_000.0);
10196 let child = Track::new_folder("child".to_string(), 1, 1, 0, 0, 64, 48_000.0);
10197 let grandchild = Track::new("grandchild".to_string(), 1, 1, 0, 0, 64, 48_000.0);
10198 insert_track(&mut engine, folder);
10199 insert_track(&mut engine, child);
10200 insert_track(&mut engine, grandchild);
10201
10202 engine
10203 .handle_request(Action::TrackSetParent {
10204 track_name: "child".to_string(),
10205 parent_name: Some("folder".to_string()),
10206 })
10207 .await;
10208 engine
10209 .handle_request(Action::TrackSetParent {
10210 track_name: "grandchild".to_string(),
10211 parent_name: Some("child".to_string()),
10212 })
10213 .await;
10214
10215 while let Ok(Some(_)) =
10217 tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
10218 {}
10219
10220 engine
10221 .handle_request(Action::RemoveTrack("folder".to_string()))
10222 .await;
10223
10224 {
10225 let state = engine.state.lock();
10226 assert!(
10227 !state.tracks.contains_key("folder"),
10228 "folder should have been removed"
10229 );
10230 assert!(
10231 !state.tracks.contains_key("child"),
10232 "child should have been removed"
10233 );
10234 assert!(
10235 !state.tracks.contains_key("grandchild"),
10236 "grandchild should have been removed"
10237 );
10238 }
10239
10240 let mut removed_names = Vec::new();
10241 for _ in 0..3 {
10242 let msg = tokio::time::timeout(TokioDuration::from_millis(100), client_rx.recv()).await;
10243 if let Ok(Some(Message::Response(Ok(Action::RemoveTrack(name))))) = msg {
10244 removed_names.push(name);
10245 }
10246 }
10247 assert_eq!(
10248 removed_names,
10249 vec!["grandchild", "child", "folder"],
10250 "descendants should be removed before the folder and clients notified"
10251 );
10252 }
10253}