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