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