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