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