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