use midly::{
Arena, Format, Header, MetaMessage, Smf, Timing, TrackEvent, TrackEventKind,
live::LiveEvent,
num::{u15, u24, u28},
};
#[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
use std::fs::read_dir;
use std::{
collections::{HashMap, VecDeque},
fs::File,
path::{Path, PathBuf},
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};
use tokio::sync::mpsc::{Receiver, Sender, channel};
use tokio::task::JoinHandle;
use tracing::error;
type HwDeviceInfo = (usize, usize, usize, ((usize, usize), (usize, usize)));
#[cfg(target_os = "linux")]
use crate::hw::alsa::{HwDriver, HwOptions, MidiHub};
#[cfg(target_os = "macos")]
use crate::hw::coreaudio::{HwDriver, HwOptions, MidiHub};
#[cfg(unix)]
use crate::hw::jack::JackRuntime;
#[cfg(target_os = "windows")]
use crate::hw::options::HwOptions;
#[cfg(target_os = "freebsd")]
use crate::hw::oss as hw;
#[cfg(target_os = "freebsd")]
use crate::hw::oss::{HwDriver, HwOptions, MidiHub};
#[cfg(target_os = "openbsd")]
use crate::hw::sndio::{HwDriver, HwOptions, MidiHub};
#[cfg(target_os = "windows")]
use crate::hw::wasapi::{self, HwDriver, MidiHub};
#[cfg(target_os = "linux")]
use crate::workers::alsa_worker::HwWorker;
#[cfg(target_os = "macos")]
use crate::workers::coreaudio_worker::HwWorker;
#[cfg(target_os = "freebsd")]
use crate::workers::oss_worker::HwWorker;
#[cfg(target_os = "openbsd")]
use crate::workers::sndio_worker::HwWorker;
#[cfg(target_os = "windows")]
use crate::workers::wasapi_worker::HwWorker;
use crate::{
audio::clip::AudioClip,
audio::io::AudioIO,
history::{History, UndoEntry, create_inverse_actions, should_record},
hw::{
config,
traits::{HwDevice, HwWorkerDriver},
},
kind::Kind,
message::{
Action, HwMidiEvent, Message, MidiControllerData, MidiNoteData, PluginKind, ProcessTask,
},
midi::clip::MIDIClip,
midi::io::{MIDIIO, MidiEvent},
mutex::UnsafeMutex,
osc::OscServer,
routing,
state::State,
track::Track,
workers::worker::Worker,
};
#[derive(Debug)]
struct WorkerData {
tx: Sender<Message>,
handle: JoinHandle<()>,
}
impl WorkerData {
pub fn new(tx: Sender<Message>, handle: JoinHandle<()>) -> Self {
Self { tx, handle }
}
}
#[derive(Debug, Clone)]
struct RecordingSession {
start_sample: usize,
samples: Vec<f32>,
channels: usize,
file_name: String,
stripe_peaks: Vec<Vec<[f32; 2]>>,
current_stripe_frames: usize,
}
const RECORDING_STRIPE_FRAMES: usize = 256;
#[derive(Debug, Clone)]
struct MidiRecordingSession {
start_sample: usize,
events: Vec<(u64, Vec<u8>)>,
file_name: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct MidiHwInRoute {
device: String,
to_track: String,
to_port: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct MidiHwOutRoute {
from_track: String,
from_port: usize,
device: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct MidiHwThruRoute {
from_device: String,
to_device: String,
}
struct OfflineBounceJob {
cancel: Arc<AtomicBool>,
}
#[cfg(unix)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum JackTransportPlaySync {
Start,
Stop,
}
#[derive(Clone, Copy)]
#[cfg(unix)]
struct AudioOpenRequest<'a> {
device: &'a str,
input_device: Option<&'a str>,
sample_rate_hz: i32,
bits: i32,
exclusive: bool,
period_frames: usize,
nperiods: usize,
sync_mode: bool,
}
struct ClipAddRequest<'a> {
name: &'a str,
track_name: &'a str,
start: usize,
length: usize,
offset: usize,
input_channel: usize,
muted: bool,
peaks_file: Option<String>,
kind: Kind,
fade_enabled: bool,
fade_in_samples: usize,
fade_out_samples: usize,
source_name: Option<String>,
source_offset: Option<usize>,
source_length: Option<usize>,
preview_name: Option<String>,
pitch_correction_points: Vec<crate::message::PitchCorrectionPointData>,
pitch_correction_frame_likeness: Option<f32>,
pitch_correction_inertia_ms: Option<u16>,
pitch_correction_formant_compensation: Option<bool>,
plugin_graph_json: Option<serde_json::Value>,
}
#[cfg(unix)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct JackTransportSyncDecision {
play_sync: Option<JackTransportPlaySync>,
position_sync: Option<usize>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum MidiLearnSlot {
Track(String, crate::message::TrackMidiLearnTarget),
Global(crate::message::GlobalMidiLearnTarget),
}
pub struct Engine {
clients: Vec<Sender<Message>>,
rx: Receiver<Message>,
state: Arc<UnsafeMutex<State>>,
tx: Sender<Message>,
workers: Vec<WorkerData>,
hw_driver: Option<Arc<UnsafeMutex<HwDriver>>>,
#[cfg(unix)]
jack_runtime: Option<Arc<UnsafeMutex<JackRuntime>>>,
midi_hub: Arc<UnsafeMutex<MidiHub>>,
hw_worker: Option<WorkerData>,
osc_server: Option<OscServer>,
pending_hw_midi_events: Vec<MidiEvent>,
pending_hw_midi_events_by_device: HashMap<String, Vec<MidiEvent>>,
pending_hw_midi_out_events: Vec<MidiEvent>,
pending_hw_midi_out_events_by_device: Vec<HwMidiEvent>,
active_hw_notes_by_track: HashMap<String, std::collections::HashSet<(String, u8, u8)>>,
active_hw_notes_cycle_start: HashMap<String, std::collections::HashSet<(String, u8, u8)>>,
midi_hw_in_routes: Vec<MidiHwInRoute>,
midi_hw_out_routes: Vec<MidiHwOutRoute>,
midi_hw_thru_routes: Vec<MidiHwThruRoute>,
ready_workers: Vec<usize>,
pending_requests: VecDeque<Action>,
awaiting_hwfinished: bool,
handling_hwfinished: bool,
track_process_epoch: usize,
transport_panic_flush_pending: bool,
transport_restart_pending: bool,
notified_loop_wrap_sample: Option<usize>,
transport_sample: usize,
hw_input_latency_frames: usize,
hw_output_latency_frames: usize,
loop_enabled: bool,
loop_range_samples: Option<(usize, usize)>,
metronome_enabled: bool,
tempo_bpm: f64,
tsig_num: u16,
tsig_denom: u16,
punch_enabled: bool,
punch_range_samples: Option<(usize, usize)>,
audio_recordings: std::collections::HashMap<String, RecordingSession>,
midi_recordings: std::collections::HashMap<String, MidiRecordingSession>,
completed_audio_recordings: Vec<(String, RecordingSession)>,
completed_midi_recordings: Vec<(String, MidiRecordingSession)>,
playing: bool,
clip_playback_enabled: bool,
record_enabled: bool,
step_recording_enabled: bool,
session_dir: Option<PathBuf>,
hw_out_level_db: f32,
hw_out_balance: f32,
hw_out_muted: bool,
last_hw_out_meter_publish: Option<Instant>,
#[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
last_hw_out_meter_linear: Vec<f32>,
hw_out_peak_hold_linear: Vec<f32>,
#[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
hw_out_meter_publish_phase: bool,
last_track_meter_publish: Option<Instant>,
track_meter_linear_by_track: HashMap<String, Vec<f32>>,
task_processing_started_at: HashMap<String, Instant>,
cycle_tasks: Vec<ProcessTask>,
cycle_task_deps: HashMap<String, Vec<String>>,
cycle_tasks_running: Vec<ProcessTask>,
cycle_tasks_finished: Vec<ProcessTask>,
latest_hw_out_meter_db: Arc<Vec<f32>>,
latest_track_meter_snapshot: Arc<Vec<(String, Vec<f32>)>>,
history: History,
history_group: Option<UndoEntry>,
history_suspended: bool,
offline_bounce_jobs: HashMap<String, OfflineBounceJob>,
pending_midi_learn: Option<(String, crate::message::TrackMidiLearnTarget, Option<String>)>,
pending_global_midi_learn: Option<crate::message::GlobalMidiLearnTarget>,
global_midi_learn_play_pause: Option<crate::message::MidiLearnBinding>,
global_midi_learn_stop: Option<crate::message::MidiLearnBinding>,
global_midi_learn_record_toggle: Option<crate::message::MidiLearnBinding>,
midi_cc_gate: HashMap<(String, u8, u8), bool>,
modulators: Vec<crate::modulator::Modulator>,
modulator_values: Option<Arc<std::collections::HashMap<usize, f32>>>,
}
type MidiEditParseResult = (
Vec<MidiNoteData>,
Vec<MidiControllerData>,
Vec<(u64, Vec<u8>)>,
);
impl Engine {
pub fn state(&self) -> Arc<UnsafeMutex<State>> {
self.state.clone()
}
const METRONOME_TRACK: &'static str = "metronome";
const METRONOME_DEFAULT_LEVEL_DB: f32 = -10.0;
const MIDI_CC_ALL_SOUND_OFF: u8 = 120;
const MIDI_CC_SUSTAIN_PEDAL: u8 = 64;
fn default_clip_plugin_graph_json(audio_ins: usize, audio_outs: usize) -> serde_json::Value {
let connections = (0..audio_ins.min(audio_outs))
.map(|port| {
serde_json::json!({
"from_node": "TrackInput",
"from_port": port,
"to_node": "TrackOutput",
"to_port": port,
"kind": "Audio",
})
})
.collect::<Vec<_>>();
serde_json::json!({
"plugins": [],
"connections": connections,
})
}
fn meter_linear_to_db(peak: f32) -> f32 {
if peak <= 1.0e-6 {
-90.0
} else {
(20.0 * peak.log10()).clamp(-90.0, 20.0)
}
}
fn note_off_events_for_track(&mut self, track_name: &str) -> Vec<HwMidiEvent> {
let Some(active) = self.active_hw_notes_by_track.remove(track_name) else {
return vec![];
};
let mut channels = std::collections::HashSet::<(String, u8)>::new();
let mut events = Vec::with_capacity(active.len() * 2);
for (device, channel, pitch) in active {
channels.insert((device.clone(), channel));
events.push(HwMidiEvent {
device,
event: MidiEvent::new(0, vec![0x80 | channel.min(15), pitch.min(127), 64]),
});
}
for (device, channel) in channels {
events.push(HwMidiEvent {
device,
event: MidiEvent::new(
0,
vec![0xB0 | channel.min(15), Self::MIDI_CC_SUSTAIN_PEDAL, 0],
),
});
}
events
}
fn set_clip_plugin_graph_json(
&mut self,
track_name: &str,
clip_index: usize,
plugin_graph_json: Option<serde_json::Value>,
) {
if let Some(track) = self.state.lock().tracks.get(track_name) {
let track = track.lock();
if let Some(clip) = track.audio.clips.get_mut(clip_index) {
clip.plugin_graph_json = plugin_graph_json;
}
}
}
fn update_active_hw_notes_for_track(&mut self, track_name: &str, device: &str, data: &[u8]) {
let Some(status) = data.first().copied() else {
return;
};
let channel = status & 0x0F;
match status & 0xF0 {
0x80 => {
if let Some(&pitch) = data.get(1)
&& let Some(active) = self.active_hw_notes_by_track.get_mut(track_name)
{
active.remove(&(device.to_string(), channel, pitch));
if active.is_empty() {
self.active_hw_notes_by_track.remove(track_name);
}
}
}
0x90 => {
let Some(&pitch) = data.get(1) else {
return;
};
let velocity = data.get(2).copied().unwrap_or(0);
if velocity == 0 {
if let Some(active) = self.active_hw_notes_by_track.get_mut(track_name) {
active.remove(&(device.to_string(), channel, pitch));
if active.is_empty() {
self.active_hw_notes_by_track.remove(track_name);
}
}
} else {
self.active_hw_notes_by_track
.entry(track_name.to_string())
.or_default()
.insert((device.to_string(), channel, pitch));
}
}
_ => {}
}
}
fn note_off_events_for_all_active_tracks(&mut self) -> Vec<HwMidiEvent> {
let track_names: Vec<String> = self.active_hw_notes_by_track.keys().cloned().collect();
let mut events = Vec::new();
for track_name in track_names {
events.extend(self.note_off_events_for_track(&track_name));
}
events
}
fn panic_events_for_all_hw_midi_outputs(&self) -> Vec<HwMidiEvent> {
let mut active_channels = std::collections::HashSet::<(String, u8)>::new();
for active in self.active_hw_notes_by_track.values() {
for (device, channel, _pitch) in active {
active_channels.insert((device.clone(), *channel));
}
}
let mut events = Vec::with_capacity(active_channels.len());
for (device, channel) in active_channels {
events.push(HwMidiEvent {
device,
event: MidiEvent::new(0, vec![0xB0 | channel, Self::MIDI_CC_ALL_SOUND_OFF, 0]),
});
}
events
}
fn note_off_events_for_active_snapshot(
&self,
snapshot: &HashMap<String, std::collections::HashSet<(String, u8, u8)>>,
frame: u32,
) -> Vec<HwMidiEvent> {
let mut channels = std::collections::HashSet::<(String, u8)>::new();
let mut events = Vec::new();
for active in snapshot.values() {
for (device, channel, pitch) in active {
channels.insert((device.clone(), *channel));
events.push(HwMidiEvent {
device: device.clone(),
event: MidiEvent::new(
frame,
vec![0x80 | (*channel).min(15), (*pitch).min(127), 64],
),
});
}
}
for (device, channel) in channels {
events.push(HwMidiEvent {
device,
event: MidiEvent::new(
frame,
vec![0xB0 | channel.min(15), Self::MIDI_CC_SUSTAIN_PEDAL, 0],
),
});
}
events
}
fn parse_midi_clip_for_edit(
path: &Path,
sample_rate: f64,
clip_start: usize,
) -> Result<MidiEditParseResult, String> {
let bytes = std::fs::read(path).map_err(|e| e.to_string())?;
let smf = Smf::parse(&bytes).map_err(|e| e.to_string())?;
let Timing::Metrical(ppq) = smf.header.timing else {
return Ok((vec![], vec![], vec![]));
};
let ppq = u64::from(ppq.as_int().max(1));
let mut tempo_changes: Vec<(u64, u32)> = vec![(0, 500_000)];
for track in &smf.tracks {
let mut tick = 0_u64;
for event in track {
tick = tick.saturating_add(event.delta.as_int() as u64);
if let TrackEventKind::Meta(MetaMessage::Tempo(us_per_q)) = event.kind {
tempo_changes.push((tick, us_per_q.as_int()));
}
}
}
tempo_changes.sort_by_key(|(tick, _)| *tick);
let mut normalized_tempos: Vec<(u64, u32)> = Vec::with_capacity(tempo_changes.len());
for (tick, tempo) in tempo_changes {
if let Some(last) = normalized_tempos.last_mut()
&& last.0 == tick
{
last.1 = tempo;
} else {
normalized_tempos.push((tick, tempo));
}
}
let tempo_changes = normalized_tempos;
let ticks_to_samples = |tick: u64| -> usize {
let mut total_us: u128 = 0;
let mut prev_tick = 0_u64;
let mut current_tempo_us = 500_000_u32;
for (change_tick, tempo_us) in &tempo_changes {
if *change_tick > tick {
break;
}
let seg_ticks = change_tick.saturating_sub(prev_tick);
total_us = total_us.saturating_add(
u128::from(seg_ticks).saturating_mul(u128::from(current_tempo_us))
/ u128::from(ppq),
);
prev_tick = *change_tick;
current_tempo_us = *tempo_us;
}
let rem = tick.saturating_sub(prev_tick);
total_us = total_us.saturating_add(
u128::from(rem).saturating_mul(u128::from(current_tempo_us)) / u128::from(ppq),
);
((total_us as f64 / 1_000_000.0) * sample_rate).round() as usize
};
let mut notes = Vec::<MidiNoteData>::new();
let mut controllers = Vec::<MidiControllerData>::new();
let mut passthrough_events = Vec::<(u64, Vec<u8>)>::new();
let mut active_notes: HashMap<(u8, u8), Vec<(u64, u8)>> = HashMap::new();
for track in &smf.tracks {
let mut tick = 0_u64;
for event in track {
tick = tick.saturating_add(event.delta.as_int() as u64);
match event.kind {
TrackEventKind::Midi { channel, message } => {
let channel_u8 = channel.as_int();
match message {
midly::MidiMessage::NoteOn { key, vel } => {
let pitch = key.as_int();
let velocity = vel.as_int();
if velocity == 0 {
if let Some(starts) = active_notes.get_mut(&(channel_u8, pitch))
&& let Some((start_tick, start_vel)) = starts.pop()
{
let start_sample = ticks_to_samples(start_tick);
let end_sample = ticks_to_samples(tick);
notes.push(MidiNoteData {
start_sample,
length_samples: end_sample
.saturating_sub(start_sample)
.max(1),
pitch,
velocity: start_vel,
channel: channel_u8,
});
}
} else {
active_notes
.entry((channel_u8, pitch))
.or_default()
.push((tick, velocity));
}
}
midly::MidiMessage::NoteOff { key, .. } => {
let pitch = key.as_int();
if let Some(starts) = active_notes.get_mut(&(channel_u8, pitch))
&& let Some((start_tick, start_vel)) = starts.pop()
{
let start_sample = ticks_to_samples(start_tick);
let end_sample = ticks_to_samples(tick);
notes.push(MidiNoteData {
start_sample,
length_samples: end_sample
.saturating_sub(start_sample)
.max(1),
pitch,
velocity: start_vel,
channel: channel_u8,
});
}
}
midly::MidiMessage::Controller { controller, value } => {
controllers.push(MidiControllerData {
sample: ticks_to_samples(tick),
controller: controller.as_int(),
value: value.as_int(),
channel: channel_u8,
});
}
_ => {
let mut data = Vec::with_capacity(3);
if (LiveEvent::Midi { channel, message })
.write(&mut data)
.is_ok()
{
passthrough_events.push((ticks_to_samples(tick) as u64, data));
}
}
}
}
TrackEventKind::SysEx(payload) => {
let mut data = Vec::with_capacity(payload.len() + 2);
data.push(0xF0);
data.extend_from_slice(payload);
if data.last().copied() != Some(0xF7) {
data.push(0xF7);
}
passthrough_events.push((ticks_to_samples(tick) as u64, data));
}
TrackEventKind::Escape(payload) => {
let mut data = Vec::with_capacity(payload.len() + 1);
data.push(0xF7);
data.extend_from_slice(payload);
passthrough_events.push((ticks_to_samples(tick) as u64, data));
}
_ => {}
}
}
}
for ((channel, pitch), starts) in active_notes {
for (start_tick, velocity) in starts {
let start_sample = ticks_to_samples(start_tick);
let end_sample = ticks_to_samples(start_tick.saturating_add(ppq / 8));
notes.push(MidiNoteData {
start_sample,
length_samples: end_sample.saturating_sub(start_sample).max(1),
pitch,
velocity,
channel,
});
}
}
notes.sort_by_key(|n| (n.start_sample, n.pitch));
controllers.sort_by_key(|c| (c.sample, c.controller));
passthrough_events.sort_by_key(|(sample, _)| *sample);
let min_sample = notes
.iter()
.map(|n| n.start_sample)
.chain(controllers.iter().map(|c| c.sample))
.chain(passthrough_events.iter().map(|(s, _)| *s as usize))
.min()
.unwrap_or(0);
if min_sample >= clip_start && clip_start > 0 {
for note in &mut notes {
note.start_sample = note.start_sample.saturating_sub(clip_start);
}
for ctrl in &mut controllers {
ctrl.sample = ctrl.sample.saturating_sub(clip_start);
}
for (sample, _) in &mut passthrough_events {
*sample = sample.saturating_sub(clip_start as u64);
}
}
Ok((notes, controllers, passthrough_events))
}
fn midi_events_from_notes_and_controllers(
notes: &[MidiNoteData],
controllers: &[MidiControllerData],
) -> Vec<(u64, Vec<u8>)> {
let mut events: Vec<(u64, u8, Vec<u8>)> = Vec::new();
for note in notes {
let channel = note.channel.min(15);
let pitch = note.pitch.min(127);
let velocity = note.velocity.min(127);
let start = note.start_sample as u64;
let end = note.start_sample.saturating_add(note.length_samples).max(1) as u64;
events.push((start, 2, vec![0x90 | channel, pitch, velocity]));
events.push((end, 0, vec![0x80 | channel, pitch, 64]));
}
for ctrl in controllers {
let channel = ctrl.channel.min(15);
let controller = ctrl.controller.min(127);
let value = ctrl.value.min(127);
events.push((
ctrl.sample as u64,
1,
vec![0xB0 | channel, controller, value],
));
}
events.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
events
.into_iter()
.map(|(sample, _, data)| (sample, data))
.collect()
}
fn is_track_frozen(&self, track_name: &str) -> bool {
self.state
.lock()
.tracks
.get(track_name)
.map(|track| track.lock().frozen())
.unwrap_or(false)
}
async fn reject_if_track_frozen(&mut self, track_name: &str, operation: &str) -> bool {
if self.is_track_frozen(track_name) {
self.notify_clients(Err(format!(
"Track '{track_name}' is frozen; {operation} is blocked"
)))
.await;
true
} else {
false
}
}
fn apply_midi_edit_action(&mut self, action: &Action) -> Result<(), String> {
let (track_name, clip_index) = match action {
Action::ModifyMidiNotes {
track_name,
clip_index,
..
}
| Action::InsertMidiNotes {
track_name,
clip_index,
..
}
| Action::DeleteMidiNotes {
track_name,
clip_index,
..
}
| Action::ModifyMidiControllers {
track_name,
clip_index,
..
}
| Action::InsertMidiControllers {
track_name,
clip_index,
..
}
| Action::DeleteMidiControllers {
track_name,
clip_index,
..
}
| Action::SetMidiSysExEvents {
track_name,
clip_index,
..
} => (track_name, *clip_index),
_ => return Ok(()),
};
let track_handle = self
.state
.lock()
.tracks
.get(track_name)
.cloned()
.ok_or_else(|| format!("Track not found: {track_name}"))?;
let (clip_name, clip_path, sample_rate, clip_start) = {
let track = track_handle.lock();
if clip_index >= track.midi.clips.len() {
return Err(format!(
"Invalid MIDI clip index {clip_index} for '{track_name}'"
));
}
let clip = &track.midi.clips[clip_index];
let clip_name = clip.name.clone();
let clip_path = track.resolve_clip_path(&clip_name);
(clip_name, clip_path, track.sample_rate, clip.start)
};
let (mut notes, mut controllers, mut passthrough_events) =
Self::parse_midi_clip_for_edit(&clip_path, sample_rate, clip_start)?;
match action {
Action::ModifyMidiNotes {
note_indices,
new_notes,
..
} => {
for (idx, new_note) in note_indices.iter().zip(new_notes.iter()) {
if let Some(note) = notes.get_mut(*idx) {
*note = new_note.clone();
}
}
}
Action::DeleteMidiNotes { note_indices, .. } => {
let mut indices = note_indices.clone();
indices.sort_unstable();
indices.dedup();
for idx in indices.into_iter().rev() {
if idx < notes.len() {
notes.remove(idx);
}
}
}
Action::InsertMidiNotes {
notes: inserted, ..
} => {
let mut sorted = inserted.clone();
sorted.sort_unstable_by_key(|(idx, _)| *idx);
for (idx, note) in sorted {
let at = idx.min(notes.len());
notes.insert(at, note);
}
}
Action::ModifyMidiControllers {
controller_indices,
new_controllers,
..
} => {
for (idx, new_ctrl) in controller_indices.iter().zip(new_controllers.iter()) {
if let Some(ctrl) = controllers.get_mut(*idx) {
*ctrl = new_ctrl.clone();
}
}
}
Action::DeleteMidiControllers {
controller_indices, ..
} => {
let mut indices = controller_indices.clone();
indices.sort_unstable();
indices.dedup();
for idx in indices.into_iter().rev() {
if idx < controllers.len() {
controllers.remove(idx);
}
}
}
Action::InsertMidiControllers {
controllers: inserted,
..
} => {
let mut sorted = inserted.clone();
sorted.sort_unstable_by_key(|(idx, _)| *idx);
for (idx, ctrl) in sorted {
let at = idx.min(controllers.len());
controllers.insert(at, ctrl);
}
}
Action::SetMidiSysExEvents {
new_sysex_events, ..
} => {
passthrough_events
.retain(|(_, data)| !matches!(data.first(), Some(0xF0) | Some(0xF7)));
passthrough_events.extend(
new_sysex_events
.iter()
.map(|ev| (ev.sample as u64, ev.data.clone())),
);
}
_ => {}
}
notes.sort_by_key(|n| (n.start_sample, n.pitch));
controllers.sort_by_key(|c| (c.sample, c.controller));
passthrough_events.sort_by_key(|(sample, _)| *sample);
let mut events = Self::midi_events_from_notes_and_controllers(¬es, &controllers);
events.extend(passthrough_events);
events.sort_by_key(|(sample, _)| *sample);
Self::write_midi_file(&clip_path, sample_rate.max(1.0) as u32, &events)?;
track_handle.lock().invalidate_midi_clip_cache(&clip_name);
Ok(())
}
const METER_PUBLISH_INTERVAL: Duration = Duration::from_millis(50);
const TRACK_PROCESS_TIMEOUT: Duration = Duration::from_millis(250);
#[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
const HW_OUT_METER_LINEAR_EPSILON: f32 = 0.0025;
#[cfg(all(unix, not(target_os = "macos")))]
fn session_plugins_dir(&self) -> Option<PathBuf> {
self.session_dir.as_ref().map(|d| d.join("plugins"))
}
fn session_audio_dir(&self) -> Option<PathBuf> {
self.session_dir.as_ref().map(|d| d.join("audio"))
}
fn session_midi_dir(&self) -> Option<PathBuf> {
self.session_dir.as_ref().map(|d| d.join("midi"))
}
fn session_peaks_dir(&self) -> Option<PathBuf> {
self.session_dir.as_ref().map(|d| d.join("peaks"))
}
fn ensure_session_subdirs(&self) {
if let Some(root) = &self.session_dir {
let _ = std::fs::create_dir_all(root.join("plugins"));
let _ = std::fs::create_dir_all(root.join("audio"));
let _ = std::fs::create_dir_all(root.join("midi"));
let _ = std::fs::create_dir_all(root.join("peaks"));
}
}
fn finalize_midi_hw_devices(mut devices: Vec<String>) -> Vec<String> {
devices.sort();
devices.dedup();
devices
}
#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"))]
fn discover_midi_hw_devices_from_dir(path: &str, prefixes: &[&str]) -> Vec<String> {
let devices = read_dir(path)
.map(|rd| {
rd.filter_map(Result::ok)
.map(|e| e.path())
.filter_map(|path| {
let name = path.file_name()?.to_str()?;
prefixes
.iter()
.any(|prefix| name.starts_with(prefix))
.then(|| path.to_string_lossy().into_owned())
})
.collect()
})
.unwrap_or_default();
Self::finalize_midi_hw_devices(devices)
}
fn discover_midi_hw_devices() -> Vec<String> {
#[cfg(target_os = "freebsd")]
let devices = Self::discover_midi_hw_devices_from_dir("/dev", &["umidi", "midi"]);
#[cfg(target_os = "linux")]
let devices = Self::discover_midi_hw_devices_from_dir("/dev/snd", &["midiC"]);
#[cfg(target_os = "openbsd")]
let devices = Self::discover_midi_hw_devices_from_dir("/dev", &["midi"]);
#[cfg(target_os = "windows")]
let devices = {
let mut devices = wasapi::list_midi_input_devices();
devices.extend(wasapi::list_midi_output_devices());
Self::finalize_midi_hw_devices(devices)
};
#[cfg(target_os = "macos")]
let devices = {
let mut devices = Vec::new();
for source in coremidi::Sources {
if let Some(name) = source.display_name() {
devices.push(name);
}
}
for dest in coremidi::Destinations {
if let Some(name) = dest.display_name() {
devices.push(name);
}
}
Self::finalize_midi_hw_devices(devices)
};
devices
}
pub fn new(rx: Receiver<Message>, tx: Sender<Message>) -> Self {
Self {
rx,
tx,
clients: vec![],
state: Arc::new(UnsafeMutex::new(State::default())),
workers: vec![],
hw_driver: None,
#[cfg(unix)]
jack_runtime: None,
midi_hub: Arc::new(UnsafeMutex::new(MidiHub::default())),
hw_worker: None,
osc_server: None,
pending_hw_midi_events: vec![],
pending_hw_midi_events_by_device: HashMap::new(),
pending_hw_midi_out_events: vec![],
pending_hw_midi_out_events_by_device: vec![],
active_hw_notes_by_track: HashMap::new(),
active_hw_notes_cycle_start: HashMap::new(),
midi_hw_in_routes: vec![],
midi_hw_out_routes: vec![],
midi_hw_thru_routes: vec![],
ready_workers: vec![],
pending_requests: VecDeque::new(),
awaiting_hwfinished: false,
handling_hwfinished: false,
track_process_epoch: 0,
transport_panic_flush_pending: false,
transport_restart_pending: false,
notified_loop_wrap_sample: None,
transport_sample: 0,
hw_input_latency_frames: 0,
hw_output_latency_frames: 0,
loop_enabled: false,
loop_range_samples: None,
metronome_enabled: false,
tempo_bpm: 120.0,
tsig_num: 4,
tsig_denom: 4,
punch_enabled: false,
punch_range_samples: None,
audio_recordings: std::collections::HashMap::new(),
midi_recordings: std::collections::HashMap::new(),
completed_audio_recordings: Vec::new(),
completed_midi_recordings: Vec::new(),
playing: false,
clip_playback_enabled: true,
record_enabled: false,
step_recording_enabled: false,
session_dir: None,
hw_out_level_db: 0.0,
hw_out_balance: 0.0,
hw_out_muted: false,
last_hw_out_meter_publish: None,
#[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
last_hw_out_meter_linear: vec![],
hw_out_peak_hold_linear: vec![],
#[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
hw_out_meter_publish_phase: false,
last_track_meter_publish: None,
track_meter_linear_by_track: HashMap::new(),
task_processing_started_at: HashMap::new(),
cycle_tasks: Vec::new(),
cycle_task_deps: HashMap::new(),
cycle_tasks_running: Vec::new(),
cycle_tasks_finished: Vec::new(),
latest_hw_out_meter_db: Arc::new(Vec::new()),
latest_track_meter_snapshot: Arc::new(Vec::new()),
history: History::default(),
history_group: None,
history_suspended: false,
offline_bounce_jobs: HashMap::new(),
pending_midi_learn: None,
pending_global_midi_learn: None,
global_midi_learn_play_pause: None,
global_midi_learn_stop: None,
global_midi_learn_record_toggle: None,
midi_cc_gate: HashMap::new(),
modulators: Vec::new(),
modulator_values: None,
}
}
fn hw_driver_cycle_samples(&self) -> Option<usize> {
self.hw_driver.as_ref().map(|o| o.lock().cycle_samples())
}
#[cfg(unix)]
fn jack_cycle_samples(&self) -> Option<usize> {
self.jack_runtime.as_ref().map(|j| j.lock().buffer_size)
}
#[cfg(not(unix))]
fn jack_cycle_samples(&self) -> Option<usize> {
None
}
fn current_cycle_samples(&self) -> usize {
self.hw_driver_cycle_samples()
.or_else(|| self.jack_cycle_samples())
.unwrap_or(0)
}
fn sample_rate(&self) -> f64 {
if let Some(hw) = &self.hw_driver {
hw.lock().sample_rate() as f64
} else {
#[cfg(unix)]
{
self.jack_runtime
.as_ref()
.map(|j| j.lock().sample_rate as f64)
.unwrap_or(48_000.0)
}
#[cfg(not(unix))]
{
48_000.0
}
}
}
fn compute_modulator_values(
&self,
sample: usize,
) -> Arc<std::collections::HashMap<usize, f32>> {
let sample_rate = self.sample_rate();
let values: std::collections::HashMap<usize, f32> = self
.modulators
.iter()
.filter(|m| m.enabled)
.map(|m| (m.id, m.value_at(sample, sample_rate)))
.collect();
Arc::new(values)
}
fn apply_modulators(&mut self, sample: usize) -> Vec<Action> {
use crate::modulator::ModulatorTarget;
let values = self.compute_modulator_values(sample);
self.modulator_values = Some(values.clone());
let mut echoes = Vec::new();
let mut per_track: HashMap<String, (Option<f32>, Option<f32>)> = HashMap::new();
let mut clap_params: HashMap<(String, usize, u32), f64> = HashMap::new();
let mut vst3_params: HashMap<(String, usize, u32), f32> = HashMap::new();
#[cfg(all(unix, not(target_os = "macos")))]
let mut lv2_params: HashMap<(String, usize, u32), f32> = HashMap::new();
let mut midi_cc_events: HashMap<String, Vec<MidiEvent>> = HashMap::new();
let map_f32 = |value: f32, min: f32, max: f32| -> f32 {
crate::modulator::map_value(value, min, max)
};
let map_f64 = |value: f32, min: f64, max: f64| -> f64 {
crate::modulator::map_value_f64(value, min, max)
};
for m in &self.modulators {
if !m.enabled {
continue;
}
let Some(&value) = values.get(&m.id) else {
continue;
};
for target in &m.targets {
match target {
ModulatorTarget::TrackVolume {
track_name,
min,
max,
} => {
let clamped = map_f32(value, *min, *max);
per_track.entry(track_name.clone()).or_default().0 = Some(clamped);
}
ModulatorTarget::TrackBalance {
track_name,
min,
max,
} => {
let clamped = map_f32(value, *min, *max);
per_track.entry(track_name.clone()).or_default().1 = Some(clamped);
}
ModulatorTarget::HwOutVolume { min, max } => {
let clamped = map_f32(value, *min, *max);
if (self.hw_out_level_db - clamped).abs() > f32::EPSILON {
self.hw_out_level_db = clamped;
echoes
.push(Action::TrackAutomationLevel("hw:out".to_string(), clamped));
}
}
ModulatorTarget::HwOutBalance { min, max } => {
let next = map_f32(value, *min, *max).clamp(-1.0, 1.0);
if (self.hw_out_balance - next).abs() > f32::EPSILON {
self.hw_out_balance = next;
echoes.push(Action::TrackAutomationBalance("hw:out".to_string(), next));
}
}
ModulatorTarget::ClapParameter {
track_name,
instance_id,
param_id,
min,
max,
} => {
let param_value = map_f64(value, *min, *max);
clap_params
.insert((track_name.clone(), *instance_id, *param_id), param_value);
}
ModulatorTarget::Vst3Parameter {
track_name,
instance_id,
param_id,
min,
max,
} => {
let param_value = map_f32(value, *min, *max);
vst3_params
.insert((track_name.clone(), *instance_id, *param_id), param_value);
}
#[cfg(all(unix, not(target_os = "macos")))]
ModulatorTarget::Lv2Parameter {
track_name,
instance_id,
index,
min,
max,
} => {
let param_value = map_f32(value, *min, *max);
lv2_params.insert((track_name.clone(), *instance_id, *index), param_value);
}
ModulatorTarget::MidiCc {
track_name,
channel,
cc,
} => {
let cc_value = (value * 127.0).round() as u8;
midi_cc_events
.entry(track_name.clone())
.or_default()
.push(MidiEvent::new(
0,
vec![0xB0 | (*channel).min(15), (*cc).min(127), cc_value],
));
}
}
}
}
for (track_name, (level, balance)) in per_track {
if let Some(level) = level
&& let Some(track) = self.state.lock().tracks.get(&track_name).cloned()
{
let t = track.lock();
if (t.level() - level).abs() > f32::EPSILON {
t.set_level(level);
echoes.push(Action::TrackAutomationLevel(track_name.clone(), level));
}
}
if let Some(balance) = balance
&& let Some(track) = self.state.lock().tracks.get(&track_name).cloned()
{
let t = track.lock();
let next = balance.clamp(-1.0, 1.0);
if (t.balance - next).abs() > f32::EPSILON {
t.set_balance(next);
echoes.push(Action::TrackAutomationBalance(track_name.clone(), next));
}
}
}
for (track_name, events) in midi_cc_events {
if let Some(track) = self.state.lock().tracks.get(&track_name).cloned() {
track.lock().pending_modulator_midi_events.extend(events);
}
}
let state = self.state.lock();
for ((track_name, instance_id, param_id), value) in clap_params {
if let Some(track) = state.tracks.get(&track_name).cloned()
&& track
.lock()
.set_clap_parameter(instance_id, param_id, value)
.is_ok()
{
echoes.push(Action::TrackSetClapParameter {
track_name,
instance_id,
param_id,
value,
});
}
}
for ((track_name, instance_id, param_id), value) in vst3_params {
if let Some(track) = state.tracks.get(&track_name).cloned()
&& track
.lock()
.set_vst3_parameter(instance_id, param_id, value)
.is_ok()
{
echoes.push(Action::TrackSetVst3Parameter {
track_name,
instance_id,
param_id,
value,
});
}
}
#[cfg(all(unix, not(target_os = "macos")))]
for ((track_name, instance_id, index), value) in lv2_params {
if let Some(track) = state.tracks.get(&track_name).cloned()
&& track
.lock()
.set_lv2_control_value(instance_id, index as usize, f64::from(value))
.is_ok()
{
echoes.push(Action::TrackSetLv2ControlValue {
track_name,
instance_id,
index,
value,
});
}
}
echoes
}
fn session_end_sample(&self) -> usize {
self.state
.lock()
.tracks
.values()
.map(|track| {
let track = track.lock();
let audio_end = track
.audio
.clips
.iter()
.map(|clip| clip.end)
.max()
.unwrap_or(0);
let midi_end = track
.midi
.clips
.iter()
.map(|clip| clip.end)
.max()
.unwrap_or(0);
audio_end.max(midi_end)
})
.max()
.unwrap_or(0)
}
async fn ensure_metronome_track(&mut self) {
if self.state.lock().tracks.contains_key(Self::METRONOME_TRACK) {
return;
}
let (cycle_samples, sample_rate_hz, output_channels): (usize, f64, usize) =
if let Some(hw) = &self.hw_driver {
let hw = hw.lock();
(
hw.cycle_samples(),
hw.sample_rate() as f64,
hw.output_channels(),
)
} else {
#[cfg(unix)]
{
if let Some(jack) = &self.jack_runtime {
let jack = jack.lock();
(
jack.buffer_size,
jack.sample_rate as f64,
jack.audio_outs().len(),
)
} else {
return;
}
}
#[cfg(not(unix))]
{
return;
}
};
if output_channels == 0 {
return;
}
self.state.lock().tracks.insert(
Self::METRONOME_TRACK.to_string(),
Arc::new(UnsafeMutex::new(Box::new(Track::new(
Self::METRONOME_TRACK.to_string(),
0,
1,
0,
0,
cycle_samples.max(1),
sample_rate_hz.max(1.0),
)))),
);
if let Some(track) = self.state.lock().tracks.get(Self::METRONOME_TRACK).cloned() {
track.lock().set_level(Self::METRONOME_DEFAULT_LEVEL_DB);
track.lock().set_metronome_enabled(self.metronome_enabled);
}
self.notify_clients(Ok(Action::AddTrack {
name: Self::METRONOME_TRACK.to_string(),
audio_ins: 0,
midi_ins: 0,
audio_outs: 1,
midi_outs: 0,
folder: false,
}))
.await;
self.notify_clients(Ok(Action::TrackLevel(
Self::METRONOME_TRACK.to_string(),
Self::METRONOME_DEFAULT_LEVEL_DB,
)))
.await;
}
fn open_hw_driver(
device: &str,
_input_device: Option<&str>,
sample_rate_hz: i32,
bits: i32,
hw_opts: HwOptions,
) -> Result<HwDriver, String> {
#[cfg(any(target_os = "windows", target_os = "freebsd", target_os = "linux"))]
{
HwDriver::new_with_options(device, _input_device, sample_rate_hz, bits, hw_opts)
.map_err(|e| e.to_string())
}
#[cfg(target_os = "openbsd")]
{
HwDriver::new_with_options(device, sample_rate_hz, bits, hw_opts)
.map_err(|e| e.to_string())
}
}
fn hw_profile_backend_label(_device: &str) -> &'static str {
#[cfg(target_os = "windows")]
let label = "WASAPI";
#[cfg(target_os = "linux")]
let label = "ALSA";
#[cfg(target_os = "freebsd")]
let label = "OSS";
#[cfg(target_os = "openbsd")]
let label = "sndio";
#[cfg(target_os = "macos")]
let label = "CoreAudio";
label
}
#[cfg(target_os = "freebsd")]
fn maybe_start_freebsd_sync_group(&self) {
if let Some(oss) = &self.hw_driver {
let in_fd = oss.lock().input_fd();
let out_fd = oss.lock().output_fd();
let mut group = 0;
let in_group = hw::add_to_sync_group(in_fd, group, true);
if in_group > 0 {
group = in_group;
}
let out_group = hw::add_to_sync_group(out_fd, group, false);
if out_group > 0 {
group = out_group;
}
let sync_started = if group > 0 {
hw::start_sync_group(in_fd, group).is_ok()
} else {
false
};
if !sync_started {
let _ = oss.lock().start_input_trigger();
let _ = oss.lock().start_output_trigger();
}
}
}
#[cfg(not(target_os = "freebsd"))]
fn maybe_start_freebsd_sync_group(&self) {}
async fn open_discovered_midi_hw_devices(&mut self) {
for device in Self::discover_midi_hw_devices() {
let (opened_in, opened_out) = {
let midi_hub = self.midi_hub.lock();
let opened_in = midi_hub.open_input(&device).is_ok();
let opened_out = midi_hub.open_output(&device).is_ok();
(opened_in, opened_out)
};
if opened_in {
self.notify_clients(Ok(Action::OpenMidiInputDevice(device.clone())))
.await;
}
if opened_out {
self.notify_clients(Ok(Action::OpenMidiOutputDevice(device.clone())))
.await;
}
}
}
#[cfg(unix)]
async fn maybe_open_jack_runtime(&mut self, request: AudioOpenRequest<'_>) -> Option<()> {
if !request.device.eq_ignore_ascii_case("jack") {
return None;
}
match JackRuntime::new(
"maolan",
crate::hw::jack::Config::default(),
self.tx.clone(),
) {
Ok(runtime) => {
let input_channels = runtime.input_channels();
let output_channels = runtime.output_channels();
let midi_inputs = runtime.midi_input_devices();
let midi_outputs = runtime.midi_output_devices();
let rate = runtime.sample_rate;
if let Some(worker) = self.hw_worker.take() {
if let Some(hw) = &self.hw_driver {
hw.lock().request_stop();
}
let _ = worker.tx.send(Message::Request(Action::Quit)).await;
let _ = worker.handle.await;
}
self.hw_driver = None;
self.jack_runtime = Some(Arc::new(UnsafeMutex::new(runtime)));
self.publish_hw_infos(input_channels, output_channels, rate)
.await;
for device in midi_inputs {
self.notify_clients(Ok(Action::OpenMidiInputDevice(device)))
.await;
}
for device in midi_outputs {
self.notify_clients(Ok(Action::OpenMidiOutputDevice(device)))
.await;
}
self.notify_clients(Ok(Action::OpenAudioDevice {
device: request.device.to_string(),
input_device: request.input_device.map(ToOwned::to_owned),
sample_rate_hz: request.sample_rate_hz,
bits: request.bits,
exclusive: request.exclusive,
period_frames: request.period_frames,
nperiods: request.nperiods,
sync_mode: request.sync_mode,
actual_period_frames: request.period_frames,
input_channels,
output_channels,
bytes_per_frame: 0,
}))
.await;
self.awaiting_hwfinished = true;
}
Err(e) => {
error!("Failed to open JACK runtime: {e}");
self.notify_clients(Err(e)).await;
}
}
Some(())
}
fn hw_driver_input_audio_port(&self, from_port: usize) -> Option<Arc<AudioIO>> {
self.hw_driver
.as_ref()
.and_then(|h| h.lock().input_port(from_port))
}
fn hw_driver_output_audio_port(&self, to_port: usize) -> Option<Arc<AudioIO>> {
self.hw_driver
.as_ref()
.and_then(|h| h.lock().output_port(to_port))
}
#[cfg(unix)]
fn jack_input_audio_port(&self, from_port: usize) -> Option<Arc<AudioIO>> {
self.jack_runtime
.as_ref()
.and_then(|j| j.lock().input_audio_port(from_port))
}
#[cfg(not(unix))]
fn jack_input_audio_port(&self, _from_port: usize) -> Option<Arc<AudioIO>> {
None
}
#[cfg(unix)]
fn jack_output_audio_port(&self, to_port: usize) -> Option<Arc<AudioIO>> {
self.jack_runtime
.as_ref()
.and_then(|j| j.lock().output_audio_port(to_port))
}
#[cfg(not(unix))]
fn jack_output_audio_port(&self, _to_port: usize) -> Option<Arc<AudioIO>> {
None
}
fn normalize_transport_sample(&self, sample: usize) -> usize {
if self.loop_enabled
&& let Some((loop_start, loop_end)) = self.loop_range_samples
&& loop_end > loop_start
&& sample >= loop_end
{
let loop_len = loop_end - loop_start;
return loop_start + (sample - loop_start) % loop_len;
}
sample
}
fn scheduled_loop_wrap_for_next_cycle(&self) -> Option<(usize, usize, usize)> {
if !self.playing || !self.loop_enabled {
return None;
}
let (loop_start, loop_end) = self.loop_range_samples?;
if loop_end <= loop_start || self.transport_sample >= loop_end {
return None;
}
let cycle_samples = self.current_cycle_samples();
if cycle_samples == 0 {
return None;
}
let next = self.transport_sample.saturating_add(cycle_samples);
if next < loop_end {
return None;
}
let after_frames = loop_end.saturating_sub(self.transport_sample);
Some((
after_frames,
loop_start,
self.normalize_transport_sample(next),
))
}
#[cfg(unix)]
fn jack_transport_sync_decision(
current_playing: bool,
current_sample: usize,
jack_playing: bool,
normalized_frame: usize,
cycle_samples: usize,
) -> JackTransportSyncDecision {
let play_sync = match (current_playing, jack_playing) {
(false, true) => Some(JackTransportPlaySync::Start),
(true, false) => Some(JackTransportPlaySync::Stop),
_ => None,
};
let position_drift = normalized_frame.abs_diff(current_sample);
let position_changed = normalized_frame != current_sample;
let should_sync_position = position_changed
&& (!jack_playing || play_sync.is_some() || position_drift > cycle_samples.max(1));
JackTransportSyncDecision {
play_sync,
position_sync: should_sync_position.then_some(normalized_frame),
}
}
#[cfg(unix)]
async fn sync_from_jack_transport(&mut self) {
let Some(jack) = self.jack_runtime.clone() else {
return;
};
let Ok((jack_state, jack_frame)) = jack.lock().transport_state_and_frame() else {
return;
};
let jack_playing = matches!(
jack_state,
jack::TransportState::Rolling | jack::TransportState::Starting
);
let normalized_frame = self.normalize_transport_sample(jack_frame);
let decision = Self::jack_transport_sync_decision(
self.playing,
self.transport_sample,
jack_playing,
normalized_frame,
self.current_cycle_samples(),
);
if let Some(play_sync) = decision.play_sync {
self.playing = matches!(play_sync, JackTransportPlaySync::Start);
if matches!(play_sync, JackTransportPlaySync::Start) {
self.transport_restart_pending = false;
self.transport_panic_flush_pending = false;
self.invalidate_track_cycle_state();
self.notify_clients(Ok(Action::Play)).await;
} else {
self.transport_panic_flush_pending = false;
self.transport_restart_pending = false;
let panic_events = self.note_off_events_for_all_active_tracks();
self.pending_hw_midi_out_events_by_device
.extend(panic_events);
self.flush_recordings().await;
self.notify_clients(Ok(Action::Stop)).await;
}
}
if let Some(sample) = decision.position_sync {
self.transport_sample = sample;
self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
.await;
}
}
fn cycle_segments(&self, frames: usize) -> Vec<(usize, usize, usize)> {
if frames == 0 {
return vec![];
}
if !self.loop_enabled {
return vec![(
self.transport_sample,
self.transport_sample.saturating_add(frames),
0,
)];
}
let Some((loop_start, loop_end)) = self.loop_range_samples else {
return vec![(
self.transport_sample,
self.transport_sample.saturating_add(frames),
0,
)];
};
if loop_end <= loop_start {
return vec![(
self.transport_sample,
self.transport_sample.saturating_add(frames),
0,
)];
}
let mut segments = Vec::new();
let mut remaining = frames;
let mut out_offset = 0usize;
let mut current = self.transport_sample;
while remaining > 0 {
let take = loop_end.saturating_sub(current).min(remaining);
if take == 0 {
current = loop_start;
continue;
}
segments.push((current, current.saturating_add(take), out_offset));
out_offset = out_offset.saturating_add(take);
remaining -= take;
current = if remaining > 0 {
loop_start
} else {
current.saturating_add(take)
};
}
segments
}
fn recording_segments_for_cycle(&self, frames: usize) -> Vec<(usize, usize, usize)> {
let segments = self.cycle_segments(frames);
let comp = self.hw_input_latency_frames;
let segments: Vec<_> = if comp > 0 {
segments
.into_iter()
.map(|(start, end, offset)| {
(start.saturating_sub(comp), end.saturating_sub(comp), offset)
})
.collect()
} else {
segments
};
if !self.punch_enabled {
return segments;
}
let Some((punch_start, punch_end)) = self.punch_range_samples else {
return vec![];
};
if punch_end <= punch_start {
return vec![];
}
let mut clipped = Vec::new();
for (segment_start, segment_end, frame_offset) in segments {
let start = segment_start.max(punch_start);
let end = segment_end.min(punch_end);
if end <= start {
continue;
}
let clipped_offset = frame_offset.saturating_add(start.saturating_sub(segment_start));
clipped.push((start, end, clipped_offset));
}
clipped
}
fn hw_device_info<D: HwDevice>(d: &D) -> HwDeviceInfo {
(
d.input_channels(),
d.output_channels(),
d.sample_rate() as usize,
d.latency_ranges(),
)
}
async fn publish_hw_infos(
&mut self,
input_channels: usize,
output_channels: usize,
rate: usize,
) {
self.notify_clients(Ok(Action::HWInfo {
channels: input_channels,
rate,
input: true,
}))
.await;
self.notify_clients(Ok(Action::HWInfo {
channels: output_channels,
rate,
input: false,
}))
.await;
}
#[cfg(unix)]
fn jack_runtime_is_some(&self) -> bool {
self.jack_runtime.is_some()
}
#[cfg(not(unix))]
fn jack_runtime_is_some(&self) -> bool {
false
}
fn can_schedule_hw_cycle(&self) -> bool {
self.playing && (self.hw_worker.is_some() || self.jack_runtime_is_some())
}
async fn ensure_hw_worker_running(&mut self) {
if self.hw_worker.is_some() || self.hw_driver.is_none() {
return;
}
let (tx, rx) = channel::<Message>(32);
let hw = self.hw_driver.clone().unwrap();
let midi_hub = self.midi_hub.clone();
let tx_engine = self.tx.clone();
let handler = tokio::spawn(async move {
let worker = HwWorker::new(hw, midi_hub, rx, tx_engine);
worker.work().await;
});
self.hw_worker = Some(WorkerData::new(tx, handler));
}
fn build_hw_options(
exclusive: bool,
period_frames: usize,
nperiods: usize,
sync_mode: bool,
) -> HwOptions {
HwOptions {
exclusive,
period_frames: period_frames.max(1).next_power_of_two(),
nperiods: nperiods.max(1),
sync_mode,
..Default::default()
}
}
async fn open_non_jack_audio_device(
&mut self,
device: &str,
input_device: Option<&str>,
sample_rate_hz: i32,
bits: i32,
hw_opts: HwOptions,
) -> Result<(), String> {
let hw_profile_enabled = config::env_flag(config::HW_PROFILE_ENV);
let d = Self::open_hw_driver(device, input_device, sample_rate_hz, bits, hw_opts)?;
let (in_channels, out_channels, rate, (in_lat, out_lat)) = Self::hw_device_info(&d);
if hw_profile_enabled {
let label = Self::hw_profile_backend_label(device);
error!(
"{} config: exclusive={}, period={}, nperiods={}, ignore_hwbuf={}, sync_mode={}, in_latency_extra={}, out_latency_extra={}, input_range={:?}, output_range={:?}",
label,
hw_opts.exclusive,
hw_opts.period_frames,
hw_opts.nperiods,
hw_opts.ignore_hwbuf,
hw_opts.sync_mode,
hw_opts.input_latency_frames,
hw_opts.output_latency_frames,
in_lat,
out_lat
);
}
self.hw_input_latency_frames = in_lat.0;
self.hw_output_latency_frames = out_lat.0;
#[cfg(unix)]
{
self.jack_runtime = None;
}
self.hw_driver = Some(Arc::new(UnsafeMutex::new(d)));
self.publish_hw_infos(in_channels, out_channels, rate).await;
Ok(())
}
async fn finalize_open_audio_device(&mut self) {
self.maybe_start_freebsd_sync_group();
if self.metronome_enabled {
self.ensure_metronome_track().await;
}
if self.hw_worker.is_none() && self.hw_driver.is_some() {
self.ensure_hw_worker_running().await;
self.request_hw_cycle().await;
}
self.open_discovered_midi_hw_devices().await;
}
fn hw_input_audio_port(&self, from_port: usize) -> Option<Arc<AudioIO>> {
self.hw_driver_input_audio_port(from_port)
.or_else(|| self.jack_input_audio_port(from_port))
}
fn hw_output_audio_port(&self, to_port: usize) -> Option<Arc<AudioIO>> {
self.hw_driver_output_audio_port(to_port)
.or_else(|| self.jack_output_audio_port(to_port))
}
fn all_hw_output_audio_ports(&self) -> Vec<Arc<AudioIO>> {
if let Some(driver) = &self.hw_driver {
let count = driver.lock().output_channels();
return (0..count)
.filter_map(|idx| self.hw_driver_output_audio_port(idx))
.collect();
}
#[cfg(unix)]
if let Some(jack) = &self.jack_runtime {
return jack.lock().audio_outs();
}
Vec::new()
}
fn all_hw_input_audio_ports(&self) -> Vec<Arc<AudioIO>> {
if let Some(driver) = &self.hw_driver {
let count = driver.lock().input_channels();
return (0..count)
.filter_map(|idx| self.hw_driver_input_audio_port(idx))
.collect();
}
#[cfg(unix)]
if let Some(jack) = &self.jack_runtime {
return jack.lock().audio_ins();
}
Vec::new()
}
#[cfg(unix)]
fn audio_ports_connected(source: &Arc<AudioIO>, target: &Arc<AudioIO>) -> bool {
source
.connections
.lock()
.iter()
.any(|conn| Arc::ptr_eq(conn, target))
}
fn resolve_audio_route_ports(
&self,
from_track: &str,
from_port: usize,
to_track: &str,
to_port: usize,
) -> (Option<Arc<AudioIO>>, Option<Arc<AudioIO>>) {
let state = self.state.lock();
let from_is_child_of_to = state
.tracks
.get(from_track)
.and_then(|t| t.lock().parent_track.as_deref())
== Some(to_track);
let to_is_child_of_from = state
.tracks
.get(to_track)
.and_then(|t| t.lock().parent_track.as_deref())
== Some(from_track);
let from_audio_io = if from_track == "hw:in" {
self.hw_input_audio_port(from_port)
} else {
state.tracks.get(from_track).and_then(|t| {
let t = t.lock();
if t.is_folder {
if to_is_child_of_from {
t.audio.ins.get(from_port).cloned()
} else {
t.audio.outs.get(from_port).cloned()
}
} else {
t.audio.outs.get(from_port).cloned()
}
})
};
let to_audio_io = if to_track == "hw:out" {
self.hw_output_audio_port(to_port)
} else {
state.tracks.get(to_track).and_then(|t| {
let t = t.lock();
if t.is_folder {
if from_is_child_of_to {
t.audio.outs.get(to_port).cloned()
} else {
t.audio.ins.get(to_port).cloned()
}
} else {
t.audio.ins.get(to_port).cloned()
}
})
};
(from_audio_io, to_audio_io)
}
async fn disconnect_audio_route_and_notify(&mut self, action: Action) -> Result<(), String> {
let Action::Disconnect {
from_track,
from_port,
to_track,
to_port,
kind,
} = &action
else {
return Err("disconnect_audio_route_and_notify requires Disconnect action".to_string());
};
if *kind != Kind::Audio {
return Err("disconnect_audio_route_and_notify only supports audio routes".to_string());
}
let (from_audio_io, to_audio_io) =
self.resolve_audio_route_ports(from_track, *from_port, to_track, *to_port);
match (from_audio_io, to_audio_io) {
(Some(source), Some(target)) => {
crate::audio::io::AudioIO::disconnect(&source, &target)
.map_err(|e| format!("Disconnect failed: {e}"))?;
self.notify_clients(Ok(action)).await;
Ok(())
}
_ => Err(format!(
"Disconnect failed: Port not found ({} -> {})",
from_track, to_track
)),
}
}
#[cfg(unix)]
fn disconnect_actions_for_removed_hw_input(
&self,
removed_port: usize,
removed_io: &Arc<AudioIO>,
) -> Vec<Action> {
let mut actions = Vec::new();
{
let state = self.state.lock();
for (track_name, track) in &state.tracks {
let track = track.lock();
for (to_port, target) in track.audio.ins.iter().enumerate() {
if Self::audio_ports_connected(removed_io, target) {
actions.push(Action::Disconnect {
from_track: "hw:in".to_string(),
from_port: removed_port,
to_track: track_name.clone(),
to_port,
kind: Kind::Audio,
});
}
}
}
}
for (to_port, target) in self.all_hw_output_audio_ports().into_iter().enumerate() {
if Self::audio_ports_connected(removed_io, &target) {
actions.push(Action::Disconnect {
from_track: "hw:in".to_string(),
from_port: removed_port,
to_track: "hw:out".to_string(),
to_port,
kind: Kind::Audio,
});
}
}
actions
}
#[cfg(unix)]
fn disconnect_actions_for_removed_hw_output(
&self,
removed_port: usize,
removed_io: &Arc<AudioIO>,
) -> Vec<Action> {
let mut actions = Vec::new();
{
let state = self.state.lock();
for (track_name, track) in &state.tracks {
let track = track.lock();
for (from_port, source) in track.audio.outs.iter().enumerate() {
if Self::audio_ports_connected(source, removed_io) {
actions.push(Action::Disconnect {
from_track: track_name.clone(),
from_port,
to_track: "hw:out".to_string(),
to_port: removed_port,
kind: Kind::Audio,
});
}
}
}
}
#[cfg(unix)]
if let Some(jack) = &self.jack_runtime {
for (from_port, source) in jack.lock().audio_ins().into_iter().enumerate() {
if Self::audio_ports_connected(&source, removed_io) {
actions.push(Action::Disconnect {
from_track: "hw:in".to_string(),
from_port,
to_track: "hw:out".to_string(),
to_port: removed_port,
kind: Kind::Audio,
});
}
}
}
actions
}
#[cfg(unix)]
fn reindex_notifications_for_removed_hw_input(&self, removed_port: usize) -> Vec<Action> {
let mut actions = Vec::new();
#[cfg(unix)]
if let Some(jack) = &self.jack_runtime {
let jack = jack.lock();
for from_port in (removed_port + 1)..jack.input_channels() {
let Some(source) = jack.input_audio_port(from_port) else {
continue;
};
{
let state = self.state.lock();
for (track_name, track) in &state.tracks {
let track = track.lock();
for (to_port, target) in track.audio.ins.iter().enumerate() {
if Self::audio_ports_connected(&source, target) {
actions.push(Action::Disconnect {
from_track: "hw:in".to_string(),
from_port,
to_track: track_name.clone(),
to_port,
kind: Kind::Audio,
});
actions.push(Action::Connect {
from_track: "hw:in".to_string(),
from_port: from_port - 1,
to_track: track_name.clone(),
to_port,
kind: Kind::Audio,
});
}
}
}
}
for (to_port, target) in self.all_hw_output_audio_ports().into_iter().enumerate() {
if Self::audio_ports_connected(&source, &target) {
actions.push(Action::Disconnect {
from_track: "hw:in".to_string(),
from_port,
to_track: "hw:out".to_string(),
to_port,
kind: Kind::Audio,
});
actions.push(Action::Connect {
from_track: "hw:in".to_string(),
from_port: from_port - 1,
to_track: "hw:out".to_string(),
to_port,
kind: Kind::Audio,
});
}
}
}
}
actions
}
#[cfg(unix)]
fn reindex_notifications_for_removed_hw_output(&self, removed_port: usize) -> Vec<Action> {
let mut actions = Vec::new();
#[cfg(unix)]
if let Some(jack) = &self.jack_runtime {
let jack = jack.lock();
for to_port in (removed_port + 1)..jack.output_channels() {
let Some(target) = jack.output_audio_port(to_port) else {
continue;
};
{
let state = self.state.lock();
for (track_name, track) in &state.tracks {
let track = track.lock();
for (from_port, source) in track.audio.outs.iter().enumerate() {
if Self::audio_ports_connected(source, &target) {
actions.push(Action::Disconnect {
from_track: track_name.clone(),
from_port,
to_track: "hw:out".to_string(),
to_port,
kind: Kind::Audio,
});
actions.push(Action::Connect {
from_track: track_name.clone(),
from_port,
to_track: "hw:out".to_string(),
to_port: to_port - 1,
kind: Kind::Audio,
});
}
}
}
}
for (from_port, source) in jack.audio_ins().into_iter().enumerate() {
if Self::audio_ports_connected(&source, &target) {
actions.push(Action::Disconnect {
from_track: "hw:in".to_string(),
from_port,
to_track: "hw:out".to_string(),
to_port,
kind: Kind::Audio,
});
actions.push(Action::Connect {
from_track: "hw:in".to_string(),
from_port,
to_track: "hw:out".to_string(),
to_port: to_port - 1,
kind: Kind::Audio,
});
}
}
}
}
actions
}
fn midi_hw_in_device(track: &str) -> Option<&str> {
track.strip_prefix("midi:hw:in:")
}
fn midi_hw_out_device(track: &str) -> Option<&str> {
track.strip_prefix("midi:hw:out:")
}
fn midi_binding_matches(
a: &crate::message::MidiLearnBinding,
b: &crate::message::MidiLearnBinding,
) -> bool {
if a.channel != b.channel || a.cc != b.cc {
return false;
}
match (&a.device, &b.device) {
(Some(ad), Some(bd)) => ad == bd,
_ => true,
}
}
fn midi_learn_slot_conflicts(
&self,
binding: &crate::message::MidiLearnBinding,
ignore: Option<MidiLearnSlot>,
) -> Vec<String> {
let mut conflicts = Vec::<String>::new();
let state = self.state.lock();
let mut push_conflict = |slot: MidiLearnSlot, label: String| {
if ignore.as_ref().is_some_and(|i| i == &slot) {
return;
}
conflicts.push(label);
};
let check_global =
|current: &Option<crate::message::MidiLearnBinding>,
target: crate::message::GlobalMidiLearnTarget,
label: &str,
push_conflict: &mut dyn FnMut(MidiLearnSlot, String)| {
if let Some(existing) = current
&& Self::midi_binding_matches(binding, existing)
{
push_conflict(MidiLearnSlot::Global(target), format!("Global {label}"));
}
};
check_global(
&self.global_midi_learn_play_pause,
crate::message::GlobalMidiLearnTarget::PlayPause,
"PlayPause",
&mut push_conflict,
);
check_global(
&self.global_midi_learn_stop,
crate::message::GlobalMidiLearnTarget::Stop,
"Stop",
&mut push_conflict,
);
check_global(
&self.global_midi_learn_record_toggle,
crate::message::GlobalMidiLearnTarget::RecordToggle,
"RecordToggle",
&mut push_conflict,
);
for (track_name, track) in state.tracks.iter() {
let t = track.lock();
let mut check_track = |current: &Option<crate::message::MidiLearnBinding>,
target: crate::message::TrackMidiLearnTarget,
label: &str| {
if let Some(existing) = current
&& Self::midi_binding_matches(binding, existing)
{
push_conflict(
MidiLearnSlot::Track(track_name.clone(), target),
format!("{track_name} {label}"),
);
}
};
check_track(
&t.midi_learn_volume,
crate::message::TrackMidiLearnTarget::Volume,
"Volume",
);
check_track(
&t.midi_learn_balance,
crate::message::TrackMidiLearnTarget::Balance,
"Balance",
);
check_track(
&t.midi_learn_mute,
crate::message::TrackMidiLearnTarget::Mute,
"Mute",
);
check_track(
&t.midi_learn_solo,
crate::message::TrackMidiLearnTarget::Solo,
"Solo",
);
check_track(
&t.midi_learn_arm,
crate::message::TrackMidiLearnTarget::Arm,
"Arm",
);
check_track(
&t.midi_learn_input_monitor,
crate::message::TrackMidiLearnTarget::InputMonitor,
"InputMonitor",
);
check_track(
&t.midi_learn_disk_monitor,
crate::message::TrackMidiLearnTarget::DiskMonitor,
"DiskMonitor",
);
}
conflicts
}
async fn handle_incoming_hw_cc(&mut self, device: &str, channel: u8, cc: u8, value: u8) {
let gate_key = (device.to_string(), channel, cc);
let high = value >= 64;
let prev_high = self.midi_cc_gate.get(&gate_key).copied().unwrap_or(false);
self.midi_cc_gate.insert(gate_key, high);
let rising = high && !prev_high;
if let Some((track_name, target, armed_device)) = self.pending_midi_learn.clone() {
let binding = crate::message::MidiLearnBinding {
device: armed_device.or(Some(device.to_string())),
channel,
cc,
};
let conflicts = self.midi_learn_slot_conflicts(
&binding,
Some(MidiLearnSlot::Track(track_name.clone(), target)),
);
if !conflicts.is_empty() {
self.pending_midi_learn = None;
self.notify_clients(Err(format!(
"MIDI learn conflict for '{}' {:?}: {}",
track_name,
target,
conflicts.join(", ")
)))
.await;
return;
}
if let Some(track) = self.state.lock().tracks.get(&track_name) {
match target {
crate::message::TrackMidiLearnTarget::Volume => {
track.lock().midi_learn_volume = Some(binding.clone());
}
crate::message::TrackMidiLearnTarget::Balance => {
track.lock().midi_learn_balance = Some(binding.clone());
}
crate::message::TrackMidiLearnTarget::Mute => {
track.lock().midi_learn_mute = Some(binding.clone());
}
crate::message::TrackMidiLearnTarget::Solo => {
track.lock().midi_learn_solo = Some(binding.clone());
}
crate::message::TrackMidiLearnTarget::Arm => {
track.lock().midi_learn_arm = Some(binding.clone());
}
crate::message::TrackMidiLearnTarget::InputMonitor => {
track.lock().midi_learn_input_monitor = Some(binding.clone());
}
crate::message::TrackMidiLearnTarget::DiskMonitor => {
track.lock().midi_learn_disk_monitor = Some(binding.clone());
}
}
self.pending_midi_learn = None;
self.notify_clients(Ok(Action::TrackSetMidiLearnBinding {
track_name: track_name.clone(),
target,
binding: Some(binding),
}))
.await;
} else {
self.pending_midi_learn = None;
}
}
if let Some(target) = self.pending_global_midi_learn.take() {
let binding = crate::message::MidiLearnBinding {
device: Some(device.to_string()),
channel,
cc,
};
let conflicts =
self.midi_learn_slot_conflicts(&binding, Some(MidiLearnSlot::Global(target)));
if !conflicts.is_empty() {
self.notify_clients(Err(format!(
"Global MIDI learn conflict for {:?}: {}",
target,
conflicts.join(", ")
)))
.await;
return;
}
match target {
crate::message::GlobalMidiLearnTarget::PlayPause => {
self.global_midi_learn_play_pause = Some(binding.clone());
}
crate::message::GlobalMidiLearnTarget::Stop => {
self.global_midi_learn_stop = Some(binding.clone());
}
crate::message::GlobalMidiLearnTarget::RecordToggle => {
self.global_midi_learn_record_toggle = Some(binding.clone());
}
}
self.notify_clients(Ok(Action::SetGlobalMidiLearnBinding {
target,
binding: Some(binding),
}))
.await;
}
let mut mapped_actions = Vec::<Action>::new();
for (track_name, track) in self.state.lock().tracks.iter() {
let t = track.lock();
if let Some(binding) = t.midi_learn_volume.as_ref() {
let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
if device_matches && binding.channel == channel && binding.cc == cc {
let level = -90.0 + (value as f32 / 127.0) * 110.0;
mapped_actions.push(Action::TrackLevel(track_name.clone(), level));
}
}
if let Some(binding) = t.midi_learn_balance.as_ref() {
let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
if device_matches && binding.channel == channel && binding.cc == cc {
let balance = (value as f32 / 127.0) * 2.0 - 1.0;
mapped_actions.push(Action::TrackBalance(track_name.clone(), balance));
}
}
if let Some(binding) = t.midi_learn_mute.as_ref() {
let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
if device_matches && binding.channel == channel && binding.cc == cc {
let wanted = value >= 64;
if t.muted != wanted {
mapped_actions.push(Action::TrackToggleMute(track_name.clone()));
}
}
}
if let Some(binding) = t.midi_learn_solo.as_ref() {
let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
if device_matches && binding.channel == channel && binding.cc == cc {
let wanted = value >= 64;
if t.soloed != wanted {
mapped_actions.push(Action::TrackToggleSolo(track_name.clone()));
}
}
}
if let Some(binding) = t.midi_learn_arm.as_ref() {
let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
if device_matches && binding.channel == channel && binding.cc == cc {
let wanted = value >= 64;
if t.armed != wanted {
mapped_actions.push(Action::TrackToggleArm(track_name.clone()));
}
}
}
if let Some(binding) = t.midi_learn_input_monitor.as_ref() {
let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
if device_matches && binding.channel == channel && binding.cc == cc {
let wanted = value >= 64;
if t.input_monitor.first() != Some(&wanted) {
mapped_actions.push(Action::TrackToggleInputMonitor {
track_name: track_name.clone(),
lane: 0,
});
}
}
}
if let Some(binding) = t.midi_learn_disk_monitor.as_ref() {
let device_matches = binding.device.as_ref().is_none_or(|d| d.as_str() == device);
if device_matches && binding.channel == channel && binding.cc == cc {
let wanted = value >= 64;
if t.disk_monitor.first() != Some(&wanted) {
mapped_actions.push(Action::TrackToggleDiskMonitor {
track_name: track_name.clone(),
lane: 0,
});
}
}
}
}
let device_matches =
|binding: &crate::message::MidiLearnBinding| binding.device.as_deref() == Some(device);
let mut mapped_global_actions = Vec::<Action>::new();
if let Some(binding) = self.global_midi_learn_play_pause.as_ref()
&& device_matches(binding)
&& binding.channel == channel
&& binding.cc == cc
&& rising
{
mapped_global_actions.push(if self.playing {
Action::Stop
} else {
Action::Play
});
}
if let Some(binding) = self.global_midi_learn_stop.as_ref()
&& device_matches(binding)
&& binding.channel == channel
&& binding.cc == cc
&& rising
&& self.playing
{
mapped_global_actions.push(Action::Stop);
}
if let Some(binding) = self.global_midi_learn_record_toggle.as_ref()
&& device_matches(binding)
&& binding.channel == channel
&& binding.cc == cc
&& rising
{
mapped_global_actions.push(Action::SetRecordEnabled(!self.record_enabled));
}
for action in mapped_actions {
match action {
Action::TrackLevel(ref track_name, level) => {
if let Some(track) = self.state.lock().tracks.get(track_name) {
track.lock().set_level(level);
self.notify_clients(Ok(Action::TrackLevel(track_name.clone(), level)))
.await;
}
}
Action::TrackBalance(ref track_name, balance) => {
if let Some(track) = self.state.lock().tracks.get(track_name) {
track.lock().set_balance(balance);
self.notify_clients(Ok(Action::TrackBalance(track_name.clone(), balance)))
.await;
}
}
Action::TrackToggleMute(ref track_name) => {
if let Some(track) = self.state.lock().tracks.get(track_name) {
track.lock().mute();
self.notify_clients(Ok(Action::TrackToggleMute(track_name.clone())))
.await;
}
}
Action::TrackTogglePhase(ref track_name) => {
if let Some(track) = self.state.lock().tracks.get(track_name) {
track.lock().invert_phase();
self.notify_clients(Ok(Action::TrackTogglePhase(track_name.clone())))
.await;
}
}
Action::TrackToggleSolo(ref track_name) => {
if let Some(track) = self.state.lock().tracks.get(track_name) {
track.lock().solo();
self.notify_clients(Ok(Action::TrackToggleSolo(track_name.clone())))
.await;
}
}
Action::TrackToggleMaster(ref track_name) => {
if let Some(track) = self.state.lock().tracks.get(track_name) {
track.lock().toggle_master();
self.notify_clients(Ok(Action::TrackToggleMaster(track_name.clone())))
.await;
}
}
Action::TrackToggleArm(ref track_name) => {
if let Some(track) = self.state.lock().tracks.get(track_name) {
track.lock().arm();
self.notify_clients(Ok(Action::TrackToggleArm(track_name.clone())))
.await;
}
}
Action::TrackToggleInputMonitor {
ref track_name,
lane,
} => {
if let Some(track) = self.state.lock().tracks.get(track_name) {
track.lock().toggle_input_monitor(lane);
self.notify_clients(Ok(Action::TrackToggleInputMonitor {
track_name: track_name.clone(),
lane,
}))
.await;
}
}
Action::TrackToggleDiskMonitor {
ref track_name,
lane,
} => {
if let Some(track) = self.state.lock().tracks.get(track_name) {
track.lock().toggle_disk_monitor(lane);
self.notify_clients(Ok(Action::TrackToggleDiskMonitor {
track_name: track_name.clone(),
lane,
}))
.await;
}
}
Action::TrackToggleMidiInputMonitor {
ref track_name,
lane,
} => {
if let Some(track) = self.state.lock().tracks.get(track_name) {
track.lock().toggle_midi_input_monitor(lane);
self.notify_clients(Ok(Action::TrackToggleMidiInputMonitor {
track_name: track_name.clone(),
lane,
}))
.await;
}
}
Action::TrackToggleMidiDiskMonitor {
ref track_name,
lane,
} => {
if let Some(track) = self.state.lock().tracks.get(track_name) {
track.lock().toggle_midi_disk_monitor(lane);
self.notify_clients(Ok(Action::TrackToggleMidiDiskMonitor {
track_name: track_name.clone(),
lane,
}))
.await;
}
}
_ => {}
}
}
for action in mapped_global_actions {
self.handle_request_inner(action, false).await;
}
}
fn upstream_audio_track_names(
&self,
seeds: &std::collections::HashSet<String>,
) -> std::collections::HashSet<String> {
let state = self.state.lock();
let mut output_to_track: std::collections::HashMap<
*const crate::audio::io::AudioIO,
String,
> = std::collections::HashMap::new();
for (name, track) in &state.tracks {
let t = track.lock();
for out in &t.audio.outs {
output_to_track.insert(std::sync::Arc::as_ptr(out), name.clone());
}
}
let mut upstream = std::collections::HashSet::new();
let mut to_process: Vec<String> = seeds.iter().cloned().collect();
let mut processed = std::collections::HashSet::new();
while let Some(target_name) = to_process.pop() {
if !processed.insert(target_name.clone()) {
continue;
}
if let Some(target_track) = state.tracks.get(&target_name) {
let tt = target_track.lock();
for input in &tt.audio.ins {
for conn in input.connections.lock().iter() {
let conn_ptr = std::sync::Arc::as_ptr(conn);
if let Some(source_name) = output_to_track.get(&conn_ptr)
&& source_name != &target_name
&& !seeds.contains(source_name)
{
upstream.insert(source_name.clone());
to_process.push(source_name.clone());
}
}
}
}
}
upstream
}
fn is_track_in_soloed_folder(
&self,
track: &Track,
tracks: &std::collections::HashMap<String, Arc<UnsafeMutex<Box<Track>>>>,
) -> bool {
let mut current = track.parent_track.as_deref();
while let Some(parent_name) = current {
if let Some(parent) = tracks.get(parent_name) {
let p = parent.lock();
if p.soloed {
return true;
}
current = p.parent_track.as_deref();
} else {
break;
}
}
false
}
fn folder_has_soloed_descendant(
&self,
folder_name: &str,
tracks: &std::collections::HashMap<String, Arc<UnsafeMutex<Box<Track>>>>,
) -> bool {
for track in tracks.values() {
let t = track.lock();
if !t.soloed {
continue;
}
let mut current = t.parent_track.as_deref();
while let Some(parent_name) = current {
if parent_name == folder_name {
return true;
}
if let Some(parent) = tracks.get(parent_name) {
current = parent.lock().parent_track.as_deref();
} else {
break;
}
}
}
false
}
fn refresh_realtime_infection(&self) {
let state = self.state.lock();
let live_seeds: std::collections::HashSet<String> = state
.tracks
.iter()
.filter_map(|(name, track)| {
let t = track.lock();
if t.armed && t.input_monitor.iter().any(|&m| m) {
Some(name.clone())
} else {
None
}
})
.collect();
let mut output_owner: std::collections::HashMap<*const crate::audio::io::AudioIO, String> =
std::collections::HashMap::new();
for (name, track) in state.tracks.iter() {
let t = track.lock();
for out in &t.audio.outs {
output_owner.insert(std::sync::Arc::as_ptr(out), name.clone());
}
}
let mut infected = live_seeds.clone();
let mut mixed_nodes = std::collections::HashSet::new();
loop {
let mut changed = false;
for (name, track) in state.tracks.iter() {
let t = track.lock();
let mut upstream_owners = std::collections::HashSet::new();
for input in &t.audio.ins {
for conn in input.connections.lock().iter() {
if let Some(owner) = output_owner.get(&std::sync::Arc::as_ptr(conn)) {
upstream_owners.insert(owner.clone());
}
}
}
if upstream_owners.is_empty() {
continue;
}
let has_realtime = upstream_owners
.iter()
.any(|owner| infected.contains(owner) || live_seeds.contains(owner));
let has_playback = upstream_owners
.iter()
.any(|owner| !infected.contains(owner) && !live_seeds.contains(owner));
if has_realtime && has_playback {
mixed_nodes.insert(name.clone());
}
if has_realtime && infected.insert(name.clone()) {
changed = true;
}
}
if !changed {
break;
}
}
for (name, track) in state.tracks.iter() {
let forced = infected.contains(name) && !live_seeds.contains(name);
let t = track.lock();
t.set_shared_realtime_mixed(mixed_nodes.contains(name));
t.set_force_realtime_domain(forced);
}
}
fn apply_mute_solo_policy(&mut self) {
let mut newly_disabled_tracks = Vec::new();
{
let tracks = &self.state.lock().tracks;
let soloed: std::collections::HashSet<String> = tracks
.iter()
.filter_map(|(name, t)| {
if t.lock().soloed {
Some(name.clone())
} else {
None
}
})
.collect();
let any_soloed = !soloed.is_empty();
let upstream = if any_soloed {
self.upstream_audio_track_names(&soloed)
} else {
std::collections::HashSet::new()
};
for track in tracks.values() {
let t = track.lock();
let was_enabled = t.output_enabled;
let in_soloed_folder = self.is_track_in_soloed_folder(t, tracks);
let folder_with_soloed_child =
t.is_folder && self.folder_has_soloed_descendant(&t.name, tracks);
let enabled = if t.is_master {
!t.muted
} else if any_soloed {
(t.soloed
|| upstream.contains(&t.name)
|| in_soloed_folder
|| folder_with_soloed_child)
&& !t.muted
} else {
!t.muted
};
t.set_output_enabled(enabled);
if was_enabled && !enabled {
newly_disabled_tracks.push(t.name.clone());
}
}
}
let mut note_off_events = Vec::new();
for track_name in newly_disabled_tracks {
note_off_events.extend(self.note_off_events_for_track(&track_name));
}
if !note_off_events.is_empty() {
self.pending_hw_midi_out_events_by_device
.extend(note_off_events);
}
}
fn sanitize_file_stem(name: &str) -> String {
let mut out = String::with_capacity(name.len());
for c in name.chars() {
if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
out.push(c);
} else {
out.push('_');
}
}
if out.is_empty() {
"track".to_string()
} else {
out
}
}
fn next_recording_file_name(track_name: &str) -> String {
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
format!("{}_{}.wav", Self::sanitize_file_stem(track_name), ts)
}
fn next_midi_recording_file_name(track_name: &str) -> String {
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
format!("{}_{}.mid", Self::sanitize_file_stem(track_name), ts)
}
fn append_recorded_cycle(&mut self) {
if !self.playing || !self.record_enabled {
return;
}
for (name, track_handle) in &self.state.lock().tracks {
let track = track_handle.lock();
if !track.armed {
continue;
}
let audio_channels = track.record_tap_outs.len();
let audio_frames = track
.record_tap_outs
.first()
.map(|ch| ch.len())
.unwrap_or(0);
let frames = audio_frames.max(self.current_cycle_samples());
if frames == 0 {
continue;
}
let segments = self.recording_segments_for_cycle(frames);
for (segment_start, segment_end, frame_offset) in segments {
let segment_len = segment_end.saturating_sub(segment_start);
if segment_len == 0 {
continue;
}
if audio_channels > 0 && audio_frames > 0 {
let audio_entry =
self.audio_recordings
.entry(name.clone())
.or_insert_with(|| RecordingSession {
start_sample: segment_start,
samples: Vec::with_capacity(segment_len * audio_channels * 2),
channels: audio_channels,
file_name: Self::next_recording_file_name(name),
stripe_peaks: vec![Vec::new(); audio_channels],
current_stripe_frames: 0,
});
if audio_entry.channels != audio_channels {
continue;
}
if let Some(entry) = self.audio_recordings.get_mut(name.as_str()) {
let from = frame_offset.min(audio_frames);
let to = frame_offset.saturating_add(segment_len).min(audio_frames);
for frame in from..to {
let is_new_stripe =
entry.current_stripe_frames % RECORDING_STRIPE_FRAMES == 0;
for ch in 0..audio_channels {
let sample = track.record_tap_outs[ch][frame].clamp(-1.0, 1.0);
if is_new_stripe {
entry.stripe_peaks[ch].push([sample, sample]);
} else {
let idx = entry.stripe_peaks[ch].len() - 1;
entry.stripe_peaks[ch][idx][0] =
entry.stripe_peaks[ch][idx][0].min(sample);
entry.stripe_peaks[ch][idx][1] =
entry.stripe_peaks[ch][idx][1].max(sample);
}
entry.samples.push(track.record_tap_outs[ch][frame]);
}
entry.current_stripe_frames += 1;
}
}
}
let entry = self.midi_recordings.entry(name.clone()).or_insert_with(|| {
MidiRecordingSession {
start_sample: segment_start,
events: Vec::new(),
file_name: Self::next_midi_recording_file_name(name),
}
});
let from = frame_offset;
let to = frame_offset.saturating_add(segment_len);
for event in &track.record_tap_midi_in {
let frame = event.frame as usize;
if frame < from || frame >= to {
continue;
}
let abs_sample = segment_start as u64 + (frame - from) as u64;
entry.events.push((abs_sample, event.data.clone()));
}
if self.punch_enabled
&& let Some((_, punch_end)) = self.punch_range_samples
&& segment_end == punch_end
{
if let Some(done) = self.audio_recordings.remove(name.as_str()) {
self.completed_audio_recordings.push((name.clone(), done));
}
if let Some(done) = self.midi_recordings.remove(name.as_str()) {
self.completed_midi_recordings.push((name.clone(), done));
}
} else if self.loop_enabled
&& let Some((_, loop_end)) = self.loop_range_samples
&& segment_end == loop_end
{
if let Some(done) = self.audio_recordings.remove(name.as_str()) {
self.completed_audio_recordings.push((name.clone(), done));
}
if let Some(done) = self.midi_recordings.remove(name.as_str()) {
self.completed_midi_recordings.push((name.clone(), done));
}
}
}
}
}
async fn flush_completed_recordings(&mut self) {
if self.completed_audio_recordings.is_empty() && self.completed_midi_recordings.is_empty() {
return;
}
let Some(audio_dir) = self.session_audio_dir() else {
self.completed_audio_recordings.clear();
self.completed_midi_recordings.clear();
return;
};
let Some(midi_dir) = self.session_midi_dir() else {
self.completed_audio_recordings.clear();
self.completed_midi_recordings.clear();
return;
};
if std::fs::create_dir_all(&audio_dir).is_err()
|| std::fs::create_dir_all(&midi_dir).is_err()
{
self.completed_audio_recordings.clear();
self.completed_midi_recordings.clear();
return;
}
let rate = self
.hw_driver
.as_ref()
.map(|o| o.lock().sample_rate())
.unwrap_or(48_000);
let completed_audio = std::mem::take(&mut self.completed_audio_recordings);
for (track_name, rec) in completed_audio {
self.flush_recording_entry(&audio_dir, rate, track_name, rec)
.await;
}
let completed_midi = std::mem::take(&mut self.completed_midi_recordings);
for (track_name, rec) in completed_midi {
self.flush_midi_recording_entry(&midi_dir, rate as u32, track_name, rec)
.await;
}
}
async fn flush_recordings(&mut self) {
let Some(audio_dir) = self.session_audio_dir() else {
if !self.audio_recordings.is_empty()
|| !self.midi_recordings.is_empty()
|| !self.completed_audio_recordings.is_empty()
|| !self.completed_midi_recordings.is_empty()
{
self.notify_clients(Err("Recording stopped: session path is not set".to_string()))
.await;
}
self.audio_recordings.clear();
self.midi_recordings.clear();
self.completed_audio_recordings.clear();
self.completed_midi_recordings.clear();
return;
};
if std::fs::create_dir_all(&audio_dir).is_err() {
self.notify_clients(Err(format!(
"Recording stopped: failed to create audio directory {}",
audio_dir.display()
)))
.await;
self.audio_recordings.clear();
self.midi_recordings.clear();
self.completed_audio_recordings.clear();
self.completed_midi_recordings.clear();
return;
}
let Some(midi_dir) = self.session_midi_dir() else {
self.audio_recordings.clear();
self.midi_recordings.clear();
self.completed_audio_recordings.clear();
self.completed_midi_recordings.clear();
return;
};
if std::fs::create_dir_all(&midi_dir).is_err() {
self.audio_recordings.clear();
self.midi_recordings.clear();
self.completed_audio_recordings.clear();
self.completed_midi_recordings.clear();
return;
}
let rate = self
.hw_driver
.as_ref()
.map(|o| o.lock().sample_rate())
.unwrap_or(48_000);
let completed_audio = std::mem::take(&mut self.completed_audio_recordings);
for (track_name, rec) in completed_audio {
self.flush_recording_entry(&audio_dir, rate, track_name, rec)
.await;
}
let completed_midi = std::mem::take(&mut self.completed_midi_recordings);
for (track_name, rec) in completed_midi {
self.flush_midi_recording_entry(&midi_dir, rate as u32, track_name, rec)
.await;
}
let recordings = std::mem::take(&mut self.audio_recordings);
for (track_name, rec) in recordings {
self.flush_recording_entry(&audio_dir, rate, track_name, rec)
.await;
}
let midi_recordings = std::mem::take(&mut self.midi_recordings);
for (track_name, rec) in midi_recordings {
self.flush_midi_recording_entry(&midi_dir, rate as u32, track_name, rec)
.await;
}
}
fn compute_peaks_from_stripes(
stripe_peaks: &[Vec<[f32; 2]>],
total_frames: usize,
channels: usize,
) -> serde_json::Value {
const MAX_PEAK_BINS: usize = 32_768;
if total_frames == 0 || stripe_peaks.is_empty() {
return serde_json::json!({"peaks": []});
}
let target_bins = total_frames.clamp(1024, MAX_PEAK_BINS);
let mut peaks = vec![vec![[0.0_f32, 0.0_f32]; target_bins]; channels];
for (ch, channel_peaks) in peaks.iter_mut().enumerate() {
let mut touched = vec![false; target_bins];
let empty = Vec::new();
let channel_stripes = stripe_peaks.get(ch).unwrap_or(&empty);
for (stripe_idx, stripe) in channel_stripes.iter().enumerate() {
let stripe_start = stripe_idx * RECORDING_STRIPE_FRAMES;
let stripe_end = ((stripe_idx + 1) * RECORDING_STRIPE_FRAMES).min(total_frames);
let start_bin = (stripe_start * target_bins) / total_frames.max(1);
let end_bin = ((stripe_end.saturating_sub(1)) * target_bins / total_frames.max(1))
.min(target_bins - 1);
for bin in start_bin..=end_bin {
if !touched[bin] {
channel_peaks[bin] = *stripe;
touched[bin] = true;
} else {
channel_peaks[bin][0] = channel_peaks[bin][0].min(stripe[0]);
channel_peaks[bin][1] = channel_peaks[bin][1].max(stripe[1]);
}
}
}
}
serde_json::json!({
"peaks": peaks.iter().map(|ch| {
ch.iter().map(|pair| serde_json::json!([pair[0], pair[1]])).collect::<Vec<_>>()
}).collect::<Vec<_>>()
})
}
async fn flush_recording_entry(
&mut self,
audio_dir: &Path,
rate: i32,
track_name: String,
rec: RecordingSession,
) {
if rec.samples.is_empty() || rec.channels == 0 {
return;
}
let trim_frames = self.hw_output_latency_frames;
let trim_samples = trim_frames * rec.channels;
let samples = if trim_samples > 0 && rec.samples.len() > trim_samples {
&rec.samples[trim_samples..]
} else {
&rec.samples[..]
};
if samples.is_empty() {
return;
}
let file_path = audio_dir.join(&rec.file_name);
let write_result =
crate::audio_codec::write_wav_f32(&file_path, samples, rec.channels, rate as u32);
if let Err(e) = write_result {
tracing::error!("flush_recording_entry: WAV write failed: {}", e);
self.notify_clients(Err(format!(
"Failed to write recording {}: {}",
file_path.display(),
e
)))
.await;
return;
}
let total_frames = rec.current_stripe_frames;
let peaks_json =
Self::compute_peaks_from_stripes(&rec.stripe_peaks, total_frames, rec.channels);
let peaks_file_name = format!("{}.json", rec.file_name);
let peaks_rel = format!("peaks/{}", peaks_file_name);
let peaks_path = self.session_peaks_dir().map(|d| d.join(&peaks_file_name));
if let Some(peaks_dir) = self.session_peaks_dir() {
let _ = std::fs::create_dir_all(&peaks_dir);
}
if let Some(ref path) = peaks_path
&& let Err(e) = std::fs::write(
path,
serde_json::to_string_pretty(&peaks_json).unwrap_or_default(),
)
{
tracing::warn!("Failed to write peaks file {}: {}", path.display(), e);
}
let length = samples.len() / rec.channels;
let start_sample = rec.start_sample.saturating_add(trim_frames);
let clip_rel_name = format!("audio/{}", rec.file_name);
let clip = AudioClip::new(
clip_rel_name.clone(),
start_sample,
start_sample.saturating_add(length.max(1)),
);
let (audio_ins, audio_outs) = if let Some(track) = self.state.lock().tracks.get(&track_name)
{
let track = track.lock();
let audio_ins = track.audio.ins.len();
let audio_outs = track.audio.outs.len();
track.audio.clips.push(clip.clone());
(audio_ins, audio_outs)
} else {
tracing::warn!(
"flush_recording_entry: track '{}' not found in engine state",
track_name
);
(0, 0)
};
self.notify_clients(Ok(Action::AddClip {
name: clip_rel_name,
track_name: track_name.clone(),
start: start_sample,
length,
offset: 0,
input_channel: 0,
muted: false,
peaks_file: peaks_path.is_some().then_some(peaks_rel),
kind: Kind::Audio,
fade_enabled: clip.fade_enabled,
fade_in_samples: clip.fade_in_samples,
fade_out_samples: clip.fade_out_samples,
source_name: None,
source_offset: None,
source_length: None,
preview_name: None,
pitch_correction_points: vec![],
pitch_correction_frame_likeness: None,
pitch_correction_inertia_ms: None,
pitch_correction_formant_compensation: None,
plugin_graph_json: Some(Self::default_clip_plugin_graph_json(audio_ins, audio_outs)),
}))
.await;
if let Some(track) = self.state.lock().tracks.get(&track_name).cloned() {
tokio::task::spawn_blocking(move || {
track.lock().preload_clips();
tracing::debug!("Preloaded clips for track '{}' after recording", track_name);
});
}
}
async fn flush_track_recording(&mut self, track_name: &str) {
let Some(audio_dir) = self.session_audio_dir() else {
self.audio_recordings.remove(track_name);
self.midi_recordings.remove(track_name);
self.completed_audio_recordings
.retain(|(name, _)| name != track_name);
self.completed_midi_recordings
.retain(|(name, _)| name != track_name);
return;
};
let Some(midi_dir) = self.session_midi_dir() else {
self.audio_recordings.remove(track_name);
self.midi_recordings.remove(track_name);
self.completed_audio_recordings
.retain(|(name, _)| name != track_name);
self.completed_midi_recordings
.retain(|(name, _)| name != track_name);
return;
};
if std::fs::create_dir_all(&audio_dir).is_err()
|| std::fs::create_dir_all(&midi_dir).is_err()
{
return;
}
let rate = self
.hw_driver
.as_ref()
.map(|o| o.lock().sample_rate())
.unwrap_or(48_000);
let mut i = 0;
while i < self.completed_audio_recordings.len() {
if self.completed_audio_recordings[i].0 == track_name {
let (name, rec) = self.completed_audio_recordings.remove(i);
self.flush_recording_entry(&audio_dir, rate, name, rec)
.await;
} else {
i += 1;
}
}
let mut j = 0;
while j < self.completed_midi_recordings.len() {
if self.completed_midi_recordings[j].0 == track_name {
let (name, rec) = self.completed_midi_recordings.remove(j);
self.flush_midi_recording_entry(&midi_dir, rate as u32, name, rec)
.await;
} else {
j += 1;
}
}
let Some(rec) = self.audio_recordings.remove(track_name) else {
if let Some(mrec) = self.midi_recordings.remove(track_name) {
self.flush_midi_recording_entry(
&midi_dir,
rate as u32,
track_name.to_string(),
mrec,
)
.await;
}
return;
};
self.flush_recording_entry(&audio_dir, rate, track_name.to_string(), rec)
.await;
if let Some(mrec) = self.midi_recordings.remove(track_name) {
self.flush_midi_recording_entry(&midi_dir, rate as u32, track_name.to_string(), mrec)
.await;
}
}
async fn flush_midi_recording_entry(
&mut self,
midi_dir: &Path,
sample_rate: u32,
track_name: String,
mut rec: MidiRecordingSession,
) {
if rec.events.is_empty() {
return;
}
rec.events.sort_by_key(|(sample, _)| *sample);
let clip_rel_name = format!("midi/{}", rec.file_name);
let clip_len_samples = rec
.events
.last()
.map(|(s, _)| s.saturating_sub(rec.start_sample as u64) as usize + 1)
.unwrap_or(1);
for (sample, _) in &mut rec.events {
*sample = sample.saturating_sub(rec.start_sample as u64);
}
let path = midi_dir.join(&rec.file_name);
if let Err(e) = Self::write_midi_file(&path, sample_rate, &rec.events) {
self.notify_clients(Err(format!(
"Failed to write MIDI recording {}: {}",
path.display(),
e
)))
.await;
return;
}
let mut clip = MIDIClip::new(
clip_rel_name.clone(),
rec.start_sample,
rec.start_sample.saturating_add(clip_len_samples.max(1)),
);
clip.offset = 0;
if let Some(track) = self.state.lock().tracks.get(&track_name) {
track.lock().midi.clips.push(clip);
}
self.notify_clients(Ok(Action::AddClip {
name: clip_rel_name,
track_name: track_name.clone(),
start: rec.start_sample,
length: clip_len_samples,
offset: 0,
input_channel: 0,
muted: false,
peaks_file: None,
kind: Kind::MIDI,
fade_enabled: true,
fade_in_samples: 240,
fade_out_samples: 240,
source_name: None,
source_offset: None,
source_length: None,
preview_name: None,
pitch_correction_points: vec![],
pitch_correction_frame_likeness: None,
pitch_correction_inertia_ms: None,
pitch_correction_formant_compensation: None,
plugin_graph_json: None,
}))
.await;
if let Some(track) = self.state.lock().tracks.get(&track_name).cloned() {
tokio::task::spawn_blocking(move || {
track.lock().preload_clips();
tracing::debug!(
"Preloaded clips for track '{}' after MIDI recording",
track_name
);
});
}
}
fn write_midi_file(
path: &Path,
sample_rate: u32,
events: &[(u64, Vec<u8>)],
) -> Result<(), String> {
let ppq: u16 = 480;
let ticks_per_second: u64 = 960;
let arena = Arena::new();
let mut track_events: Vec<TrackEvent<'_>> = vec![TrackEvent {
delta: u28::new(0),
kind: TrackEventKind::Meta(MetaMessage::Tempo(u24::new(500_000))),
}];
let mut prev_ticks = 0_u64;
for (sample, data) in events {
let ticks = sample.saturating_mul(ticks_per_second) / sample_rate.max(1) as u64;
let delta = ticks.saturating_sub(prev_ticks).min(u32::MAX as u64) as u32;
prev_ticks = ticks;
let Ok(live) = LiveEvent::parse(data) else {
continue;
};
let kind = live.as_track_event(&arena);
track_events.push(TrackEvent {
delta: u28::new(delta),
kind,
});
}
track_events.push(TrackEvent {
delta: u28::new(0),
kind: TrackEventKind::Meta(MetaMessage::EndOfTrack),
});
let smf = Smf {
header: Header::new(Format::SingleTrack, Timing::Metrical(u15::new(ppq))),
tracks: vec![track_events],
};
let mut file = File::create(path).map_err(|e| e.to_string())?;
smf.write_std(&mut file).map_err(|e| e.to_string())
}
pub async fn init(&mut self) {
let max_threads = num_cpus::get();
for id in 0..max_threads {
let (tx, rx) = channel::<Message>(32);
let tx_thread = self.tx.clone();
let handler = tokio::spawn(async move {
let wrk = Worker::new(id, rx, tx_thread, 8);
wrk.await.work().await;
});
self.workers.push(WorkerData::new(tx.clone(), handler));
}
}
async fn notify_clients(&mut self, action: Result<Action, String>) {
self.clients.retain(|client| !client.is_closed());
for client in self.clients.iter() {
if client
.send(Message::Response(action.clone()))
.await
.is_err()
{}
}
}
fn spawn_plugin_host_stderr_reader(&self, stderr: std::process::ChildStderr, source: String) {
let tx = self.tx.clone();
std::thread::spawn(move || {
use std::io::{BufRead, BufReader};
let reader = BufReader::new(stderr);
for line in reader.lines() {
if let Ok(line) = line
&& !line.is_empty()
{
let _ = tx.blocking_send(Message::Request(Action::Log {
source: source.clone(),
message: line,
}));
}
}
});
}
fn set_osc_enabled_with<F>(&mut self, enabled: bool, start_server: F) -> Result<(), String>
where
F: FnOnce(Sender<Message>) -> Result<OscServer, String>,
{
if enabled {
if self.osc_server.is_none() {
self.osc_server = Some(start_server(self.tx.clone())?);
}
} else if let Some(mut server) = self.osc_server.take() {
server.stop();
}
Ok(())
}
fn track_handle_by_name(&self, track_name: &str) -> Option<Arc<UnsafeMutex<Box<Track>>>> {
self.state.lock().tracks.get(track_name).cloned()
}
fn track_handle_or_err(
&self,
track_name: &str,
) -> Result<Arc<UnsafeMutex<Box<Track>>>, String> {
self.track_handle_by_name(track_name)
.ok_or_else(|| format!("Track not found: {track_name}"))
}
fn add_clip_to_track(&self, request: ClipAddRequest<'_>) {
if let Some(track) = self.state.lock().tracks.get(request.track_name) {
let track = track.lock();
if track.is_master || track.is_folder {
return;
}
match request.kind {
Kind::Audio => {
let mut clip = AudioClip::new(
request.name.to_string(),
request.start,
request.start.saturating_add(request.length.max(1)),
);
clip.offset = request.offset;
let max_lane = track.audio.ins.len().saturating_sub(1);
clip.input_channel = request.input_channel.min(max_lane);
clip.muted = request.muted;
clip.peaks_file = request.peaks_file;
clip.fade_enabled = request.fade_enabled;
clip.fade_in_samples = request.fade_in_samples;
clip.fade_out_samples = request.fade_out_samples;
clip.pitch_correction_preview_name = request.preview_name;
clip.pitch_correction_source_name = request.source_name;
clip.pitch_correction_source_offset = request.source_offset;
clip.pitch_correction_source_length = request.source_length;
clip.pitch_correction_points = request.pitch_correction_points;
clip.pitch_correction_frame_likeness = request.pitch_correction_frame_likeness;
clip.pitch_correction_inertia_ms = request.pitch_correction_inertia_ms;
clip.pitch_correction_formant_compensation =
request.pitch_correction_formant_compensation;
clip.plugin_graph_json = request.plugin_graph_json;
track.audio.clips.push(clip);
#[cfg(unix)]
track.clip_pitch_shifters.clear();
}
Kind::MIDI => {
let mut clip = MIDIClip::new(
request.name.to_string(),
request.start,
request.start.saturating_add(request.length.max(1)),
);
clip.offset = request.offset;
let max_lane = track.midi.ins.len().saturating_sub(1);
clip.input_channel = request.input_channel.min(max_lane);
clip.muted = request.muted;
track.midi.clips.push(clip);
}
}
}
}
fn audio_clip_from_data(data: &crate::message::AudioClipData) -> AudioClip {
let mut clip = AudioClip::new(
data.name.clone(),
data.start,
data.start.saturating_add(data.length.max(1)),
);
clip.offset = data.offset;
clip.input_channel = data.input_channel;
clip.muted = data.muted;
clip.peaks_file = data.peaks_file.clone();
clip.fade_enabled = data.fade_enabled;
clip.fade_in_samples = data.fade_in_samples;
clip.fade_out_samples = data.fade_out_samples;
clip.pitch_correction_preview_name = data.preview_name.clone();
clip.pitch_correction_source_name = data.source_name.clone();
clip.pitch_correction_source_offset = data.source_offset;
clip.pitch_correction_source_length = data.source_length;
clip.pitch_correction_points = data.pitch_correction_points.clone();
clip.pitch_correction_frame_likeness = data.pitch_correction_frame_likeness;
clip.pitch_correction_inertia_ms = data.pitch_correction_inertia_ms;
clip.pitch_correction_formant_compensation = data.pitch_correction_formant_compensation;
clip.plugin_graph_json = data.plugin_graph_json.clone();
clip.grouped_clips = data
.grouped_clips
.iter()
.map(Self::audio_clip_from_data)
.collect();
for child in &mut clip.grouped_clips {
child.fade_enabled = false;
child.fade_in_samples = 0;
child.fade_out_samples = 0;
}
clip
}
fn midi_clip_from_data(data: &crate::message::MidiClipData) -> MIDIClip {
let mut clip = MIDIClip::new(
data.name.clone(),
data.start,
data.start.saturating_add(data.length.max(1)),
);
clip.offset = data.offset;
clip.input_channel = data.input_channel;
clip.muted = data.muted;
clip.grouped_clips = data
.grouped_clips
.iter()
.map(Self::midi_clip_from_data)
.collect();
clip
}
fn add_grouped_clip_to_track(
&self,
track_name: &str,
kind: Kind,
audio_clip: Option<crate::message::AudioClipData>,
midi_clip: Option<crate::message::MidiClipData>,
) {
if let Some(track) = self.state.lock().tracks.get(track_name) {
let track = track.lock();
if track.is_master {
return;
}
match kind {
Kind::Audio => {
if let Some(mut clip) = audio_clip.map(|clip| Self::audio_clip_from_data(&clip))
{
let max_lane = track.audio.ins.len().saturating_sub(1);
clip.input_channel = clip.input_channel.min(max_lane);
track.audio.clips.push(clip);
#[cfg(unix)]
track.clip_pitch_shifters.clear();
}
}
Kind::MIDI => {
if let Some(mut clip) = midi_clip.map(|clip| Self::midi_clip_from_data(&clip)) {
let max_lane = track.midi.ins.len().saturating_sub(1);
clip.input_channel = clip.input_channel.min(max_lane);
track.midi.clips.push(clip);
}
}
}
}
}
fn remove_clips_from_track(&self, track_name: &str, kind: Kind, clip_indices: &[usize]) {
if let Some(track) = self.state.lock().tracks.get(track_name) {
let track = track.lock();
let mut indices = clip_indices.to_vec();
indices.sort_unstable();
indices.dedup();
match kind {
Kind::Audio => {
for idx in indices.into_iter().rev() {
if idx < track.audio.clips.len() {
track.audio.clips.remove(idx);
}
}
#[cfg(unix)]
track.clip_pitch_shifters.clear();
}
Kind::MIDI => {
for idx in indices.into_iter().rev() {
if idx < track.midi.clips.len() {
track.midi.clips.remove(idx);
}
}
}
}
}
}
fn rename_clip_references(
&self,
track_name: &str,
kind: Kind,
clip_index: usize,
new_name: &str,
) {
let Some(track) = self.state.lock().tracks.get(track_name) else {
return;
};
let track = track.lock();
let old_name = match kind {
Kind::Audio => {
if clip_index >= track.audio.clips.len() {
return;
}
track.audio.clips[clip_index].name.clone()
}
Kind::MIDI => {
if clip_index >= track.midi.clips.len() {
return;
}
track.midi.clips[clip_index].name.clone()
}
};
let new_file_name = match kind {
Kind::Audio => format!("audio/{}.wav", new_name),
Kind::MIDI => {
let ext = std::path::Path::new(&old_name)
.extension()
.and_then(|e| e.to_str())
.map(|s| s.to_ascii_lowercase())
.filter(|e| e == "mid" || e == "midi")
.unwrap_or_else(|| "mid".to_string());
format!("midi/{}.{}", new_name, ext)
}
};
let _ = track;
for (_, other_track) in self.state.lock().tracks.iter() {
let other_track = other_track.lock();
match kind {
Kind::Audio => {
for clip in &mut other_track.audio.clips {
if clip.name == old_name {
clip.name = new_file_name.clone();
}
if clip.pitch_correction_source_name.as_deref() == Some(old_name.as_str()) {
clip.pitch_correction_source_name = Some(new_file_name.clone());
}
}
}
Kind::MIDI => {
for clip in &mut other_track.midi.clips {
if clip.name == old_name {
clip.name = new_file_name.clone();
}
}
}
}
}
}
fn set_clip_fade(
&self,
track_name: &str,
clip_index: usize,
kind: Kind,
fade_enabled: bool,
fade_in_samples: usize,
fade_out_samples: usize,
) {
let Some(track) = self.state.lock().tracks.get(track_name) else {
return;
};
let track = track.lock();
match kind {
Kind::Audio => {
if let Some(clip) = track.audio.clips.get_mut(clip_index) {
clip.fade_enabled = fade_enabled;
clip.fade_in_samples = fade_in_samples;
clip.fade_out_samples = fade_out_samples;
}
}
Kind::MIDI => {}
}
}
fn set_clip_bounds(
&self,
track_name: &str,
clip_index: usize,
kind: Kind,
start: usize,
length: usize,
offset: usize,
) {
let Some(track) = self.state.lock().tracks.get(track_name) else {
return;
};
let track = track.lock();
match kind {
Kind::Audio => {
if let Some(clip) = track.audio.clips.get_mut(clip_index) {
clip.start = start;
clip.end = start.saturating_add(length.max(1));
clip.offset = offset;
clip.pitch_correction_preview_name = None;
clip.pitch_correction_source_name = None;
clip.pitch_correction_source_offset = None;
clip.pitch_correction_source_length = None;
clip.pitch_correction_points.clear();
clip.pitch_correction_frame_likeness = None;
clip.pitch_correction_inertia_ms = None;
clip.pitch_correction_formant_compensation = None;
}
#[cfg(unix)]
track.clip_pitch_shifters.clear();
}
Kind::MIDI => {
if let Some(clip) = track.midi.clips.get_mut(clip_index) {
clip.start = start;
clip.end = start.saturating_add(length.max(1));
clip.offset = offset;
}
}
}
}
fn set_clip_source_name(&self, track_name: &str, clip_index: usize, kind: Kind, name: String) {
let Some(track) = self.state.lock().tracks.get(track_name) else {
return;
};
let track = track.lock();
match kind {
Kind::Audio => {
if let Some(clip) = track.audio.clips.get_mut(clip_index) {
clip.name = name;
}
#[cfg(unix)]
track.clip_pitch_shifters.clear();
}
Kind::MIDI => {
if let Some(clip) = track.midi.clips.get_mut(clip_index) {
clip.name = name;
}
}
}
}
fn set_clip_muted(&self, track_name: &str, clip_index: usize, kind: Kind, muted: bool) {
let Some(track) = self.state.lock().tracks.get(track_name) else {
return;
};
let track = track.lock();
match kind {
Kind::Audio => {
if let Some(clip) = track.audio.clips.get_mut(clip_index) {
clip.muted = muted;
}
}
Kind::MIDI => {
if let Some(clip) = track.midi.clips.get_mut(clip_index) {
clip.muted = muted;
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn set_clip_pitch_correction(
&self,
track_name: &str,
clip_index: usize,
preview_name: Option<String>,
source_name: Option<String>,
source_offset: Option<usize>,
source_length: Option<usize>,
pitch_correction_points: Vec<crate::message::PitchCorrectionPointData>,
pitch_correction_frame_likeness: Option<f32>,
pitch_correction_inertia_ms: Option<u16>,
pitch_correction_formant_compensation: Option<bool>,
) {
if let Some(track) = self.state.lock().tracks.get(track_name) {
let track = track.lock();
if let Some(clip) = track.audio.clips.get_mut(clip_index) {
clip.pitch_correction_preview_name = preview_name;
clip.pitch_correction_source_name = source_name;
clip.pitch_correction_source_offset = source_offset;
clip.pitch_correction_source_length = source_length;
clip.pitch_correction_points = pitch_correction_points;
clip.pitch_correction_frame_likeness = pitch_correction_frame_likeness;
clip.pitch_correction_inertia_ms = pitch_correction_inertia_ms;
clip.pitch_correction_formant_compensation = pitch_correction_formant_compensation;
}
#[cfg(unix)]
track.clip_pitch_shifters.clear();
}
}
async fn request_hw_cycle(&mut self) {
if self.awaiting_hwfinished {
tracing::debug!("request_hw_cycle skipped (already awaiting)");
return;
}
tracing::debug!("request_hw_cycle sending TracksFinished");
self.apply_hw_out_gain_and_meter().await;
if let Some((after_frames, loop_start, cycle_end_sample)) =
self.scheduled_loop_wrap_for_next_cycle()
{
self.notified_loop_wrap_sample = Some(cycle_end_sample);
self.notify_clients(Ok(Action::TransportPositionAt {
sample: loop_start,
after_frames,
}))
.await;
} else {
self.notified_loop_wrap_sample = None;
}
if let Some(worker) = &self.hw_worker {
if !self.pending_hw_midi_out_events_by_device.is_empty() {
let out_events = std::mem::take(&mut self.pending_hw_midi_out_events_by_device);
if let Err(e) = worker.tx.send(Message::HWMidiOutEvents(out_events)).await {
error!("Error sending HWMidiOutEvents {e}");
}
}
match worker.tx.send(Message::TracksFinished).await {
Ok(_) => {
self.awaiting_hwfinished = true;
}
Err(e) => {
error!("Error sending TracksFinished {e}");
}
}
}
}
async fn clear_hw_midi_output_state(&mut self, send_panic: bool) {
self.pending_hw_midi_out_events.clear();
self.pending_hw_midi_out_events_by_device.clear();
{
let state = self.state.lock();
for track in state.tracks.values() {
track.lock().take_hw_midi_out_events();
}
}
let panic_events = if send_panic {
self.note_off_events_for_all_active_tracks()
} else {
vec![]
};
if let Some(worker) = &self.hw_worker {
if let Err(e) = worker.tx.send(Message::ClearHWMidiOutEvents).await {
error!("Error clearing pending HWMidiOutEvents {e}");
}
if !panic_events.is_empty()
&& let Err(e) = worker.tx.send(Message::HWMidiOutEvents(panic_events)).await
{
error!("Error sending transport restart MIDI panic events {e}");
}
} else if !panic_events.is_empty() {
self.pending_hw_midi_out_events_by_device
.extend(panic_events);
}
}
fn invalidate_track_cycle_state(&mut self) {
self.track_process_epoch = self.track_process_epoch.saturating_add(1);
self.task_processing_started_at.clear();
self.cycle_tasks.clear();
self.cycle_task_deps.clear();
self.cycle_tasks_running.clear();
self.cycle_tasks_finished.clear();
let state = self.state.lock();
for track in state.tracks.values() {
let t = track.lock();
t.audio.finished = false;
t.audio.processing = false;
}
}
fn force_stalled_task_completions(&mut self) {
let now = Instant::now();
let running: Vec<ProcessTask> = self.cycle_tasks_running.clone();
for task in running {
let key = Self::task_key(&task);
let Some(started) = self.task_processing_started_at.get(&key).copied() else {
continue;
};
if now.duration_since(started) < Self::TRACK_PROCESS_TIMEOUT {
continue;
}
if Self::task_running_finished_contains(&self.cycle_tasks_finished, &task) {
self.task_processing_started_at.remove(&key);
continue;
}
let track = match &task {
ProcessTask::Track(t)
| ProcessTask::FolderInput(t)
| ProcessTask::FolderOutput(t) => t.clone(),
ProcessTask::Plugin { track, .. } => track.clone(),
};
{
let t = track.lock();
if t.audio.finished || !t.audio.processing {
self.task_processing_started_at.remove(&key);
continue;
}
for out in &t.audio.outs {
out.buffer.lock().fill(0.0);
*out.finished.lock() = true;
}
t.audio.processing = false;
t.audio.finished = true;
}
self.cycle_tasks_running
.retain(|t| Self::task_key(t) != key);
self.cycle_tasks_finished.push(task.clone());
self.task_processing_started_at.remove(&key);
tracing::warn!(
"Task '{}' exceeded process timeout ({} ms); forcing silent completion for cycle",
Self::task_track_name(&task),
Self::TRACK_PROCESS_TIMEOUT.as_millis()
);
}
}
fn should_publish_hw_out_meters(&mut self) -> bool {
let now = Instant::now();
match self.last_hw_out_meter_publish {
Some(last) if now.duration_since(last) < Self::METER_PUBLISH_INTERVAL => false,
_ => {
self.last_hw_out_meter_publish = Some(now);
true
}
}
}
fn should_publish_track_meters(&mut self) -> bool {
let now = Instant::now();
match self.last_track_meter_publish {
Some(last) if now.duration_since(last) < Self::METER_PUBLISH_INTERVAL => false,
_ => {
self.last_track_meter_publish = Some(now);
true
}
}
}
fn should_publish_hw_out_linear(&mut self, peaks_linear: &[f32]) -> bool {
#[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
{
self.hw_out_meter_publish_phase = !self.hw_out_meter_publish_phase;
if !self.hw_out_meter_publish_phase {
return false;
}
let changed = if self.last_hw_out_meter_linear.len() != peaks_linear.len() {
true
} else {
self.last_hw_out_meter_linear
.iter()
.zip(peaks_linear.iter())
.any(|(prev, next)| (prev - next).abs() >= Self::HW_OUT_METER_LINEAR_EPSILON)
};
if !changed {
return false;
}
self.last_hw_out_meter_linear.clear();
self.last_hw_out_meter_linear
.extend_from_slice(peaks_linear);
true
}
#[cfg(not(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd")))]
{
let _ = peaks_linear;
false
}
}
async fn maybe_notify_hw_out_meter(&mut self, _meter_db: Vec<f32>) {
{}
}
fn collect_changed_track_meters(
&mut self,
_tracks: &[(String, Arc<UnsafeMutex<Box<Track>>>)],
) -> Vec<(String, Vec<f32>)> {
Vec::new()
}
async fn apply_hw_out_gain_and_meter(&mut self) {
let gain = if self.hw_out_muted {
0.0
} else {
10.0_f32.powf(self.hw_out_level_db / 20.0)
};
let should_notify_interval = self.should_publish_hw_out_meters();
if let Some(oss) = self.hw_driver.clone() {
let hw = oss.lock();
hw.set_output_gain_balance(gain, self.hw_out_balance);
if !should_notify_interval {
return;
}
} else {
#[cfg(unix)]
{
if let Some(jack) = self.jack_runtime.clone() {
jack.lock().set_output_gain_linear(gain);
jack.lock().set_output_balance(self.hw_out_balance);
if !should_notify_interval {
return;
}
} else {
return;
}
}
#[cfg(not(unix))]
{
return;
}
}
let peaks_linear = if let Some(oss) = self.hw_driver.clone() {
oss.lock().output_meter_linear(gain, self.hw_out_balance)
} else {
#[cfg(unix)]
{
if let Some(jack) = self.jack_runtime.clone() {
let outs = jack.lock().audio_outs();
let out_count = outs.len();
let b = if out_count == 2 {
self.hw_out_balance.clamp(-1.0, 1.0)
} else {
0.0
};
let mut meters_linear = Vec::with_capacity(out_count);
for (channel_idx, channel) in outs.iter().enumerate() {
let balance_gain = if out_count == 2 {
if channel_idx == 0 {
(1.0 - b).clamp(0.0, 1.0)
} else {
(1.0 + b).clamp(0.0, 1.0)
}
} else {
1.0
};
let buf = channel.buffer.lock();
let peak = crate::simd::peak_abs(buf) * gain * balance_gain;
meters_linear.push(peak);
}
meters_linear
} else {
return;
}
}
#[cfg(not(unix))]
{
return;
}
};
if self.hw_out_peak_hold_linear.len() != peaks_linear.len() {
self.hw_out_peak_hold_linear.resize(peaks_linear.len(), 0.0);
}
let mut held_peaks = Vec::with_capacity(peaks_linear.len());
for (idx, peak_now) in peaks_linear.iter().copied().enumerate() {
let held = self.hw_out_peak_hold_linear[idx] * 0.92;
let next = peak_now.max(held);
self.hw_out_peak_hold_linear[idx] = next;
held_peaks.push(next);
}
let should_notify =
should_notify_interval && self.should_publish_hw_out_linear(&held_peaks);
let meter_db: Vec<f32> = held_peaks
.into_iter()
.map(Self::meter_linear_to_db)
.collect();
self.latest_hw_out_meter_db = Arc::new(meter_db.clone());
if should_notify {
self.maybe_notify_hw_out_meter(meter_db).await;
}
}
fn preload_track_clips_spawn(&self) {
let tracks: Vec<_> = self.state.lock().tracks.values().cloned().collect();
for track in tracks {
tokio::task::spawn_blocking(move || {
track.lock().preload_clips();
});
}
}
async fn preload_track_clips(&self) {
let tracks: Vec<_> = self.state.lock().tracks.values().cloned().collect();
if tracks.is_empty() {
return;
}
let mut handles = Vec::with_capacity(tracks.len());
for track in tracks {
handles.push(tokio::task::spawn_blocking(move || {
track.lock().preload_clips();
}));
}
for handle in handles {
if let Err(e) = handle.await {
tracing::warn!("Clip preload task panicked: {e}");
}
}
}
fn build_task_graph(
&self,
) -> (
Vec<ProcessTask>,
std::collections::HashMap<String, Vec<String>>,
) {
let state = self.state.lock();
let ordered: Vec<(String, Arc<UnsafeMutex<Box<Track>>>)> = state
.tracks
.iter()
.map(|(name, track)| (name.clone(), track.clone()))
.collect();
let mut tasks = Vec::new();
let mut deps = std::collections::HashMap::new();
for (_name, track) in &ordered {
let t = track.lock();
if t.parent_track.is_some() {
continue;
}
self.append_track_tasks(track.clone(), None, &mut tasks, &mut deps);
}
(tasks, deps)
}
fn append_track_tasks(
&self,
track: Arc<UnsafeMutex<Box<Track>>>,
predecessor: Option<String>,
tasks: &mut Vec<ProcessTask>,
deps: &mut std::collections::HashMap<String, Vec<String>>,
) -> (String, String) {
use crate::message::ConnectableRef;
let t = track.lock();
if t.is_folder {
let folder_input = ProcessTask::FolderInput(track.clone());
let folder_input_key = Self::task_key(&folder_input);
tasks.push(folder_input.clone());
let folder_input_deps: Vec<_> = predecessor.into_iter().collect();
deps.insert(folder_input_key.clone(), folder_input_deps);
let mut source_keys: std::collections::HashMap<ConnectableRef, String> =
std::collections::HashMap::new();
let mut target_keys: std::collections::HashMap<ConnectableRef, String> =
std::collections::HashMap::new();
source_keys.insert(ConnectableRef::TrackInput, folder_input_key.clone());
target_keys.insert(ConnectableRef::TrackInput, folder_input_key.clone());
let mut plugin_keys: Vec<String> = Vec::new();
for idx in 0..t.clap_plugins.len() {
let plugin_task = ProcessTask::Plugin {
track: track.clone(),
kind: PluginKind::Clap,
index: idx,
};
let plugin_key = Self::task_key(&plugin_task);
let id = t.clap_plugins[idx].id;
source_keys.insert(ConnectableRef::ClapPlugin(id), plugin_key.clone());
target_keys.insert(ConnectableRef::ClapPlugin(id), plugin_key.clone());
tasks.push(plugin_task);
deps.insert(plugin_key.clone(), vec![folder_input_key.clone()]);
plugin_keys.push(plugin_key);
}
for idx in 0..t.vst3_plugins.len() {
let plugin_task = ProcessTask::Plugin {
track: track.clone(),
kind: PluginKind::Vst3,
index: idx,
};
let plugin_key = Self::task_key(&plugin_task);
let id = t.vst3_plugins[idx].id;
source_keys.insert(ConnectableRef::Vst3Plugin(id), plugin_key.clone());
target_keys.insert(ConnectableRef::Vst3Plugin(id), plugin_key.clone());
tasks.push(plugin_task);
deps.insert(plugin_key.clone(), vec![folder_input_key.clone()]);
plugin_keys.push(plugin_key);
}
#[cfg(all(unix, not(target_os = "macos")))]
for idx in 0..t.lv2_plugins.len() {
let plugin_task = ProcessTask::Plugin {
track: track.clone(),
kind: PluginKind::Lv2,
index: idx,
};
let plugin_key = Self::task_key(&plugin_task);
let id = t.lv2_plugins[idx].id;
source_keys.insert(ConnectableRef::Lv2Plugin(id), plugin_key.clone());
target_keys.insert(ConnectableRef::Lv2Plugin(id), plugin_key.clone());
tasks.push(plugin_task);
deps.insert(plugin_key.clone(), vec![folder_input_key.clone()]);
plugin_keys.push(plugin_key);
}
let mut child_keys = Vec::new();
for child_track in &t.child_tracks {
let (child_first, child_last) = self.append_track_tasks(
child_track.clone(),
Some(folder_input_key.clone()),
tasks,
deps,
);
let child_name = child_track.lock().name.clone();
source_keys.insert(
ConnectableRef::ChildTrack(child_name.clone()),
child_last.clone(),
);
target_keys.insert(ConnectableRef::ChildTrack(child_name), child_first.clone());
child_keys.push((child_first, child_last.clone()));
}
let folder_output = ProcessTask::FolderOutput(track.clone());
let folder_output_key = Self::task_key(&folder_output);
source_keys.insert(ConnectableRef::TrackOutput, folder_output_key.clone());
target_keys.insert(ConnectableRef::TrackOutput, folder_output_key.clone());
tasks.push(folder_output.clone());
let mut folder_output_deps = vec![folder_input_key.clone()];
folder_output_deps.extend(plugin_keys);
folder_output_deps.extend(child_keys.iter().map(|(_, last)| last.clone()));
deps.insert(folder_output_key.clone(), folder_output_deps);
for conn in t.connectable_connections() {
let Some(source_key) = source_keys.get(&conn.from) else {
continue;
};
let Some(target_key) = target_keys.get(&conn.to) else {
continue;
};
if source_key == target_key {
continue;
}
let entry = deps.entry(target_key.clone()).or_default();
if !entry.contains(source_key) {
entry.push(source_key.clone());
}
}
(folder_input_key, folder_output_key)
} else {
let task = ProcessTask::Track(track.clone());
let task_key = Self::task_key(&task);
tasks.push(task.clone());
deps.insert(
task_key.clone(),
predecessor.into_iter().collect::<Vec<_>>(),
);
(task_key.clone(), task_key)
}
}
fn task_track_name(task: &ProcessTask) -> String {
match task {
ProcessTask::Track(t) | ProcessTask::FolderInput(t) | ProcessTask::FolderOutput(t) => {
t.lock().name.clone()
}
ProcessTask::Plugin { track, .. } => track.lock().name.clone(),
}
}
fn task_key(task: &ProcessTask) -> String {
match task {
ProcessTask::Track(t) => format!("Track:{:p}", std::sync::Arc::as_ptr(t)),
ProcessTask::FolderInput(t) => {
format!("FolderInput:{:p}", std::sync::Arc::as_ptr(t))
}
ProcessTask::FolderOutput(t) => {
format!("FolderOutput:{:p}", std::sync::Arc::as_ptr(t))
}
ProcessTask::Plugin { track, kind, index } => format!(
"Plugin:{:?}:{:p}:{}",
kind,
std::sync::Arc::as_ptr(track),
index
),
}
}
fn task_running_finished_contains(haystack: &[ProcessTask], needle: &ProcessTask) -> bool {
let needle_key = Self::task_key(needle);
haystack.iter().any(|t| Self::task_key(t) == needle_key)
}
fn task_ready(&self, task: &ProcessTask) -> bool {
match task {
ProcessTask::Track(t) | ProcessTask::FolderInput(t) => {
let track = t.lock();
track.audio.ready()
}
ProcessTask::Plugin { .. } | ProcessTask::FolderOutput(_) => true,
}
}
fn task_dependencies_satisfied(&self, task: &ProcessTask) -> bool {
let key = Self::task_key(task);
let Some(deps) = self.cycle_task_deps.get(&key) else {
return true;
};
let finished_keys: std::collections::HashSet<String> = self
.cycle_tasks_finished
.iter()
.map(Self::task_key)
.collect();
deps.iter().all(|d| finished_keys.contains(d))
}
fn prepare_task_track(&self, task: &ProcessTask) {
let track = match task {
ProcessTask::Track(t) | ProcessTask::FolderInput(t) | ProcessTask::FolderOutput(t) => t,
ProcessTask::Plugin { track, .. } => track,
};
let t = track.lock();
t.set_transport_sample(self.transport_sample);
t.set_loop_config(self.loop_enabled, self.loop_range_samples);
t.set_transport_timing(self.tempo_bpm, self.tsig_num, self.tsig_denom);
t.process_epoch = self.track_process_epoch;
t.set_clip_playback_enabled(self.clip_playback_enabled && self.playing);
t.set_record_tap_enabled(self.playing && self.record_enabled);
t.audio.processing = true;
}
async fn send_tasks(&mut self) -> bool {
if !self.playing {
return false;
}
self.refresh_realtime_infection();
self.force_stalled_task_completions();
if self.cycle_tasks.is_empty() {
let (tasks, deps) = self.build_task_graph();
let task_names: Vec<String> = tasks.iter().map(Self::task_track_name).collect();
tracing::debug!(
"send_tasks rebuilt graph: {} tasks ({:?})",
tasks.len(),
task_names
);
self.cycle_tasks = tasks;
self.cycle_task_deps = deps;
self.cycle_tasks_running.clear();
self.cycle_tasks_finished.clear();
}
let mut finished = true;
let mut dispatched = 0;
loop {
let next_task = {
let mut next = None;
tracing::debug!(
"selecting next: cycle={} running={} finished={}",
self.cycle_tasks.len(),
self.cycle_tasks_running.len(),
self.cycle_tasks_finished.len()
);
for task in &self.cycle_tasks {
let in_running =
Self::task_running_finished_contains(&self.cycle_tasks_running, task);
let in_finished =
Self::task_running_finished_contains(&self.cycle_tasks_finished, task);
tracing::debug!(
"checking task {} in_running={} in_finished={}",
Self::task_track_name(task),
in_running,
in_finished
);
if in_finished || in_running {
continue;
}
finished = false;
if !self.task_dependencies_satisfied(task) {
continue;
}
if !self.task_ready(task) {
continue;
}
next = Some(task.clone());
break;
}
next
};
let Some(task) = next_task else {
tracing::debug!(
"send_tasks returning finished={} (dispatched {})",
finished,
dispatched
);
return finished;
};
let Some(worker_index) = self.take_ready_worker_index() else {
self.force_stalled_task_completions();
tracing::debug!(
"send_tasks returning false (no ready worker; dispatched {})",
dispatched
);
return false;
};
if Self::task_running_finished_contains(&self.cycle_tasks_finished, &task)
|| Self::task_running_finished_contains(&self.cycle_tasks_running, &task)
{
continue;
}
dispatched += 1;
let task_key = Self::task_key(&task);
tracing::debug!(
"send_tasks dispatching {} (running={} finished={})",
Self::task_track_name(&task),
self.cycle_tasks_running.len(),
self.cycle_tasks_finished.len()
);
self.prepare_task_track(&task);
self.cycle_tasks_running.push(task.clone());
tracing::debug!(
"inserted task {} -> running_size={}",
Self::task_track_name(&task),
self.cycle_tasks_running.len()
);
self.task_processing_started_at
.insert(task_key.clone(), Instant::now());
let worker = &self.workers[worker_index];
if let Err(e) = worker.tx.send(Message::ProcessTask(task.clone())).await {
self.cycle_tasks_running
.retain(|t| Self::task_key(t) != task_key);
self.task_processing_started_at.remove(&task_key);
self.notify_clients(Err(format!("Failed to send task to worker: {}", e)))
.await;
}
}
}
async fn on_all_tracks_finished(&mut self) {
if self.transport_restart_pending {
let state = self.state.lock();
for track in state.tracks.values() {
track.lock().take_hw_midi_out_events();
}
} else if self.hw_worker.is_some() {
self.active_hw_notes_cycle_start = self.active_hw_notes_by_track.clone();
let mut out_events = self.collect_hw_midi_output_events_by_device();
if self.loop_enabled
&& let Some((_, loop_end)) = self.loop_range_samples
{
let cycle_end = self
.transport_sample
.saturating_add(self.current_cycle_samples());
if self.transport_sample < loop_end && cycle_end >= loop_end {
let wrap_frame = loop_end
.saturating_sub(self.transport_sample)
.min(self.current_cycle_samples())
as u32;
out_events.extend(self.note_off_events_for_active_snapshot(
&self.active_hw_notes_cycle_start,
wrap_frame,
));
out_events.sort_by(|a, b| {
a.event
.frame
.cmp(&b.event.frame)
.then_with(|| a.device.cmp(&b.device))
});
}
}
self.pending_hw_midi_out_events_by_device.extend(out_events);
} else {
self.pending_hw_midi_out_events = self.collect_hw_midi_output_events();
}
self.request_hw_cycle().await;
}
fn take_ready_worker_index(&mut self) -> Option<usize> {
while !self.ready_workers.is_empty() {
let worker_index = self.ready_workers.remove(0);
if worker_index < self.workers.len() {
return Some(worker_index);
}
}
None
}
fn push_ready_worker(&mut self, worker_index: usize) {
self.ready_workers.push(worker_index);
}
async fn publish_track_meters(&mut self) {
if !self.should_publish_track_meters() {
return;
}
let tracks: Vec<(String, Arc<UnsafeMutex<Box<Track>>>)> = self
.state
.lock()
.tracks
.iter()
.map(|(name, track)| (name.clone(), track.clone()))
.collect();
let mut snapshot = Vec::with_capacity(tracks.len());
for (name, track) in &tracks {
let linear = self
.track_meter_linear_by_track
.get(name)
.cloned()
.unwrap_or_else(|| track.lock().output_meter_linear());
let output_db = linear
.iter()
.copied()
.map(Self::meter_linear_to_db)
.collect::<Vec<_>>();
snapshot.push((name.clone(), output_db));
}
self.latest_track_meter_snapshot = Arc::new(snapshot);
let meters = self.collect_changed_track_meters(&tracks);
for (track_name, output_db) in meters {
self.notify_clients(Ok(Action::TrackMeters {
track_name,
output_db,
}))
.await;
}
}
async fn publish_clap_state_dirty(&mut self) {
let tracks: Vec<(String, Arc<UnsafeMutex<Box<Track>>>)> = self
.state
.lock()
.tracks
.iter()
.map(|(name, track)| (name.clone(), track.clone()))
.collect();
for (track_name, track) in &tracks {
let dirty = track.lock().take_dirty_clap_instances();
for instance_id in dirty {
self.notify_clients(Ok(Action::TrackClapStateDirty {
track_name: track_name.clone(),
instance_id,
}))
.await;
}
}
}
fn reset_meters_after_stop(&mut self) {
self.last_hw_out_meter_publish = None;
self.last_track_meter_publish = None;
self.hw_out_peak_hold_linear.fill(0.0);
#[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))]
{
self.last_hw_out_meter_linear.clear();
}
let hw_channels = self.latest_hw_out_meter_db.len();
self.latest_hw_out_meter_db = Arc::new(vec![-90.0; hw_channels]);
let tracks: Vec<(String, Arc<UnsafeMutex<Box<Track>>>)> = self
.state
.lock()
.tracks
.iter()
.map(|(name, track)| (name.clone(), track.clone()))
.collect();
self.track_meter_linear_by_track.clear();
let mut snapshot = Vec::with_capacity(tracks.len());
for (name, track) in tracks {
let t = track.lock();
t.clear_output_meters();
let width = t.output_meter_linear().len();
let zero_linear = vec![0.0; width];
self.track_meter_linear_by_track
.insert(name.clone(), zero_linear);
snapshot.push((name, vec![-90.0; width]));
}
self.latest_track_meter_snapshot = Arc::new(snapshot);
}
pub fn check_if_leads_to_kind(
&self,
kind: Kind,
current_track_name: &str,
target_track_name: &str,
) -> bool {
routing::would_create_cycle(
&target_track_name.to_string(),
¤t_track_name.to_string(),
|track_name| self.connected_neighbors(kind, track_name),
)
}
fn connected_neighbors(&self, kind: Kind, current_track_name: &str) -> Vec<String> {
let state = self.state.lock();
let mut found_neighbors = Vec::new();
if let Some(current_track_handle) = state.tracks.get(current_track_name) {
let current_track = current_track_handle.lock();
match kind {
Kind::Audio => {
for out_port in ¤t_track.audio.outs {
let conns = out_port.connections.lock();
for conn in conns.iter() {
for (name, next_track_handle) in &state.tracks {
let next_track = next_track_handle.lock();
let is_connected =
next_track.audio.ins.iter().any(|ins_port| {
Arc::ptr_eq(&ins_port.buffer, &conn.buffer)
});
if is_connected {
found_neighbors.push(name.clone());
}
}
}
}
}
Kind::MIDI => {
for out_port in ¤t_track.midi.outs {
let conns = out_port.lock().connections.clone();
for conn in conns.iter() {
for (name, next_track_handle) in &state.tracks {
let next_track = next_track_handle.lock();
let is_connected = next_track
.midi
.ins
.iter()
.any(|ins_port| Arc::ptr_eq(ins_port, conn));
if is_connected {
found_neighbors.push(name.clone());
}
}
}
}
}
}
}
found_neighbors
}
async fn handle_request(&mut self, a: Action) {
match a {
Action::Log { source, message } => {
self.notify_clients(Ok(Action::Log { source, message }))
.await;
}
Action::Undo => {
let actions = match self.history.undo() {
Some(actions) => actions,
None => {
self.notify_clients(Ok(Action::Undo)).await;
self.notify_clients(Ok(Action::HistoryState {
dirty: self.history.is_dirty(),
}))
.await;
return;
}
};
let was_suspended = self.history_suspended;
self.history_suspended = true;
for action in actions {
self.handle_request_inner(action, false).await;
}
self.history_suspended = was_suspended;
self.notify_clients(Ok(Action::Undo)).await;
self.notify_clients(Ok(Action::HistoryState {
dirty: self.history.is_dirty(),
}))
.await;
}
Action::Redo => {
let actions = match self.history.redo() {
Some(actions) => actions,
None => {
self.notify_clients(Ok(Action::Redo)).await;
self.notify_clients(Ok(Action::HistoryState {
dirty: self.history.is_dirty(),
}))
.await;
return;
}
};
let was_suspended = self.history_suspended;
self.history_suspended = true;
for action in actions {
self.handle_request_inner(action, false).await;
}
self.history_suspended = was_suspended;
self.notify_clients(Ok(Action::Redo)).await;
self.notify_clients(Ok(Action::HistoryState {
dirty: self.history.is_dirty(),
}))
.await;
}
Action::ApplyGroupedActions(actions) => {
self.handle_request_inner(Action::BeginHistoryGroup, true)
.await;
for action in actions {
self.handle_request_inner(action, true).await;
}
self.handle_request_inner(Action::EndHistoryGroup, true)
.await;
}
other => {
self.handle_request_inner(other, true).await;
}
}
}
fn find_audio_io_owner(
&self,
state: &crate::state::State,
io: &std::sync::Arc<crate::audio::io::AudioIO>,
) -> Option<(String, usize)> {
for (name, track) in &state.tracks {
let t = track.lock();
for (i, out) in t.audio.outs.iter().enumerate() {
if std::sync::Arc::ptr_eq(out, io) {
return Some((name.clone(), i));
}
}
for (i, inp) in t.audio.ins.iter().enumerate() {
if std::sync::Arc::ptr_eq(inp, io) {
return Some((name.clone(), i));
}
}
}
None
}
fn find_midi_io_owner(
&self,
state: &crate::state::State,
io: &std::sync::Arc<crate::mutex::UnsafeMutex<Box<crate::midi::io::MIDIIO>>>,
) -> Option<(String, usize, bool)> {
for (name, track) in &state.tracks {
let t = track.lock();
for (i, out) in t.midi.outs.iter().enumerate() {
if std::sync::Arc::ptr_eq(out, io) {
return Some((name.clone(), i, false));
}
}
for (i, inp) in t.midi.ins.iter().enumerate() {
if std::sync::Arc::ptr_eq(inp, io) {
return Some((name.clone(), i, true));
}
}
}
None
}
fn collect_descendant_track_names(&self, name: &str, out: &mut Vec<String>) {
let child_arcs: Vec<Arc<UnsafeMutex<Box<Track>>>> = {
let state = self.state.lock();
if let Some(track) = state.tracks.get(name) {
track.lock().child_tracks.clone()
} else {
Vec::new()
}
};
for child in child_arcs {
let child_name = { child.lock().name.clone() };
self.collect_descendant_track_names(&child_name, out);
out.push(child_name);
}
}
async fn remove_single_track(&mut self, name: &str) {
let children: Vec<Arc<UnsafeMutex<Box<Track>>>> = {
let state = self.state.lock();
if let Some(removed) = state.tracks.get(name).cloned() {
removed.lock().child_tracks.clone()
} else {
Vec::new()
}
};
let parent_name: Option<String> = {
let state = self.state.lock();
state
.tracks
.get(name)
.map(|t| t.lock().parent_track.clone())
.unwrap_or(None)
};
if let Some(parent_name) = parent_name {
let state = self.state.lock();
if let Some(parent) = state.tracks.get(&parent_name).cloned() {
let parent = parent.lock();
parent.child_tracks.retain(|c| c.lock().name != *name);
}
}
if let Some(removed_track) = self.state.lock().tracks.get(name).cloned() {
for child in children {
let removed = removed_track.lock();
child.lock().disconnect_from_parent(removed);
child.lock().parent_track = None;
}
}
self.state.lock().tracks.remove(name);
self.audio_recordings.remove(name);
self.midi_recordings.remove(name);
self.midi_hw_in_routes.retain(|r| r.to_track != *name);
self.midi_hw_out_routes.retain(|r| r.from_track != *name);
if self
.pending_midi_learn
.as_ref()
.is_some_and(|(track_name, _, _)| track_name == name)
{
self.pending_midi_learn = None;
}
}
async fn handle_request_inner(&mut self, mut action_to_process: Action, record_history: bool) {
let a = action_to_process.clone();
let suppress_timing_history = self.playing
&& matches!(
&action_to_process,
Action::SetTempo(_) | Action::SetTimeSignature { .. }
);
let mut extra_inverse_actions: Vec<Action> = Vec::new();
if record_history
&& !self.history_suspended
&& let Action::RemoveTrack(ref track_name) = action_to_process
{
for route in self
.midi_hw_in_routes
.iter()
.filter(|route| &route.to_track == track_name)
{
extra_inverse_actions.push(Action::Connect {
from_track: format!("midi:hw:in:{}", route.device),
from_port: 0,
to_track: route.to_track.clone(),
to_port: route.to_port,
kind: Kind::MIDI,
});
}
for route in self
.midi_hw_out_routes
.iter()
.filter(|route| &route.from_track == track_name)
{
extra_inverse_actions.push(Action::Connect {
from_track: route.from_track.clone(),
from_port: route.from_port,
to_track: format!("midi:hw:out:{}", route.device),
to_port: 0,
kind: Kind::MIDI,
});
}
}
if record_history
&& !self.history_suspended
&& matches!(action_to_process, Action::ClearAllMidiLearnBindings)
{
if let Some(binding) = self.global_midi_learn_play_pause.clone() {
extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
target: crate::message::GlobalMidiLearnTarget::PlayPause,
binding: Some(binding),
});
}
if let Some(binding) = self.global_midi_learn_stop.clone() {
extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
target: crate::message::GlobalMidiLearnTarget::Stop,
binding: Some(binding),
});
}
if let Some(binding) = self.global_midi_learn_record_toggle.clone() {
extra_inverse_actions.push(Action::SetGlobalMidiLearnBinding {
target: crate::message::GlobalMidiLearnTarget::RecordToggle,
binding: Some(binding),
});
}
}
let mut inverse_actions = if record_history
&& !suppress_timing_history
&& should_record(&action_to_process)
&& !self.history_suspended
{
let state = self.state.lock();
create_inverse_actions(&action_to_process, state).map(|mut actions| {
actions.extend(extra_inverse_actions);
actions
})
} else {
None
};
if record_history && !suppress_timing_history && !self.history_suspended {
match &action_to_process {
Action::SetTempo(_) => {
inverse_actions = Some(vec![Action::SetTempo(self.tempo_bpm)]);
}
Action::SetLoopEnabled(_) => {
inverse_actions = Some(vec![Action::SetLoopEnabled(self.loop_enabled)]);
}
Action::SetLoopRange(_) => {
inverse_actions = Some(vec![
Action::SetLoopRange(self.loop_range_samples),
Action::SetLoopEnabled(self.loop_enabled),
]);
}
Action::SetPunchEnabled(_) => {
inverse_actions = Some(vec![Action::SetPunchEnabled(self.punch_enabled)]);
}
Action::SetPunchRange(_) => {
inverse_actions = Some(vec![
Action::SetPunchRange(self.punch_range_samples),
Action::SetPunchEnabled(self.punch_enabled),
]);
}
Action::SetMetronomeEnabled(_) => {
inverse_actions =
Some(vec![Action::SetMetronomeEnabled(self.metronome_enabled)]);
}
Action::SetTimeSignature { .. } => {
inverse_actions = Some(vec![Action::SetTimeSignature {
numerator: self.tsig_num,
denominator: self.tsig_denom,
}]);
}
Action::SetClipPlaybackEnabled(_) => {
inverse_actions = Some(vec![Action::SetClipPlaybackEnabled(
self.clip_playback_enabled,
)]);
}
Action::SetRecordEnabled(_) => {
inverse_actions = Some(vec![Action::SetRecordEnabled(self.record_enabled)]);
}
Action::SetGlobalMidiLearnBinding { target, .. } => {
let binding = match target {
crate::message::GlobalMidiLearnTarget::PlayPause => {
self.global_midi_learn_play_pause.clone()
}
crate::message::GlobalMidiLearnTarget::Stop => {
self.global_midi_learn_stop.clone()
}
crate::message::GlobalMidiLearnTarget::RecordToggle => {
self.global_midi_learn_record_toggle.clone()
}
};
inverse_actions = Some(vec![Action::SetGlobalMidiLearnBinding {
target: *target,
binding,
}]);
}
_ => {}
}
}
match action_to_process {
Action::Play => {
tracing::debug!(
"Action::Play pressed, transport_sample={}",
self.transport_sample
);
self.playing = true;
self.transport_restart_pending = true;
self.notified_loop_wrap_sample = None;
self.invalidate_track_cycle_state();
if let Some(driver) = self.hw_driver.as_mut() {
driver.lock().set_playing(true);
}
#[cfg(unix)]
if let Some(jack) = &self.jack_runtime
&& let Err(e) = jack.lock().transport_start()
{
self.notify_clients(Err(e)).await;
}
self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
.await;
self.preload_track_clips().await;
{
let echoes = self.apply_modulators(self.transport_sample);
for action in echoes {
self.notify_clients(Ok(action)).await;
}
}
let send_result = self.send_tasks().await;
tracing::debug!("send_tasks after Play returned finished={}", send_result);
if !self.awaiting_hwfinished
&& !self.handling_hwfinished
&& send_result
&& self.hw_worker.is_some()
{
self.transport_restart_pending = false;
self.request_hw_cycle().await;
}
}
Action::Pause => {
self.clip_playback_enabled = false;
for track in self.state.lock().tracks.values() {
track.lock().set_clip_playback_enabled(false);
}
if !self.playing {
self.playing = true;
self.transport_restart_pending = true;
self.notified_loop_wrap_sample = None;
self.invalidate_track_cycle_state();
if let Some(driver) = self.hw_driver.as_mut() {
driver.lock().set_playing(true);
}
#[cfg(unix)]
if let Some(jack) = &self.jack_runtime
&& let Err(e) = jack.lock().transport_start()
{
self.notify_clients(Err(e)).await;
}
self.preload_track_clips().await;
if !self.awaiting_hwfinished
&& !self.handling_hwfinished
&& self.send_tasks().await
&& self.hw_worker.is_some()
{
self.transport_restart_pending = false;
self.request_hw_cycle().await;
}
}
self.notify_clients(Ok(Action::Pause)).await;
self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
.await;
}
Action::Stop => {
self.playing = false;
self.transport_panic_flush_pending = false;
self.transport_restart_pending = false;
self.notified_loop_wrap_sample = None;
self.invalidate_track_cycle_state();
if let Some(driver) = self.hw_driver.as_mut() {
driver.lock().set_playing(false);
}
#[cfg(unix)]
if let Some(jack) = &self.jack_runtime
&& let Err(e) = jack.lock().transport_stop()
{
self.notify_clients(Err(e)).await;
}
let panic_events = self.note_off_events_for_all_active_tracks();
if let Some(worker) = &self.hw_worker {
if !panic_events.is_empty()
&& let Err(e) = worker.tx.send(Message::HWMidiOutEvents(panic_events)).await
{
error!("Error sending stop MIDI panic events {e}");
}
} else {
self.pending_hw_midi_out_events_by_device
.extend(panic_events);
}
self.reset_meters_after_stop();
self.flush_recordings().await;
self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
.await;
}
Action::JumpToEnd => {
self.transport_sample = self.normalize_transport_sample(self.session_end_sample());
self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
.await;
}
Action::Panic => {
let panic_events = self.panic_events_for_all_hw_midi_outputs();
if let Some(worker) = &self.hw_worker {
if !panic_events.is_empty() {
if let Err(e) = worker.tx.send(Message::ClearHWMidiOutEvents).await {
error!("Error clearing HW MIDI queue for panic {e}");
}
self.midi_hub
.lock()
.write_events_blocking(&panic_events, Duration::from_millis(250));
}
} else if !panic_events.is_empty() {
self.pending_hw_midi_out_events_by_device
.extend(panic_events);
}
}
Action::SetClipPlaybackEnabled(enabled) => {
self.clip_playback_enabled = enabled;
for track in self.state.lock().tracks.values() {
track.lock().set_clip_playback_enabled(enabled);
}
}
Action::TransportPosition(sample) => {
self.transport_sample = self.normalize_transport_sample(sample);
self.notified_loop_wrap_sample = None;
{
let echoes = self.apply_modulators(self.transport_sample);
for action in echoes {
self.notify_clients(Ok(action)).await;
}
}
#[cfg(unix)]
if let Some(jack) = &self.jack_runtime
&& let Err(e) = jack.lock().transport_locate(self.transport_sample)
{
self.notify_clients(Err(e)).await;
}
if self.playing {
self.transport_restart_pending = true;
self.invalidate_track_cycle_state();
self.transport_panic_flush_pending = self.hw_worker.is_some();
self.clear_hw_midi_output_state(true).await;
if !self.awaiting_hwfinished && !self.handling_hwfinished {
if self.hw_worker.is_some() {
self.request_hw_cycle().await;
} else if self.send_tasks().await {
self.transport_restart_pending = false;
self.request_hw_cycle().await;
}
}
}
}
Action::SetLoopEnabled(enabled) => {
self.loop_enabled = enabled && self.loop_range_samples.is_some();
self.notified_loop_wrap_sample = None;
}
Action::SetLoopRange(range) => {
self.loop_range_samples = range.and_then(|(start, end)| {
if end > start {
Some((start, end))
} else {
None
}
});
self.loop_enabled = self.loop_range_samples.is_some();
self.notified_loop_wrap_sample = None;
if self.loop_enabled
&& let Some((loop_start, loop_end)) = self.loop_range_samples
&& self.transport_sample >= loop_end
{
self.transport_sample = loop_start;
self.notify_clients(Ok(Action::TransportPosition(self.transport_sample)))
.await;
}
}
Action::SetPunchEnabled(enabled) => {
self.punch_enabled = enabled && self.punch_range_samples.is_some();
}
Action::SetPunchRange(range) => {
self.punch_range_samples = range.and_then(|(start, end)| {
if end > start {
Some((start, end))
} else {
None
}
});
self.punch_enabled = self.punch_range_samples.is_some();
}
Action::SetMetronomeEnabled(enabled) => {
self.metronome_enabled = enabled;
if enabled {
self.ensure_metronome_track().await;
}
if let Some(track) = self.state.lock().tracks.get(Self::METRONOME_TRACK).cloned() {
track.lock().set_metronome_enabled(enabled);
}
}
Action::SetTempo(bpm) => {
self.tempo_bpm = bpm.max(1.0);
}
Action::SetTimeSignature {
numerator,
denominator,
} => {
self.tsig_num = numerator.max(1);
self.tsig_denom = denominator.max(1);
}
Action::SetOscEnabled(enabled) => {
if let Err(err) = self.set_osc_enabled_with(enabled, OscServer::start) {
self.notify_clients(Err(err)).await;
}
}
Action::SetRecordEnabled(enabled) => {
self.record_enabled = enabled;
if !enabled {
if self.awaiting_hwfinished {
self.append_recorded_cycle();
}
self.flush_recordings().await;
} else if self.session_dir.is_none() {
self.notify_clients(Err(
"Recording enabled but session path is not set".to_string()
))
.await;
}
}
Action::SetModulators(ref modulators) => {
self.modulators = modulators.clone();
let echoes = self.apply_modulators(self.transport_sample);
for action in echoes {
self.notify_clients(Ok(action)).await;
}
}
Action::SetStepRecording(enabled) => {
self.step_recording_enabled = enabled;
}
Action::BeginHistoryGroup if self.history_group.is_none() => {
self.history_group = Some(UndoEntry {
forward_actions: vec![],
inverse_actions: vec![],
});
}
Action::EndHistoryGroup => {
if let Some(mut group) = self.history_group.take()
&& !group.forward_actions.is_empty()
&& !group.inverse_actions.is_empty()
{
let mut add_tracks = Vec::new();
let mut connections = Vec::new();
let mut rest = Vec::new();
for action in group.inverse_actions {
if matches!(action, Action::AddTrack { .. }) {
add_tracks.push(action);
} else if matches!(action, Action::Connect { .. }) {
connections.push(action);
} else {
rest.push(action);
}
}
group.inverse_actions = add_tracks;
group.inverse_actions.extend(rest);
group.inverse_actions.extend(connections);
self.history.record(group);
}
}
Action::SetSessionPath(ref path) => {
self.session_dir = Some(Path::new(path).to_path_buf());
self.ensure_session_subdirs();
#[cfg(all(unix, not(target_os = "macos")))]
let _lv2_dir = self.session_plugins_dir();
for track in self.state.lock().tracks.values() {
track.lock().set_session_base_dir(self.session_dir.clone());
}
}
Action::MarkHistorySavePoint => {
self.history.mark_save_point();
self.notify_clients(Ok(Action::HistoryState {
dirty: self.history.is_dirty(),
}))
.await;
}
Action::ClearHistory => {
self.history.clear();
self.history.mark_save_point();
}
Action::BeginSessionRestore => {
self.history_suspended = true;
self.history.clear();
}
Action::EndSessionRestore => {
self.history.clear();
self.history_suspended = false;
self.preload_track_clips_spawn();
}
Action::Quit => {
self.flush_recordings().await;
if let Some(worker) = self.hw_worker.take() {
if let Some(hw) = &self.hw_driver {
hw.lock().request_stop();
}
let panic_events = self.panic_events_for_all_hw_midi_outputs();
if !panic_events.is_empty() {
let _ = worker.tx.send(Message::HWMidiOutEvents(panic_events)).await;
}
if let Err(e) = worker.tx.send(Message::Request(a.clone())).await {
error!("Error sending quit message to HW worker: {e}");
}
worker
.handle
.await
.unwrap_or_else(|e| error!("Error waiting for HW worker to quit: {e}"));
}
if let Some(hw) = &self.hw_driver {
hw.lock().close_fds();
}
self.midi_hub.lock().close_all();
self.hw_driver = None;
self.notify_clients(Ok(Action::Quit)).await;
self.ready_workers.clear();
while !self.workers.is_empty() {
let worker = self.workers.remove(0);
if let Err(e) = worker.tx.send(Message::Request(a.clone())).await {
error!("Error sending quit message to worker: {e}");
}
worker
.handle
.await
.unwrap_or_else(|e| error!("Error waiting for worker to quit: {e}"));
}
#[cfg(unix)]
{
self.jack_runtime = None;
}
self.osc_server = None;
return;
}
Action::AddTrack {
ref name,
audio_ins,
midi_ins,
audio_outs,
midi_outs,
folder,
} => {
let tracks = &mut self.state.lock().tracks;
if tracks.contains_key(name) {
self.notify_clients(Err(format!("Track {} already exists", name)))
.await;
return;
}
let maybe_hw = if let Some(oss) = &self.hw_driver {
let hw = oss.lock();
Some((hw.cycle_samples(), hw.sample_rate() as f64))
} else {
#[cfg(unix)]
if let Some(jack) = &self.jack_runtime {
let j = jack.lock();
Some((j.buffer_size, j.sample_rate as f64))
} else {
None
}
#[cfg(not(unix))]
None
};
if let Some((chsamples, sample_rate)) = maybe_hw {
let track = if folder {
Track::new_folder(
name.clone(),
audio_ins,
audio_outs,
midi_ins,
midi_outs,
chsamples,
sample_rate,
)
} else {
Track::new(
name.clone(),
audio_ins,
audio_outs,
midi_ins,
midi_outs,
chsamples,
sample_rate,
)
};
tracks.insert(name.clone(), Arc::new(UnsafeMutex::new(Box::new(track))));
if let Some(track) = tracks.get(name) {
let t = track.lock();
t.set_clip_playback_enabled(self.clip_playback_enabled);
t.set_transport_timing(self.tempo_bpm, self.tsig_num, self.tsig_denom);
t.set_session_base_dir(self.session_dir.clone());
}
} else {
self.notify_clients(Err(
"Engine needs to open audio device before adding audio track".to_string(),
))
.await;
}
}
Action::TrackAddAudioInput(ref name) => {
let track = match self.track_handle_or_err(name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
if let Err(e) = track.lock().add_audio_input() {
self.notify_clients(Err(e)).await;
return;
}
}
Action::TrackAddAudioOutput(ref name) => {
let track = match self.track_handle_or_err(name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
if let Err(e) = track.lock().add_audio_output() {
self.notify_clients(Err(e)).await;
return;
}
}
Action::TrackRemoveAudioInput(ref name) => {
let track = match self.track_handle_or_err(name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
if let Err(e) = track.lock().remove_audio_input() {
self.notify_clients(Err(e)).await;
return;
}
}
Action::TrackRemoveAudioOutput(ref name) => {
let track = match self.track_handle_or_err(name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
let (hw_outputs, track_inputs) = {
let state = self.state.lock();
let hw_outputs = self.all_hw_output_audio_ports();
let track_inputs = state
.tracks
.iter()
.filter(|(track_name, _)| *track_name != name)
.flat_map(|(_, handle)| handle.lock().audio.ins.clone())
.collect::<Vec<_>>();
(hw_outputs, track_inputs)
};
if let Err(e) = track.lock().remove_audio_output(&hw_outputs, &track_inputs) {
self.notify_clients(Err(e)).await;
return;
}
}
Action::RenameTrack {
ref old_name,
ref new_name,
} => {
if self.state.lock().tracks.contains_key(new_name) {
self.notify_clients(Err(format!("Track '{}' already exists", new_name)))
.await;
return;
}
let Some(track) = self.state.lock().tracks.remove(old_name) else {
self.notify_clients(Err(format!("Track '{}' not found", old_name)))
.await;
return;
};
track.lock().name = new_name.clone();
self.state.lock().tracks.insert(new_name.clone(), track);
for other in self.state.lock().tracks.values() {
let other = other.lock();
if other.parent_track.as_deref() == Some(old_name.as_str()) {
other.parent_track = Some(new_name.clone());
}
}
if let Some(recording) = self.audio_recordings.remove(old_name) {
self.audio_recordings.insert(new_name.clone(), recording);
}
if let Some(recording) = self.midi_recordings.remove(old_name) {
self.midi_recordings.insert(new_name.clone(), recording);
}
for route in &mut self.midi_hw_in_routes {
if route.to_track == *old_name {
route.to_track = new_name.clone();
}
}
for route in &mut self.midi_hw_out_routes {
if route.from_track == *old_name {
route.from_track = new_name.clone();
}
}
if let Some((armed_track, target, device)) = self.pending_midi_learn.clone()
&& armed_track == *old_name
{
self.pending_midi_learn = Some((new_name.clone(), target, device));
}
self.notify_clients(Ok(Action::RenameTrack {
old_name: old_name.clone(),
new_name: new_name.clone(),
}))
.await;
}
Action::RemoveTrack(ref name) => {
let mut descendant_names = Vec::new();
self.collect_descendant_track_names(name, &mut descendant_names);
let names_to_remove: Vec<String> = descendant_names
.iter()
.cloned()
.chain(std::iter::once(name.clone()))
.collect();
let combined_inverse = if record_history && !self.history_suspended {
let state = self.state.lock();
let mut inv = Vec::new();
for n in &names_to_remove {
if let Some(mut actions) =
create_inverse_actions(&Action::RemoveTrack(n.clone()), state)
{
inv.append(&mut actions);
}
for route in self.midi_hw_in_routes.iter().filter(|r| &r.to_track == n) {
inv.push(Action::Connect {
from_track: format!("midi:hw:in:{}", route.device),
from_port: 0,
to_track: route.to_track.clone(),
to_port: route.to_port,
kind: Kind::MIDI,
});
}
for route in self
.midi_hw_out_routes
.iter()
.filter(|r| &r.from_track == n)
{
inv.push(Action::Connect {
from_track: route.from_track.clone(),
from_port: route.from_port,
to_track: format!("midi:hw:out:{}", route.device),
to_port: 0,
kind: Kind::MIDI,
});
}
}
let mut add_tracks = Vec::new();
let mut connections = Vec::new();
let mut rest = Vec::new();
for action in inv {
match action {
Action::AddTrack { .. } => add_tracks.push(action),
Action::Connect { .. } => connections.push(action),
_ => rest.push(action),
}
}
let mut ordered = add_tracks;
ordered.extend(rest);
ordered.extend(connections);
ordered
} else {
Vec::new()
};
for n in &descendant_names {
self.remove_single_track(n).await;
self.notify_clients(Ok(Action::RemoveTrack(n.clone())))
.await;
}
self.remove_single_track(name).await;
if record_history && !self.history_suspended && !combined_inverse.is_empty() {
self.history.record(UndoEntry {
forward_actions: vec![Action::RemoveTrack(name.clone())],
inverse_actions: combined_inverse,
});
}
inverse_actions = None;
}
Action::TrackLevel(ref name, level) => {
if name == "hw:out" {
self.hw_out_level_db = level;
} else if let Some(track) = self.state.lock().tracks.get(name) {
track.lock().set_level(level);
}
}
Action::TrackBalance(ref name, balance) => {
if name == "hw:out" {
self.hw_out_balance = balance.clamp(-1.0, 1.0);
} else if let Some(track) = self.state.lock().tracks.get(name) {
track.lock().set_balance(balance);
}
}
Action::TrackAutomationLevel(ref name, level) => {
tracing::debug!(%name, level, "engine received TrackAutomationLevel");
if name == "hw:out" {
self.hw_out_level_db = level;
} else if let Some(track) = self.state.lock().tracks.get(name) {
track.lock().set_level(level);
}
}
Action::TrackAutomationBalance(ref name, balance) => {
if name == "hw:out" {
self.hw_out_balance = balance.clamp(-1.0, 1.0);
} else if let Some(track) = self.state.lock().tracks.get(name) {
track.lock().set_balance(balance);
}
}
Action::TrackMidiCc {
ref track_name,
channel,
cc,
value,
} => {
if let Some(track) = self.state.lock().tracks.get(track_name) {
track
.lock()
.pending_automation_midi_events
.push(MidiEvent::new(
0,
vec![0xB0 | channel.min(15), cc.min(127), value.min(127)],
));
}
}
Action::RequestMeterSnapshot => {
self.notify_clients(Ok(Action::MeterSnapshot {
hw_out_db: self.latest_hw_out_meter_db.clone(),
track_meters: self.latest_track_meter_snapshot.clone(),
}))
.await;
return;
}
Action::TrackMeters { .. } => {}
Action::MeterSnapshot { .. } => {}
Action::TrackToggleArm(ref name) => {
if self.reject_if_track_frozen(name, "arming/disarming").await {
return;
}
if let Some(track) = self.state.lock().tracks.get(name).cloned() {
track.lock().arm();
let armed = track.lock().armed;
if !armed && self.audio_recordings.contains_key(name) {
self.flush_track_recording(name).await;
}
} else {
tracing::warn!(
"TrackToggleArm for '{}' but track not found in engine",
name
);
}
}
Action::TrackToggleMute(ref name) => {
if name == "hw:out" {
self.hw_out_muted = !self.hw_out_muted;
} else if let Some(track) = self.state.lock().tracks.get(name) {
track.lock().mute();
}
}
Action::TrackTogglePhase(ref name) => {
if let Some(track) = self.state.lock().tracks.get(name) {
track.lock().invert_phase();
}
}
Action::TrackToggleSolo(ref name) => {
if name == "hw:out" {
return;
}
if let Some(track) = self.state.lock().tracks.get(name) {
track.lock().solo();
}
}
Action::TrackToggleMaster(ref name) => {
if let Some(track) = self.state.lock().tracks.get(name) {
track.lock().toggle_master();
}
}
Action::TrackToggleInputMonitor {
ref track_name,
lane,
} => {
if let Some(track) = self.state.lock().tracks.get(track_name) {
track.lock().toggle_input_monitor(lane);
}
}
Action::TrackToggleDiskMonitor {
ref track_name,
lane,
} => {
if let Some(track) = self.state.lock().tracks.get(track_name) {
track.lock().toggle_disk_monitor(lane);
}
}
Action::TrackToggleMidiInputMonitor {
ref track_name,
lane,
} => {
if let Some(track) = self.state.lock().tracks.get(track_name) {
track.lock().toggle_midi_input_monitor(lane);
}
}
Action::TrackToggleMidiDiskMonitor {
ref track_name,
lane,
} => {
if let Some(track) = self.state.lock().tracks.get(track_name) {
track.lock().toggle_midi_disk_monitor(lane);
}
}
Action::TrackSetColor {
ref track_name,
color,
} => {
if let Some(track) = self.state.lock().tracks.get(track_name) {
track.lock().color = color;
}
}
Action::TrackArmMidiLearn {
ref track_name,
target,
} => {
if let Err(e) = self.track_handle_or_err(track_name) {
self.notify_clients(Err(e)).await;
return;
}
self.pending_midi_learn = Some((track_name.clone(), target, None));
}
Action::GlobalArmMidiLearn { target } => {
self.pending_global_midi_learn = Some(target);
}
Action::TrackSetMidiLearnBinding {
ref track_name,
target,
ref binding,
} => {
if let Some(binding) = binding.as_ref() {
let conflicts = self.midi_learn_slot_conflicts(
binding,
Some(MidiLearnSlot::Track(track_name.clone(), target)),
);
if !conflicts.is_empty() {
self.notify_clients(Err(format!(
"MIDI learn conflict for '{}' {:?}: {}",
track_name,
target,
conflicts.join(", ")
)))
.await;
return;
}
}
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
match target {
crate::message::TrackMidiLearnTarget::Volume => {
track.lock().midi_learn_volume = binding.clone();
}
crate::message::TrackMidiLearnTarget::Balance => {
track.lock().midi_learn_balance = binding.clone();
}
crate::message::TrackMidiLearnTarget::Mute => {
track.lock().midi_learn_mute = binding.clone();
}
crate::message::TrackMidiLearnTarget::Solo => {
track.lock().midi_learn_solo = binding.clone();
}
crate::message::TrackMidiLearnTarget::Arm => {
track.lock().midi_learn_arm = binding.clone();
}
crate::message::TrackMidiLearnTarget::InputMonitor => {
track.lock().midi_learn_input_monitor = binding.clone();
}
crate::message::TrackMidiLearnTarget::DiskMonitor => {
track.lock().midi_learn_disk_monitor = binding.clone();
}
}
}
Action::SetGlobalMidiLearnBinding {
target,
ref binding,
} => {
if let Some(binding) = binding.as_ref() {
let conflicts = self
.midi_learn_slot_conflicts(binding, Some(MidiLearnSlot::Global(target)));
if !conflicts.is_empty() {
self.notify_clients(Err(format!(
"Global MIDI learn conflict for {:?}: {}",
target,
conflicts.join(", ")
)))
.await;
return;
}
}
match target {
crate::message::GlobalMidiLearnTarget::PlayPause => {
self.global_midi_learn_play_pause = binding.clone();
}
crate::message::GlobalMidiLearnTarget::Stop => {
self.global_midi_learn_stop = binding.clone();
}
crate::message::GlobalMidiLearnTarget::RecordToggle => {
self.global_midi_learn_record_toggle = binding.clone();
}
}
}
Action::TrackSetFolder {
ref track_name,
is_folder,
} => {
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
{
let track = track.lock();
track.is_folder = is_folder;
track.ensure_default_audio_passthrough();
track.ensure_default_midi_passthrough();
}
self.notify_clients(Ok(Action::TrackSetFolder {
track_name: track_name.clone(),
is_folder,
}))
.await;
}
Action::TrackSetParent {
ref track_name,
ref parent_name,
} => {
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
if parent_name.as_deref() == Some(track_name.as_str()) {
self.notify_clients(Err("Track cannot be its own parent".to_string()))
.await;
return;
}
if let Some(parent_name) = parent_name {
let state = self.state.lock();
let parent = state.tracks.get(parent_name);
if parent.is_none() {
self.notify_clients(Err(format!(
"Parent track '{}' does not exist",
parent_name
)))
.await;
return;
}
if !parent.unwrap().lock().is_folder {
self.notify_clients(Err(format!(
"Track '{}' is not a folder",
parent_name
)))
.await;
return;
}
}
{
let old_parent_name = track.lock().parent_track.clone();
if let Some(old_parent_name) = old_parent_name {
let state = self.state.lock();
if let (Some(parent_arc), Some(child_arc)) = (
state.tracks.get(&old_parent_name).cloned(),
state.tracks.get(track_name).cloned(),
) {
{
let parent = parent_arc.lock();
parent.child_tracks.retain(|c| c.lock().name != *track_name);
}
{
let child = child_arc.lock();
let parent = parent_arc.lock();
child.disconnect_from_parent(parent);
}
}
}
}
let mut disconnect_actions = Vec::new();
{
let state = self.state.lock();
let hw_inputs = self.all_hw_input_audio_ports();
let hw_outputs = self.all_hw_output_audio_ports();
if let Some(child_arc) = state.tracks.get(track_name).cloned() {
let child = child_arc.lock();
for (port_idx, inp) in child.audio.ins.iter().enumerate() {
let sources = inp.connections.lock().clone();
for src in sources {
let _ = AudioIO::disconnect(&src, inp);
if let Some((src_name, src_port)) =
self.find_audio_io_owner(state, &src)
{
disconnect_actions.push(Action::Disconnect {
from_track: src_name,
from_port: src_port,
to_track: track_name.clone(),
to_port: port_idx,
kind: Kind::Audio,
});
} else if let Some(src_port) = hw_inputs
.iter()
.position(|hw_in| std::sync::Arc::ptr_eq(hw_in, &src))
{
disconnect_actions.push(Action::Disconnect {
from_track: "hw:in".to_string(),
from_port: src_port,
to_track: track_name.clone(),
to_port: port_idx,
kind: Kind::Audio,
});
}
}
}
for (port_idx, out) in child.audio.outs.iter().enumerate() {
let targets = out.connections.lock().clone();
for tgt in targets {
let _ = AudioIO::disconnect(out, &tgt);
if let Some((tgt_name, tgt_port)) =
self.find_audio_io_owner(state, &tgt)
{
disconnect_actions.push(Action::Disconnect {
from_track: track_name.clone(),
from_port: port_idx,
to_track: tgt_name,
to_port: tgt_port,
kind: Kind::Audio,
});
} else if let Some(tgt_port) = hw_outputs
.iter()
.position(|hw_out| std::sync::Arc::ptr_eq(hw_out, &tgt))
{
disconnect_actions.push(Action::Disconnect {
from_track: track_name.clone(),
from_port: port_idx,
to_track: "hw:out".to_string(),
to_port: tgt_port,
kind: Kind::Audio,
});
}
}
}
for route in self
.midi_hw_in_routes
.iter()
.filter(|r| r.to_track == *track_name)
{
disconnect_actions.push(Action::Disconnect {
from_track: format!("midi:hw:in:{}", route.device),
from_port: 0,
to_track: track_name.clone(),
to_port: route.to_port,
kind: Kind::MIDI,
});
}
self.midi_hw_in_routes.retain(|r| r.to_track != *track_name);
for route in self
.midi_hw_out_routes
.iter()
.filter(|r| r.from_track == *track_name)
{
disconnect_actions.push(Action::Disconnect {
from_track: track_name.clone(),
from_port: route.from_port,
to_track: format!("midi:hw:out:{}", route.device),
to_port: 0,
kind: Kind::MIDI,
});
}
self.midi_hw_out_routes
.retain(|r| r.from_track != *track_name);
for (port_idx, out) in child.midi.outs.iter().enumerate() {
let targets = out.lock().connections.clone();
for tgt in targets {
if let Some((tgt_name, tgt_port, _)) =
self.find_midi_io_owner(state, &tgt)
{
let _ = MIDIIO::disconnect(out, &tgt);
disconnect_actions.push(Action::Disconnect {
from_track: track_name.clone(),
from_port: port_idx,
to_track: tgt_name,
to_port: tgt_port,
kind: Kind::MIDI,
});
}
}
}
}
let child_input_arcs: Vec<_> =
if let Some(child_arc) = state.tracks.get(track_name).cloned() {
let child = child_arc.lock();
child.midi.ins.clone()
} else {
Vec::new()
};
for (other_name, other_track) in &state.tracks {
if other_name == track_name {
continue;
}
let other = other_track.lock();
for (out_port, out) in other.midi.outs.iter().enumerate() {
let targets = out.lock().connections.clone();
for tgt in targets {
if let Some(to_port) = child_input_arcs
.iter()
.position(|inp| std::sync::Arc::ptr_eq(inp, &tgt))
{
let _ = MIDIIO::disconnect(out, &tgt);
disconnect_actions.push(Action::Disconnect {
from_track: other_name.clone(),
from_port: out_port,
to_track: track_name.clone(),
to_port,
kind: Kind::MIDI,
});
}
}
}
}
}
{
track.lock().parent_track = parent_name.clone();
}
if let Some(parent_name) = parent_name {
let state = self.state.lock();
if let (Some(parent_arc), Some(child_arc)) = (
state.tracks.get(parent_name).cloned(),
state.tracks.get(track_name).cloned(),
) {
{
let parent = parent_arc.lock();
parent.child_tracks.push(child_arc.clone());
}
{
let child = child_arc.lock();
let parent = parent_arc.lock();
if parent.audio.ins.len() == child.audio.ins.len() {
for (parent_in, child_in) in
parent.audio.ins.iter().zip(child.audio.ins.iter())
{
Track::connect_directed_audio(parent_in, child_in);
}
}
if parent.audio.outs.len() == child.audio.outs.len() {
for (child_out, parent_out) in
child.audio.outs.iter().zip(parent.audio.outs.iter())
{
AudioIO::connect(child_out, parent_out);
}
}
if parent.midi.ins.len() == child.midi.ins.len() {
for (parent_in, child_in) in
parent.midi.ins.iter().zip(child.midi.ins.iter())
{
let child_in_lock = child_in.lock();
if !child_in_lock
.connections
.iter()
.any(|c| Arc::ptr_eq(c, parent_in))
{
child_in_lock.connections.push(parent_in.clone());
}
}
}
if parent.midi.outs.len() == child.midi.outs.len() {
for (child_out, parent_out) in
child.midi.outs.iter().zip(parent.midi.outs.iter())
{
let child_out_lock = child_out.lock();
if !child_out_lock
.connections
.iter()
.any(|c| Arc::ptr_eq(c, parent_out))
{
child_out_lock.connections.push(parent_out.clone());
}
}
}
child.invalidate_audio_route_cache();
parent.invalidate_audio_route_cache();
child.invalidate_midi_route_cache();
parent.invalidate_midi_route_cache();
}
}
}
{
let state = self.state.lock();
if let Some(child_arc) = state.tracks.get(track_name).cloned() {
let child = child_arc.lock();
child.ensure_default_audio_passthrough();
child.ensure_default_midi_passthrough();
}
}
for action in disconnect_actions {
self.notify_clients(Ok(action)).await;
}
self.notify_clients(Ok(Action::TrackSetParent {
track_name: track_name.clone(),
parent_name: parent_name.clone(),
}))
.await;
}
Action::TrackToggleFolder { ref track_name } => {
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
{
let t = track.lock();
t.folder_open = !t.folder_open;
}
self.notify_clients(Ok(Action::TrackToggleFolder {
track_name: track_name.clone(),
}))
.await;
self.notify_clients(Ok(Action::TrackSetFolder {
track_name: track_name.clone(),
is_folder: track.lock().is_folder,
}))
.await;
}
Action::TrackSetMidiLaneChannel {
ref track_name,
lane,
channel,
} => {
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
track.lock().set_midi_lane_channel(lane, channel);
}
Action::TrackSetFrozen {
ref track_name,
frozen,
} => {
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
track.lock().set_frozen(frozen);
}
Action::TrackOfflineBounce {
track_name,
output_path,
start_sample,
length_samples,
automation_lanes,
apply_fader,
} => {
if self.offline_bounce_jobs.contains_key(&track_name) {
self.notify_clients(Err(format!(
"Offline bounce for track '{}' is already in progress",
track_name
)))
.await;
return;
}
if let Err(e) = self.track_handle_or_err(&track_name) {
self.notify_clients(Err(e)).await;
return;
}
if length_samples == 0 {
self.notify_clients(Err(format!(
"Track '{}' has no renderable content for offline bounce",
track_name
)))
.await;
return;
}
let Some(worker_index) = self.take_ready_worker_index() else {
self.pending_requests
.push_front(Action::TrackOfflineBounce {
track_name,
output_path,
start_sample,
length_samples,
automation_lanes,
apply_fader,
});
return;
};
let cancel = Arc::new(AtomicBool::new(false));
self.offline_bounce_jobs.insert(
track_name.clone(),
OfflineBounceJob {
cancel: cancel.clone(),
},
);
let track_name_clone = track_name.clone();
let worker = &self.workers[worker_index];
let job = crate::message::OfflineBounceWork {
state: self.state.clone(),
track_name,
output_path,
start_sample,
length_samples,
tempo_bpm: self.tempo_bpm,
tsig_num: self.tsig_num,
tsig_denom: self.tsig_denom,
automation_lanes,
cancel,
apply_fader,
};
if let Err(e) = worker.tx.send(Message::ProcessOfflineBounce(job)).await {
self.offline_bounce_jobs.remove(&track_name_clone);
self.notify_clients(Err(format!("Failed to schedule offline bounce: {e}")))
.await;
}
return;
}
Action::TrackOfflineBounceCancel { .. } => {}
Action::TrackOfflineBounceCancelAll => {}
Action::TrackOfflineBounceCanceled { .. } => {}
Action::TrackOfflineBounceProgress { .. } => {}
Action::PianoKey {
ref track_name,
note,
velocity,
on,
} => {
if let Some(track) = self.state.lock().tracks.get(track_name) {
let status = if on { 0x90 } else { 0x80 };
let event = MidiEvent::new(0, vec![status, note.min(127), velocity.min(127)]);
track.lock().push_hw_midi_events(&[event]);
}
}
Action::ModifyMidiNotes { .. }
| Action::ModifyMidiControllers { .. }
| Action::DeleteMidiControllers { .. }
| Action::InsertMidiControllers { .. }
| Action::DeleteMidiNotes { .. }
| Action::InsertMidiNotes { .. } => {
if let Err(e) = self.apply_midi_edit_action(&action_to_process) {
self.notify_clients(Err(e)).await;
return;
}
}
Action::SetMidiSysExEvents { .. } => {
if let Err(e) = self.apply_midi_edit_action(&action_to_process) {
self.notify_clients(Err(e)).await;
return;
}
}
Action::TrackClearDefaultPassthrough { ref track_name } => {
if self
.reject_if_track_frozen(track_name, "plugin graph editing")
.await
{
return;
}
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
track.lock().clear_default_passthrough();
}
Action::TrackClearPlugins { ref track_name } => {
if self
.reject_if_track_frozen(track_name, "plugin graph editing")
.await
{
return;
}
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
track.lock().clear_plugins();
self.notify_clients(Ok(Action::Log {
source: "engine".to_string(),
message: format!("Cleared plugins from track '{track_name}'"),
}))
.await;
}
#[cfg(all(unix, not(target_os = "macos")))]
Action::ClipSetLv2PluginState { ref track_name, .. } => {
self.notify_clients(Err(format!(
"Track '{}': clip LV2 plugin state changes are not supported",
track_name
)))
.await;
}
Action::TrackGetClapNoteNames { ref track_name } => {
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
let note_names = track.lock().get_clap_note_names();
self.notify_clients(Ok(Action::TrackClapNoteNames {
track_name: track_name.clone(),
note_names,
}))
.await;
}
Action::TrackGetPluginGraph { ref track_name } => {
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
let (plugins, connections, connectable_connections) = {
let track = track.lock();
(
track.plugin_graph_plugins(),
track.plugin_graph_connections(),
track.connectable_connections(),
)
};
self.notify_clients(Ok(Action::TrackPluginGraph {
track_name: track_name.clone(),
plugins,
connections,
connectable_connections,
}))
.await;
return;
}
Action::TrackPluginGraph { .. } => {}
Action::TrackConnectPluginAudio {
ref track_name,
ref from_node,
from_port,
ref to_node,
to_port,
} => {
if self
.reject_if_track_frozen(track_name, "plugin routing changes")
.await
{
return;
}
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
if let Err(e) = track.lock().connect_plugin_audio(
from_node.clone(),
from_port,
to_node.clone(),
to_port,
) {
self.notify_clients(Err(e)).await;
return;
}
}
Action::TrackConnectPluginMidi {
ref track_name,
ref from_node,
from_port,
ref to_node,
to_port,
} => {
if self
.reject_if_track_frozen(track_name, "plugin routing changes")
.await
{
return;
}
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
if let Err(e) = track.lock().connect_plugin_midi(
from_node.clone(),
from_port,
to_node.clone(),
to_port,
) {
self.notify_clients(Err(e)).await;
return;
}
}
Action::TrackDisconnectPluginAudio {
ref track_name,
ref from_node,
from_port,
ref to_node,
to_port,
} => {
if self
.reject_if_track_frozen(track_name, "plugin routing changes")
.await
{
return;
}
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
if let Err(e) = track.lock().disconnect_plugin_audio(
from_node.clone(),
from_port,
to_node.clone(),
to_port,
) {
self.notify_clients(Err(e)).await;
return;
}
}
Action::TrackDisconnectPluginMidi {
ref track_name,
ref from_node,
from_port,
ref to_node,
to_port,
} => {
if self
.reject_if_track_frozen(track_name, "plugin routing changes")
.await
{
return;
}
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
if let Err(e) = track.lock().disconnect_plugin_midi(
from_node.clone(),
from_port,
to_node.clone(),
to_port,
) {
self.notify_clients(Err(e)).await;
return;
}
}
Action::TrackConnectAudio {
ref track_name,
ref from,
from_port,
ref to,
to_port,
} => {
if self
.reject_if_track_frozen(track_name, "routing changes")
.await
{
return;
}
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
if let Err(e) = track.lock().connect_audio_connectable(
from.clone(),
from_port,
to.clone(),
to_port,
) {
self.notify_clients(Err(e)).await;
return;
}
}
Action::TrackDisconnectAudio {
ref track_name,
ref from,
from_port,
ref to,
to_port,
} => {
if self
.reject_if_track_frozen(track_name, "routing changes")
.await
{
return;
}
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
if let Err(e) = track.lock().disconnect_audio_connectable(
from.clone(),
from_port,
to.clone(),
to_port,
) {
self.notify_clients(Err(e)).await;
return;
}
}
Action::TrackConnectMidi {
ref track_name,
ref from,
from_port,
ref to,
to_port,
} => {
if self
.reject_if_track_frozen(track_name, "routing changes")
.await
{
return;
}
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
if let Err(e) = track.lock().connect_midi_connectable(
from.clone(),
from_port,
to.clone(),
to_port,
) {
self.notify_clients(Err(e)).await;
return;
}
}
Action::TrackDisconnectMidi {
ref track_name,
ref from,
from_port,
ref to,
to_port,
} => {
if self
.reject_if_track_frozen(track_name, "routing changes")
.await
{
return;
}
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
if let Err(e) = track.lock().disconnect_midi_connectable(
from.clone(),
from_port,
to.clone(),
to_port,
) {
self.notify_clients(Err(e)).await;
return;
}
}
#[cfg(all(unix, not(target_os = "macos")))]
Action::ListLv2Plugins => {
match crate::plugins::scan_plugins::<crate::plugins::types::Lv2PluginInfo>("lv2") {
Ok(plugins) => {
self.notify_clients(Ok(Action::Lv2Plugins(plugins))).await;
}
Err(e) => {
tracing::error!("LV2 plugin scan failed: {e}");
self.notify_clients(Ok(Action::Lv2PluginsUnavailable { error: e }))
.await;
}
}
return;
}
#[cfg(all(unix, not(target_os = "macos")))]
Action::Lv2Plugins(_) => {}
#[cfg(all(unix, not(target_os = "macos")))]
Action::Lv2PluginsUnavailable { .. } => {}
Action::ListVst3Plugins => {
match crate::plugins::scan_plugins::<crate::plugins::types::Vst3PluginInfo>("vst3")
{
Ok(plugins) => {
self.notify_clients(Ok(Action::Vst3Plugins(plugins))).await;
}
Err(e) => {
tracing::error!("VST3 plugin scan failed: {e}");
self.notify_clients(Ok(Action::Vst3PluginsUnavailable { error: e }))
.await;
}
}
return;
}
Action::Vst3Plugins(_) => {}
Action::Vst3PluginsUnavailable { .. } => {}
Action::ListClapPlugins => {
match crate::plugins::scan_plugins::<crate::plugins::types::ClapPluginInfo>("clap")
{
Ok(plugins) => {
self.notify_clients(Ok(Action::ClapPlugins(plugins))).await;
}
Err(e) => {
tracing::error!("CLAP plugin scan failed: {e}");
self.notify_clients(Ok(Action::ClapPluginsUnavailable { error: e }))
.await;
}
}
return;
}
Action::ListClapPluginsWithCapabilities => {
match crate::plugins::scan_plugins::<crate::plugins::types::ClapPluginInfo>("clap")
{
Ok(plugins) => {
self.notify_clients(Ok(Action::ClapPlugins(plugins))).await;
}
Err(e) => {
tracing::error!("CLAP plugin scan failed: {e}");
self.notify_clients(Ok(Action::ClapPluginsUnavailable { error: e }))
.await;
}
}
return;
}
Action::ClapPlugins(_) => {}
Action::ClapPluginsUnavailable { .. } => {}
Action::TrackLoadClapPlugin {
ref track_name,
ref plugin_path,
instance_id,
} => {
if self
.reject_if_track_frozen(track_name, "CLAP plugin loading")
.await
{
return;
}
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
let track = track.lock();
if track.audio.processing {
self.notify_clients(Err(format!(
"Track '{}' is currently processing audio; stop playback before loading CLAP plugins",
track_name
)))
.await;
return;
}
if let Err(e) = track.load_clap_plugin(plugin_path, instance_id) {
self.notify_clients(Err(e)).await;
return;
}
self.notify_clients(Ok(Action::Log {
source: "engine".to_string(),
message: format!("CLAP plugin loaded on track '{track_name}': {plugin_path}"),
}))
.await;
if let Some(instance) = track.clap_plugins.last()
&& let Some(stderr) = instance.processor.lock().take_stderr()
{
let source = format!("clap:{plugin_path}");
self.spawn_plugin_host_stderr_reader(stderr, source);
self.notify_clients(Ok(Action::Log {
source: "engine".to_string(),
message: format!(
"Attached stderr reader for CLAP plugin on track '{track_name}'"
),
}))
.await;
}
}
Action::TrackUnloadClapPlugin {
ref track_name,
ref plugin_path,
} => {
if self
.reject_if_track_frozen(track_name, "CLAP plugin unloading")
.await
{
return;
}
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
let track = track.lock();
if track.audio.processing {
self.notify_clients(Err(format!(
"Track '{}' is currently processing audio; stop playback before unloading CLAP plugins",
track_name
)))
.await;
return;
}
if let Err(e) = track.unload_clap_plugin(plugin_path) {
self.notify_clients(Err(e)).await;
return;
}
}
Action::TrackUnloadClapPluginInstance {
ref track_name,
instance_id,
} => {
if self
.reject_if_track_frozen(track_name, "CLAP plugin unloading")
.await
{
return;
}
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
let track = track.lock();
if track.audio.processing {
self.notify_clients(Err(format!(
"Track '{}' is currently processing audio; stop playback before unloading CLAP plugins",
track_name
)))
.await;
return;
}
if let Err(e) = track.unload_clap_plugin_instance(instance_id) {
self.notify_clients(Err(e)).await;
return;
}
}
Action::TrackShowClapGui {
ref track_name,
instance_id,
} => {
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
if let Err(e) = track.lock().show_clap_gui(instance_id) {
self.notify_clients(Err(e)).await;
return;
}
}
Action::TrackLoadVst3Plugin {
ref track_name,
ref plugin_path,
instance_id,
} => {
if self
.reject_if_track_frozen(track_name, "VST3 plugin loading")
.await
{
return;
}
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
let track = track.lock();
if track.audio.processing {
self.notify_clients(Err(format!(
"Track '{}' is currently processing audio; stop playback before loading VST3 plugins",
track_name
)))
.await;
return;
}
if let Err(e) = track.load_vst3_plugin(plugin_path, instance_id) {
self.notify_clients(Err(e)).await;
return;
}
if let Some(instance) = track.vst3_plugins.last()
&& let Some(stderr) = instance.processor.lock().take_stderr()
{
let source = format!("vst3:{plugin_path}");
self.spawn_plugin_host_stderr_reader(stderr, source);
}
}
Action::TrackUnloadVst3Plugin {
ref track_name,
ref plugin_path,
} => {
if self
.reject_if_track_frozen(track_name, "VST3 plugin unloading")
.await
{
return;
}
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
let track = track.lock();
if track.audio.processing {
self.notify_clients(Err(format!(
"Track '{}' is currently processing audio; stop playback before unloading VST3 plugins",
track_name
)))
.await;
return;
}
if let Err(e) = track.unload_vst3_plugin(plugin_path) {
self.notify_clients(Err(e)).await;
return;
}
}
Action::TrackUnloadVst3PluginInstance {
ref track_name,
instance_id,
} => {
if self
.reject_if_track_frozen(track_name, "VST3 plugin unloading")
.await
{
return;
}
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
let track = track.lock();
if track.audio.processing {
self.notify_clients(Err(format!(
"Track '{}' is currently processing audio; stop playback before unloading VST3 plugins",
track_name
)))
.await;
return;
}
if let Err(e) = track.unload_vst3_plugin_instance(instance_id) {
self.notify_clients(Err(e)).await;
return;
}
}
Action::TrackShowVst3Gui {
ref track_name,
instance_id,
} => {
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
if let Err(e) = track.lock().show_vst3_gui(instance_id) {
self.notify_clients(Err(e)).await;
return;
}
}
#[cfg(all(unix, not(target_os = "macos")))]
Action::TrackLoadLv2Plugin {
ref track_name,
ref plugin_uri,
instance_id,
} => {
if self
.reject_if_track_frozen(track_name, "LV2 plugin loading")
.await
{
return;
}
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
let track = track.lock();
if track.audio.processing {
self.notify_clients(Err(format!(
"Track '{}' is currently processing audio; stop playback before loading LV2 plugins",
track_name
)))
.await;
return;
}
if let Err(e) = track.load_lv2_plugin(plugin_uri, instance_id) {
self.notify_clients(Err(e)).await;
return;
}
if let Some(instance) = track.lv2_plugins.last()
&& let Some(stderr) = instance.processor.lock().take_stderr()
{
let source = format!("lv2:{plugin_uri}");
self.spawn_plugin_host_stderr_reader(stderr, source);
}
}
#[cfg(all(unix, not(target_os = "macos")))]
Action::TrackUnloadLv2Plugin {
ref track_name,
ref plugin_uri,
} => {
if self
.reject_if_track_frozen(track_name, "LV2 plugin unloading")
.await
{
return;
}
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
let track = track.lock();
if track.audio.processing {
self.notify_clients(Err(format!(
"Track '{}' is currently processing audio; stop playback before unloading LV2 plugins",
track_name
)))
.await;
return;
}
if let Err(e) = track.unload_lv2_plugin(plugin_uri) {
self.notify_clients(Err(e)).await;
return;
}
}
#[cfg(all(unix, not(target_os = "macos")))]
Action::TrackUnloadLv2PluginInstance {
ref track_name,
instance_id,
} => {
if self
.reject_if_track_frozen(track_name, "LV2 plugin unloading")
.await
{
return;
}
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
let track = track.lock();
if track.audio.processing {
self.notify_clients(Err(format!(
"Track '{}' is currently processing audio; stop playback before unloading LV2 plugins",
track_name
)))
.await;
return;
}
if let Err(e) = track.unload_lv2_plugin_instance(instance_id) {
self.notify_clients(Err(e)).await;
return;
}
}
#[cfg(all(unix, not(target_os = "macos")))]
Action::TrackShowLv2Gui {
ref track_name,
instance_id,
} => {
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
if let Err(e) = track.lock().show_lv2_gui(instance_id) {
self.notify_clients(Err(e)).await;
return;
}
}
Action::TrackSetPluginResourceDir {
ref track_name,
instance_id,
ref format,
ref directory,
} => {
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
let dir = std::path::Path::new(directory);
let result = if format.eq_ignore_ascii_case("CLAP") {
track.lock().set_clap_plugin_resource_dir(instance_id, dir)
} else if format.eq_ignore_ascii_case("LV2") {
#[cfg(all(unix, not(target_os = "macos")))]
{
track.lock().set_lv2_plugin_resource_dir(instance_id, dir)
}
#[cfg(not(all(unix, not(target_os = "macos"))))]
Err("LV2 is not supported on this platform".to_string())
} else {
Err(format!(
"Unsupported plugin format for resource dir: {format}"
))
};
if let Err(e) = result {
self.notify_clients(Err(e)).await;
return;
}
}
Action::TrackClapFileReferences {
ref track_name,
instance_id,
refs: _,
} => match self.track_handle_or_err(track_name) {
Ok(track) => {
let refs = track.lock().clap_file_references(instance_id).unwrap_or_else(|e| {
tracing::warn!(track_name = %track_name, instance_id, error = %e, "Failed to enumerate CLAP file references");
Vec::new()
});
self.notify_clients(Ok(Action::TrackClapFileReferences {
track_name: track_name.clone(),
instance_id,
refs,
}))
.await;
}
Err(e) => {
self.notify_clients(Err(e)).await;
}
},
Action::TrackUpdateClapFileReference {
ref track_name,
instance_id,
index,
ref path,
} => {
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
if let Err(e) = track
.lock()
.update_clap_file_reference(instance_id, index, path)
{
self.notify_clients(Err(e)).await;
return;
}
}
Action::ClipSetPluginResourceDir {
ref track_name,
clip_idx,
instance_id,
ref format,
ref directory,
} => {
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
let dir = std::path::Path::new(directory);
let track = track.lock();
let result = if format.eq_ignore_ascii_case("CLAP") {
track.clip_set_clap_plugin_resource_dir(clip_idx, instance_id, dir)
} else if format.eq_ignore_ascii_case("LV2") {
#[cfg(all(unix, not(target_os = "macos")))]
{
track.clip_set_lv2_plugin_resource_dir(clip_idx, instance_id, dir)
}
#[cfg(not(all(unix, not(target_os = "macos"))))]
Err("LV2 is not supported on this platform".to_string())
} else {
Err(format!(
"Unsupported plugin format for resource dir: {format}"
))
};
if let Err(e) = result {
self.notify_clients(Err(e)).await;
return;
}
}
Action::ClipClapFileReferences {
ref track_name,
clip_idx,
instance_id,
refs: _,
} => match self.track_handle_or_err(track_name) {
Ok(track) => {
let track = track.lock();
let refs = track
.clip_clap_file_references(clip_idx, instance_id)
.unwrap_or_else(|e| {
tracing::warn!(
track_name = %track_name,
clip_idx,
instance_id,
error = %e,
"Failed to enumerate clip CLAP file references"
);
Vec::new()
});
self.notify_clients(Ok(Action::ClipClapFileReferences {
track_name: track_name.clone(),
clip_idx,
instance_id,
refs,
}))
.await;
}
Err(e) => {
self.notify_clients(Err(e)).await;
}
},
Action::ClipUpdateClapFileReference {
ref track_name,
clip_idx,
instance_id,
index,
ref path,
} => {
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
if let Err(e) =
track
.lock()
.clip_update_clap_file_reference(clip_idx, instance_id, index, path)
{
self.notify_clients(Err(e)).await;
return;
}
}
Action::TrackSetClapParameter {
ref track_name,
instance_id,
param_id,
value,
} => {
if self
.reject_if_track_frozen(track_name, "CLAP parameter changes")
.await
{
return;
}
match self.track_handle_or_err(track_name) {
Ok(track) => {
if let Err(e) =
track
.lock()
.set_clap_parameter(instance_id, param_id, value)
{
self.notify_clients(Err(e)).await;
return;
}
self.notify_clients(Ok(a.clone())).await;
}
Err(e) => {
self.notify_clients(Err(e)).await;
}
}
}
Action::ClipSetClapParameter {
ref track_name,
clip_idx,
instance_id,
param_id,
value,
} => {
if self
.reject_if_track_frozen(track_name, "CLAP parameter changes")
.await
{
return;
}
match self.track_handle_or_err(track_name) {
Ok(track) => {
if let Err(e) = track.lock().clip_set_clap_parameter(
clip_idx,
instance_id,
param_id,
value,
) {
self.notify_clients(Err(e)).await;
return;
}
self.notify_clients(Ok(a.clone())).await;
}
Err(e) => {
self.notify_clients(Err(e)).await;
}
}
}
Action::TrackSetClapParameterAt {
ref track_name,
instance_id,
param_id,
value,
frame,
} => {
if self
.reject_if_track_frozen(track_name, "CLAP parameter changes")
.await
{
return;
}
match self.track_handle_or_err(track_name) {
Ok(track) => {
if let Err(e) =
track
.lock()
.set_clap_parameter_at(instance_id, param_id, value, frame)
{
self.notify_clients(Err(e)).await;
return;
}
self.notify_clients(Ok(a.clone())).await;
}
Err(e) => {
self.notify_clients(Err(e)).await;
}
}
}
Action::TrackBeginClapParameterEdit {
ref track_name,
instance_id,
param_id,
frame,
} => {
if self
.reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
.await
{
return;
}
match self.track_handle_or_err(track_name) {
Ok(track) => {
if let Err(e) =
track
.lock()
.begin_clap_parameter_edit(instance_id, param_id, frame)
{
self.notify_clients(Err(e)).await;
return;
}
self.notify_clients(Ok(a.clone())).await;
}
Err(e) => {
self.notify_clients(Err(e)).await;
}
}
}
Action::TrackEndClapParameterEdit {
ref track_name,
instance_id,
param_id,
frame,
} => {
if self
.reject_if_track_frozen(track_name, "CLAP parameter edit gestures")
.await
{
return;
}
match self.track_handle_or_err(track_name) {
Ok(track) => {
if let Err(e) =
track
.lock()
.end_clap_parameter_edit(instance_id, param_id, frame)
{
self.notify_clients(Err(e)).await;
return;
}
self.notify_clients(Ok(a.clone())).await;
}
Err(e) => {
self.notify_clients(Err(e)).await;
}
}
}
Action::TrackGetClapParameters {
ref track_name,
instance_id,
} => match self.track_handle_or_err(track_name) {
Ok(track) => match track.lock().get_clap_parameters(instance_id) {
Ok(parameters) => {
self.notify_clients(Ok(Action::TrackClapParameters {
track_name: track_name.clone(),
instance_id,
parameters,
}))
.await;
}
Err(e) => {
self.notify_clients(Err(e)).await;
}
},
Err(e) => {
self.notify_clients(Err(e)).await;
}
},
Action::TrackClapParameters { .. } => {}
Action::TrackClapSnapshotState {
ref track_name,
instance_id,
} => match self.track_handle_or_err(track_name) {
Ok(track) => {
let plugin_path = track
.lock()
.clap_plugins
.iter()
.find(|instance| instance.id == instance_id)
.map(|instance| instance.processor.lock().path().to_string())
.unwrap_or_default();
match track.lock().clap_snapshot_state(instance_id) {
Ok(state) => {
self.notify_clients(Ok(Action::TrackClapStateSnapshot {
track_name: track_name.clone(),
instance_id,
plugin_path,
state,
}))
.await;
}
Err(e) => {
self.notify_clients(Err(e)).await;
}
}
}
Err(e) => {
self.notify_clients(Err(e)).await;
}
},
Action::ClipClapSnapshotState {
ref track_name,
clip_idx,
instance_id,
} => match self.track_handle_or_err(track_name) {
Ok(track) => match track.lock().clip_clap_snapshot_state(clip_idx, instance_id) {
Ok((plugin_path, state)) => {
self.notify_clients(Ok(Action::ClipClapStateSnapshot {
track_name: track_name.clone(),
clip_idx,
instance_id,
plugin_path,
state,
}))
.await;
}
Err(e) => {
self.notify_clients(Err(e)).await;
}
},
Err(e) => {
self.notify_clients(Err(e)).await;
}
},
Action::TrackClapStateSnapshot { .. } => {}
Action::ClipClapStateSnapshot { .. } => {}
Action::TrackClapStateDirty { .. } => {}
Action::ClipClapStateDirty { .. } => {}
Action::TrackClapRestoreState {
ref track_name,
instance_id,
ref state,
} => {
if self
.reject_if_track_frozen(track_name, "CLAP state restore")
.await
{
return;
}
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
let track = track.lock();
if track.audio.processing {
self.notify_clients(Err(format!(
"Track '{}' is currently processing audio; stop playback before restoring CLAP state",
track_name
)))
.await;
return;
}
if let Err(e) = track.clap_restore_state(instance_id, state) {
self.notify_clients(Err(e)).await;
return;
}
}
Action::ClipClapRestoreState {
ref track_name,
clip_idx,
instance_id,
ref state,
} => {
if self
.reject_if_track_frozen(track_name, "CLAP state restore")
.await
{
return;
}
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
let track = track.lock();
if track.audio.processing {
self.notify_clients(Err(format!(
"Track '{}' is currently processing audio; stop playback before restoring CLAP state",
track_name
)))
.await;
return;
}
if let Err(e) = track.clip_clap_restore_state(clip_idx, instance_id, state) {
self.notify_clients(Err(e)).await;
return;
}
}
Action::TrackSnapshotAllClapStates { ref track_name } => {
let track = match self.track_handle_or_err(track_name) {
Ok(track) => track,
Err(e) => {
self.notify_clients(Err(e)).await;
return;
}
};
let instances: Vec<_> = {
let locked = track.lock();
locked
.clap_plugins
.iter()
.map(|i| (i.id, i.processor.lock().path().to_string()))
.collect()
};
for (instance_id, plugin_path) in instances {
match track.lock().clap_snapshot_state(instance_id) {
Ok(state) => {
self.notify_clients(Ok(Action::TrackClapStateSnapshot {
track_name: track_name.clone(),
instance_id,
plugin_path,
state,
}))
.await;
}
Err(_e) => {}
}
}
self.notify_clients(Ok(Action::TrackSnapshotAllClapStatesDone {
track_name: track_name.clone(),
}))
.await;
}
Action::TrackSnapshotAllClapStatesDone { .. } => {}
Action::TrackGetVst3Graph { ref track_name } => {
match self.track_handle_or_err(track_name) {
Ok(track) => {
let t = track.lock();
let plugins = t.vst3_graph_plugins();
let connections = t.vst3_graph_connections();
self.notify_clients(Ok(Action::TrackVst3Graph {
track_name: track_name.clone(),
plugins,
connections,
}))
.await;
}
Err(e) => {
self.notify_clients(Err(e)).await;
}
}
}
Action::TrackVst3Graph { .. } => {}
Action::TrackSetVst3Parameter {
ref track_name,
instance_id,
param_id,
value,
} => {
if self
.reject_if_track_frozen(track_name, "VST3 parameter changes")
.await
{
return;
}
match self.track_handle_or_err(track_name) {
Ok(track) => {
if let Err(e) =
track
.lock()
.set_vst3_parameter(instance_id, param_id, value)
{
self.notify_clients(Err(e)).await;
return;
}
self.notify_clients(Ok(a.clone())).await;
}
Err(e) => {
self.notify_clients(Err(e)).await;
}
}
}
Action::TrackSetPluginBypassed {
ref track_name,
instance_id,
ref format,
bypassed,
} => match self.track_handle_or_err(track_name) {
Ok(track) => {
let result = match format.as_str() {
"CLAP" => track.lock().set_clap_plugin_bypassed(instance_id, bypassed),
"VST3" => track.lock().set_vst3_plugin_bypassed(instance_id, bypassed),
#[cfg(all(unix, not(target_os = "macos")))]
"LV2" => track.lock().set_lv2_plugin_bypassed(instance_id, bypassed),
_ => Err(format!("Unknown plugin format for bypass: {format}")),
};
if let Err(e) = result {
self.notify_clients(Err(e)).await;
return;
}
self.notify_clients(Ok(a.clone())).await;
}
Err(e) => {
self.notify_clients(Err(e)).await;
}
},
Action::TrackGetVst3Parameters {
ref track_name,
instance_id,
} => match self.track_handle_or_err(track_name) {
Ok(track) => match track.lock().get_vst3_parameters(instance_id) {
Ok(parameters) => {
self.notify_clients(Ok(Action::TrackVst3Parameters {
track_name: track_name.clone(),
instance_id,
parameters,
}))
.await;
}
Err(e) => {
self.notify_clients(Err(e)).await;
}
},
Err(e) => {
self.notify_clients(Err(e)).await;
}
},
Action::TrackVst3Parameters { .. } => {}
Action::TrackVst3SnapshotState {
ref track_name,
instance_id,
} => match self.track_handle_or_err(track_name) {
Ok(track) => match track.lock().vst3_snapshot_state(instance_id) {
Ok(state) => {
self.notify_clients(Ok(Action::TrackVst3StateSnapshot {
track_name: track_name.clone(),
instance_id,
state,
}))
.await;
}
Err(e) => {
self.notify_clients(Err(e)).await;
}
},
Err(e) => {
self.notify_clients(Err(e)).await;
}
},
Action::ClipVst3SnapshotState {
ref track_name,
clip_idx,
instance_id,
} => match self.track_handle_or_err(track_name) {
Ok(track) => match track.lock().clip_vst3_snapshot_state(clip_idx, instance_id) {
Ok(state) => {
self.notify_clients(Ok(Action::ClipVst3StateSnapshot {
track_name: track_name.clone(),
clip_idx,
instance_id,
state,
}))
.await;
}
Err(e) => {
self.notify_clients(Err(e)).await;
}
},
Err(e) => {
self.notify_clients(Err(e)).await;
}
},
Action::TrackVst3StateSnapshot { .. } => {}
Action::ClipVst3StateSnapshot { .. } => {}
Action::TrackVst3RestoreState {
ref track_name,
instance_id,
ref state,
} => match self.track_handle_or_err(track_name) {
Ok(track) => {
if let Err(e) = track.lock().vst3_restore_state(instance_id, state) {
self.notify_clients(Err(e)).await;
return;
}
self.notify_clients(Ok(a.clone())).await;
}
Err(e) => {
self.notify_clients(Err(e)).await;
}
},
Action::TrackConnectVst3Audio {
ref track_name,
ref from_node,
from_port,
ref to_node,
to_port,
} => {
if self
.reject_if_track_frozen(track_name, "VST3 routing changes")
.await
{
return;
}
match self.track_handle_or_err(track_name) {
Ok(track) => {
if let Err(e) = track
.lock()
.connect_vst3_audio(from_node, from_port, to_node, to_port)
{
self.notify_clients(Err(e)).await;
return;
}
self.notify_clients(Ok(a.clone())).await;
}
Err(e) => {
self.notify_clients(Err(e)).await;
}
}
}
Action::TrackDisconnectVst3Audio {
ref track_name,
ref from_node,
from_port,
ref to_node,
to_port,
} => {
if self
.reject_if_track_frozen(track_name, "VST3 routing changes")
.await
{
return;
}
match self.track_handle_or_err(track_name) {
Ok(track) => {
if let Err(e) = track
.lock()
.disconnect_vst3_audio(from_node, from_port, to_node, to_port)
{
self.notify_clients(Err(e)).await;
return;
}
self.notify_clients(Ok(a.clone())).await;
}
Err(e) => {
self.notify_clients(Err(e)).await;
}
}
}
Action::ClipMove {
ref kind,
ref from,
ref to,
copy,
} => {
if let Some(from_track_handle) = self.state.lock().tracks.get(&from.track_name)
&& let Some(to_track_handle) = self.state.lock().tracks.get(&to.track_name)
{
let from_track = from_track_handle.lock();
let to_track = to_track_handle.lock();
match kind {
Kind::Audio => {
if from.clip_index >= from_track.audio.clips.len() {
self.notify_clients(Err(format!(
"Clip index {} is too high, as track {} has only {} clips!",
from.clip_index,
from_track.name.clone(),
from_track.audio.clips.len(),
)))
.await;
return;
}
if from_track.audio.ins.len() != to_track.audio.ins.len() {
self.notify_clients(Err(format!(
"Cannot move/copy audio clip from '{}' ({} inputs) to '{}' ({} inputs)",
from_track.name,
from_track.audio.ins.len(),
to_track.name,
to_track.audio.ins.len()
)))
.await;
return;
}
let clip_copy = from_track.audio.clips[from.clip_index].clone();
if !copy {
from_track.audio.clips.remove(from.clip_index);
}
let mut clip_copy = clip_copy;
clip_copy.start = to.sample_offset;
let max_lane = to_track.audio.ins.len().saturating_sub(1);
clip_copy.input_channel = to.input_channel.min(max_lane);
to_track.audio.clips.push(clip_copy);
}
Kind::MIDI => {
if from.clip_index >= from_track.midi.clips.len() {
self.notify_clients(Err(format!(
"Clip index {} is too high, as track {} has only {} clips!",
from.clip_index,
from_track.name.clone(),
from_track.midi.clips.len(),
)))
.await;
return;
}
let clip_copy = from_track.midi.clips[from.clip_index].clone();
if !copy {
from_track.midi.clips.remove(from.clip_index);
}
let mut clip_copy = clip_copy;
clip_copy.start = to.sample_offset;
let max_lane = to_track.midi.ins.len().saturating_sub(1);
clip_copy.input_channel = to.input_channel.min(max_lane);
to_track.midi.clips.push(clip_copy);
}
}
}
}
Action::AddClip {
ref name,
ref track_name,
start,
length,
offset,
input_channel,
muted,
ref peaks_file,
kind,
fade_enabled,
fade_in_samples,
fade_out_samples,
ref source_name,
source_offset,
source_length,
ref preview_name,
ref pitch_correction_points,
pitch_correction_frame_likeness,
pitch_correction_inertia_ms,
pitch_correction_formant_compensation,
ref plugin_graph_json,
} => {
self.add_clip_to_track(ClipAddRequest {
name,
track_name,
start,
length,
offset,
input_channel,
muted,
peaks_file: peaks_file.clone(),
kind,
fade_enabled,
fade_in_samples,
fade_out_samples,
source_name: source_name.clone(),
source_offset,
source_length,
preview_name: preview_name.clone(),
pitch_correction_points: pitch_correction_points.clone(),
pitch_correction_frame_likeness,
pitch_correction_inertia_ms,
pitch_correction_formant_compensation,
plugin_graph_json: plugin_graph_json.clone(),
});
if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
let track_name = track_name.clone();
tokio::task::spawn_blocking(move || {
track.lock().preload_clips();
tracing::debug!("Preloaded clips for track '{}' after AddClip", track_name);
});
}
}
Action::AddGroupedClip {
ref track_name,
kind,
ref audio_clip,
ref midi_clip,
} => {
self.add_grouped_clip_to_track(
track_name,
kind,
audio_clip.clone(),
midi_clip.clone(),
);
if let Some(track) = self.state.lock().tracks.get(track_name).cloned() {
let track_name = track_name.clone();
tokio::task::spawn_blocking(move || {
track.lock().preload_clips();
tracing::debug!(
"Preloaded clips for track '{}' after AddGroupedClip",
track_name
);
});
}
}
Action::RemoveClip {
ref track_name,
kind,
ref clip_indices,
} => {
self.remove_clips_from_track(track_name, kind, clip_indices);
}
Action::RenameClip {
ref track_name,
kind,
clip_index,
ref new_name,
} => {
self.rename_clip_references(track_name, kind, clip_index, new_name);
}
Action::SetClipSourceName {
ref track_name,
kind,
clip_index,
ref name,
} => {
self.set_clip_source_name(track_name, clip_index, kind, name.clone());
}
Action::SetClipFade {
ref track_name,
clip_index,
kind,
fade_enabled,
fade_in_samples,
fade_out_samples,
} => {
self.set_clip_fade(
track_name,
clip_index,
kind,
fade_enabled,
fade_in_samples,
fade_out_samples,
);
}
Action::SetClipBounds {
ref track_name,
clip_index,
kind,
start,
length,
offset,
} => {
self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
}
Action::SyncClipBounds {
ref track_name,
clip_index,
kind,
start,
length,
offset,
} => {
self.set_clip_bounds(track_name, clip_index, kind, start, length, offset);
}
Action::SetClipMuted {
ref track_name,
clip_index,
kind,
muted,
} => {
self.set_clip_muted(track_name, clip_index, kind, muted);
}
Action::SetClipPluginGraphJson {
ref track_name,
clip_index,
ref plugin_graph_json,
} => {
self.set_clip_plugin_graph_json(track_name, clip_index, plugin_graph_json.clone());
}
Action::SetClipPitchCorrection {
ref track_name,
clip_index,
ref preview_name,
ref source_name,
source_offset,
source_length,
ref pitch_correction_points,
pitch_correction_frame_likeness,
pitch_correction_inertia_ms,
pitch_correction_formant_compensation,
} => {
self.set_clip_pitch_correction(
track_name,
clip_index,
preview_name.clone(),
source_name.clone(),
source_offset,
source_length,
pitch_correction_points.clone(),
pitch_correction_frame_likeness,
pitch_correction_inertia_ms,
pitch_correction_formant_compensation,
);
}
Action::Connect {
ref from_track,
from_port,
ref to_track,
to_port,
kind,
} => {
match kind {
Kind::Audio => {
let from_audio_io = if from_track == "hw:in" {
self.hw_input_audio_port(from_port)
} else {
self.state
.lock()
.tracks
.get(from_track)
.and_then(|t| t.lock().audio.outs.get(from_port).cloned())
};
let to_audio_io = if to_track == "hw:out" {
self.hw_output_audio_port(to_port)
} else {
self.state
.lock()
.tracks
.get(to_track)
.and_then(|t| t.lock().audio.ins.get(to_port).cloned())
};
match (from_audio_io, to_audio_io) {
(Some(source), Some(target)) => {
if from_track != "hw:in"
&& to_track != "hw:out"
&& self.check_if_leads_to_kind(
Kind::Audio,
to_track,
from_track,
)
{
self.notify_clients(Err(
"Circular routing is not allowed!".into()
))
.await;
return;
}
crate::audio::io::AudioIO::connect(&source, &target);
}
(None, _) => {
self.notify_clients(Err(format!(
"Source track '{}' not found",
from_track
)))
.await;
return;
}
(_, None) => {
self.notify_clients(Err(format!(
"Destination track '{}' not found",
to_track
)))
.await;
return;
}
}
}
Kind::MIDI => {
let from_hw_in_device = Self::midi_hw_in_device(from_track);
let to_hw_out_device = Self::midi_hw_out_device(to_track);
let from_is_invalid_hw = Self::midi_hw_out_device(from_track).is_some();
let to_is_invalid_hw = Self::midi_hw_in_device(to_track).is_some();
if from_is_invalid_hw || to_is_invalid_hw {
self.notify_clients(Err(
"Invalid MIDI hardware connection direction".to_string()
))
.await;
return;
}
if from_hw_in_device.is_none()
&& to_hw_out_device.is_none()
&& self.check_if_leads_to_kind(Kind::MIDI, to_track, from_track)
{
self.notify_clients(Err("Circular routing is not allowed!".into()))
.await;
return;
}
let state = self.state.lock();
let from_track_handle = state.tracks.get(from_track);
let to_track_handle = state.tracks.get(to_track);
if let (Some(from_device), Some(to_device)) =
(from_hw_in_device, to_hw_out_device)
{
let route = MidiHwThruRoute {
from_device: from_device.to_string(),
to_device: to_device.to_string(),
};
if !self.midi_hw_thru_routes.iter().any(|r| r == &route) {
self.midi_hw_thru_routes.push(route);
}
} else if let Some(device) = from_hw_in_device {
if let Some(t_t) = to_track_handle {
if t_t.lock().midi.ins.get(to_port).is_none() {
self.notify_clients(Err(format!(
"MIDI input port {} not found on track '{}'",
to_port, to_track
)))
.await;
return;
}
let route = MidiHwInRoute {
device: device.to_string(),
to_track: to_track.to_string(),
to_port,
};
if !self.midi_hw_in_routes.iter().any(|r| r == &route) {
self.midi_hw_in_routes.push(route);
}
} else {
self.notify_clients(Err(format!(
"MIDI destination track not found: {}",
to_track
)))
.await;
return;
}
} else if let Some(device) = to_hw_out_device {
if let Some(f_t) = from_track_handle {
if f_t.lock().midi.outs.get(from_port).is_none() {
self.notify_clients(Err(format!(
"MIDI output port {} not found on track '{}'",
from_port, from_track
)))
.await;
return;
}
let route = MidiHwOutRoute {
from_track: from_track.to_string(),
from_port,
device: device.to_string(),
};
if !self.midi_hw_out_routes.iter().any(|r| r == &route) {
self.midi_hw_out_routes.push(route);
}
} else {
self.notify_clients(Err(format!(
"MIDI source track not found: {}",
from_track
)))
.await;
return;
}
} else {
match (from_track_handle, to_track_handle) {
(Some(f_t), Some(t_t)) => {
let to_in_res = t_t.lock().midi.ins.get(to_port).cloned();
if let Some(to_in) = to_in_res {
let from_track = f_t.lock();
if let Err(e) =
from_track.midi.connect_out(from_port, to_in)
{
self.notify_clients(Err(e)).await;
return;
}
from_track.invalidate_midi_route_cache();
} else {
self.notify_clients(Err(format!(
"MIDI input port {} not found on track '{}'",
to_port, to_track
)))
.await;
return;
}
}
_ => {
self.notify_clients(Err(format!(
"MIDI tracks not found: {} or {}",
from_track, to_track
)))
.await;
return;
}
}
}
}
};
}
Action::Disconnect {
ref from_track,
from_port,
ref to_track,
to_port,
kind,
} => {
if kind == Kind::Audio {
if let Err(e) = self.disconnect_audio_route_and_notify(a.clone()).await {
self.notify_clients(Err(e)).await;
}
} else if kind == Kind::MIDI {
let from_hw_in_device = Self::midi_hw_in_device(from_track);
let to_hw_out_device = Self::midi_hw_out_device(to_track);
if let (Some(from_device), Some(to_device)) =
(from_hw_in_device, to_hw_out_device)
{
let before = self.midi_hw_thru_routes.len();
self.midi_hw_thru_routes.retain(|r| {
!(r.from_device == from_device && r.to_device == to_device)
});
if self.midi_hw_thru_routes.len() < before {
self.notify_clients(Ok(a.clone())).await;
} else {
self.notify_clients(Err(format!(
"Disconnect failed: MIDI route not found ({} -> {})",
from_track, to_track
)))
.await;
}
return;
}
if let Some(device) = from_hw_in_device {
let before = self.midi_hw_in_routes.len();
self.midi_hw_in_routes.retain(|r| {
!(r.device == device && r.to_track == *to_track && r.to_port == to_port)
});
if self.midi_hw_in_routes.len() < before {
self.notify_clients(Ok(a.clone())).await;
} else {
self.notify_clients(Err(format!(
"Disconnect failed: MIDI route not found ({} -> {})",
from_track, to_track
)))
.await;
}
return;
}
if let Some(device) = to_hw_out_device {
let before = self.midi_hw_out_routes.len();
self.midi_hw_out_routes.retain(|r| {
!(r.from_track == *from_track
&& r.from_port == from_port
&& r.device == device)
});
if self.midi_hw_out_routes.len() < before {
self.notify_clients(Ok(a.clone())).await;
} else {
self.notify_clients(Err(format!(
"Disconnect failed: MIDI route not found ({} -> {})",
from_track, to_track
)))
.await;
}
return;
}
let state = self.state.lock();
if let (Some(f_t), Some(t_t)) =
(state.tracks.get(from_track), state.tracks.get(to_track))
&& let Some(to_in) = t_t.lock().midi.ins.get(to_port).cloned()
{
let from_track = f_t.lock();
if let Err(e) = from_track.midi.disconnect_out(from_port, &to_in) {
self.notify_clients(Err(e)).await;
} else {
from_track.invalidate_midi_route_cache();
self.notify_clients(Ok(a.clone())).await;
}
} else {
self.notify_clients(Err(format!(
"Disconnect failed: MIDI ports not found ({} -> {})",
from_track, to_track
)))
.await;
}
}
}
Action::OpenAudioDevice {
ref device,
ref input_device,
sample_rate_hz,
bits,
exclusive,
period_frames,
nperiods,
sync_mode,
..
} => {
#[cfg(unix)]
{
let request = AudioOpenRequest {
device,
input_device: input_device.as_deref(),
sample_rate_hz,
bits,
exclusive,
period_frames,
nperiods,
sync_mode,
};
if self.maybe_open_jack_runtime(request).await.is_some() {
return;
}
}
let hw_opts = Self::build_hw_options(exclusive, period_frames, nperiods, sync_mode);
let open_result = self
.open_non_jack_audio_device(
device,
input_device.as_deref(),
sample_rate_hz,
bits,
hw_opts,
)
.await;
match open_result {
Ok(()) => {}
Err(e) => {
error!("Failed to open audio device: {e}");
self.notify_clients(Err(e)).await;
return;
}
}
self.finalize_open_audio_device().await;
if let Some(hw) = &self.hw_driver {
let effective_action = {
let hw = hw.lock();
Action::OpenAudioDevice {
device: device.clone(),
input_device: input_device.clone(),
sample_rate_hz: hw.sample_rate(),
bits: hw.sample_bits(),
exclusive,
period_frames,
nperiods,
sync_mode,
actual_period_frames: hw.cycle_samples(),
input_channels: hw.input_channels(),
output_channels: hw.output_channels(),
bytes_per_frame: hw.frame_size_bytes(),
}
};
action_to_process = effective_action;
}
}
Action::JackAddAudioInputPort => {
#[cfg(unix)]
{
if let Some(jack) = self.jack_runtime.clone() {
let (input_channels, output_channels, rate) = {
let jack = jack.lock();
if let Err(e) = jack.add_audio_input_port() {
self.notify_clients(Err(e)).await;
return;
}
(
jack.input_channels(),
jack.output_channels(),
jack.sample_rate,
)
};
self.publish_hw_infos(input_channels, output_channels, rate)
.await;
self.notify_clients(Ok(a.clone())).await;
} else {
self.notify_clients(Err(
"JACK runtime is not active; open the JACK backend first".to_string(),
))
.await;
}
}
#[cfg(not(unix))]
{
self.notify_clients(Err(
"JACK backend is not available on this platform build".to_string(),
))
.await;
}
}
Action::JackRemoveAudioInputPort(_removed_port) => {
#[cfg(unix)]
{
let removed_port = _removed_port;
if let Some(jack) = self.jack_runtime.clone() {
let (removed_port, removed_io) = {
let jack = jack.lock();
let removed_port = Some(removed_port);
let removed_io =
removed_port.and_then(|port| jack.input_audio_port(port));
match (removed_port, removed_io) {
(Some(port), Some(io)) => (port, io),
_ => {
self.notify_clients(Err(
"JACK audio input port index is out of range".to_string(),
))
.await;
return;
}
}
};
let reindex_notifications =
self.reindex_notifications_for_removed_hw_input(removed_port);
for disconnect in
self.disconnect_actions_for_removed_hw_input(removed_port, &removed_io)
{
if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await
{
self.notify_clients(Err(e)).await;
return;
}
}
let (input_channels, output_channels, rate) = {
let jack = jack.lock();
if let Err(e) = jack.remove_audio_input_port(removed_port) {
self.notify_clients(Err(e)).await;
return;
}
(
jack.input_channels(),
jack.output_channels(),
jack.sample_rate,
)
};
for action in reindex_notifications {
self.notify_clients(Ok(action)).await;
}
self.publish_hw_infos(input_channels, output_channels, rate)
.await;
self.notify_clients(Ok(a.clone())).await;
} else {
self.notify_clients(Err(
"JACK runtime is not active; open the JACK backend first".to_string(),
))
.await;
}
}
#[cfg(not(unix))]
{
self.notify_clients(Err(
"JACK backend is not available on this platform build".to_string(),
))
.await;
}
}
Action::JackAddAudioOutputPort => {
#[cfg(unix)]
{
if let Some(jack) = self.jack_runtime.clone() {
let (input_channels, output_channels, rate) = {
let jack = jack.lock();
if let Err(e) = jack.add_audio_output_port() {
self.notify_clients(Err(e)).await;
return;
}
(
jack.input_channels(),
jack.output_channels(),
jack.sample_rate,
)
};
self.publish_hw_infos(input_channels, output_channels, rate)
.await;
self.notify_clients(Ok(a.clone())).await;
} else {
self.notify_clients(Err(
"JACK runtime is not active; open the JACK backend first".to_string(),
))
.await;
}
}
#[cfg(not(unix))]
{
self.notify_clients(Err(
"JACK backend is not available on this platform build".to_string(),
))
.await;
}
}
Action::JackRemoveAudioOutputPort(_removed_port) => {
#[cfg(unix)]
{
let removed_port = _removed_port;
if let Some(jack) = self.jack_runtime.clone() {
let (removed_port, removed_io) = {
let jack = jack.lock();
let removed_port = Some(removed_port);
let removed_io =
removed_port.and_then(|port| jack.output_audio_port(port));
match (removed_port, removed_io) {
(Some(port), Some(io)) => (port, io),
_ => {
self.notify_clients(Err(
"JACK audio output port index is out of range".to_string(),
))
.await;
return;
}
}
};
let reindex_notifications =
self.reindex_notifications_for_removed_hw_output(removed_port);
for disconnect in
self.disconnect_actions_for_removed_hw_output(removed_port, &removed_io)
{
if let Err(e) = self.disconnect_audio_route_and_notify(disconnect).await
{
self.notify_clients(Err(e)).await;
return;
}
}
let (input_channels, output_channels, rate) = {
let jack = jack.lock();
if let Err(e) = jack.remove_audio_output_port(removed_port) {
self.notify_clients(Err(e)).await;
return;
}
(
jack.input_channels(),
jack.output_channels(),
jack.sample_rate,
)
};
for action in reindex_notifications {
self.notify_clients(Ok(action)).await;
}
self.publish_hw_infos(input_channels, output_channels, rate)
.await;
self.notify_clients(Ok(a.clone())).await;
} else {
self.notify_clients(Err(
"JACK runtime is not active; open the JACK backend first".to_string(),
))
.await;
}
}
#[cfg(not(unix))]
{
self.notify_clients(Err(
"JACK backend is not available on this platform build".to_string(),
))
.await;
}
}
Action::OpenMidiInputDevice(ref device) => {
let midi_hub = self.midi_hub.lock();
if let Err(e) = midi_hub.open_input(device) {
self.notify_clients(Err(e)).await;
return;
}
}
Action::OpenMidiOutputDevice(ref device) => {
let midi_hub = self.midi_hub.lock();
if let Err(e) = midi_hub.open_output(device) {
self.notify_clients(Err(e)).await;
return;
}
}
Action::RequestSessionDiagnostics => {
let (
track_count,
frozen_track_count,
audio_clip_count,
midi_clip_count,
lv2_instance_count,
vst3_instance_count,
clap_instance_count,
) = {
let tracks = &self.state.lock().tracks;
let mut track_count = 0usize;
let mut frozen_track_count = 0usize;
let mut audio_clip_count = 0usize;
let mut midi_clip_count = 0usize;
#[cfg(all(unix, not(target_os = "macos")))]
let mut lv2_instance_count = 0usize;
#[cfg(not(all(unix, not(target_os = "macos"))))]
let lv2_instance_count = 0usize;
let mut vst3_instance_count = 0usize;
let mut clap_instance_count = 0usize;
for track in tracks.values() {
let t = track.lock();
track_count += 1;
if t.frozen {
frozen_track_count += 1;
}
audio_clip_count += t.audio.clips.len();
midi_clip_count += t.midi.clips.len();
#[cfg(all(unix, not(target_os = "macos")))]
{
lv2_instance_count += t.lv2_plugins.len();
}
vst3_instance_count += t.vst3_plugins.len();
clap_instance_count += t.clap_plugins.len();
}
(
track_count,
frozen_track_count,
audio_clip_count,
midi_clip_count,
lv2_instance_count,
vst3_instance_count,
clap_instance_count,
)
};
#[cfg(not(all(unix, not(target_os = "macos"))))]
let _lv2_instance_count = lv2_instance_count;
let pending_hw_midi_events = self.pending_hw_midi_events.len()
+ self
.pending_hw_midi_events_by_device
.values()
.map(std::vec::Vec::len)
.sum::<usize>();
let sample_rate_hz = if let Some(hw) = &self.hw_driver {
hw.lock().sample_rate() as usize
} else {
#[cfg(unix)]
{
self.jack_runtime
.as_ref()
.map(|j| j.lock().sample_rate)
.unwrap_or(0)
}
#[cfg(not(unix))]
0
};
let cycle_samples = self.current_cycle_samples();
self.notify_clients(Ok(Action::SessionDiagnosticsReport {
track_count,
frozen_track_count,
audio_clip_count,
midi_clip_count,
#[cfg(all(unix, not(target_os = "macos")))]
lv2_instance_count,
vst3_instance_count,
clap_instance_count,
pending_requests: self.pending_requests.len(),
workers_total: self.workers.len(),
workers_ready: self.ready_workers.len(),
pending_hw_midi_events,
playing: self.playing,
transport_sample: self.transport_sample,
tempo_bpm: self.tempo_bpm,
sample_rate_hz,
cycle_samples,
}))
.await;
}
Action::RequestMidiLearnMappingsReport => {
let mut lines = Vec::<String>::new();
let fmt_binding = |b: &crate::message::MidiLearnBinding| {
let device = b.device.as_deref().unwrap_or("*");
format!("{device} CH{} CC{}", b.channel + 1, b.cc)
};
if let Some(b) = self.global_midi_learn_play_pause.as_ref() {
lines.push(format!("Global PlayPause: {}", fmt_binding(b)));
}
if let Some(b) = self.global_midi_learn_stop.as_ref() {
lines.push(format!("Global Stop: {}", fmt_binding(b)));
}
if let Some(b) = self.global_midi_learn_record_toggle.as_ref() {
lines.push(format!("Global RecordToggle: {}", fmt_binding(b)));
}
for (track_name, track) in self.state.lock().tracks.iter() {
let t = track.lock();
if let Some(b) = t.midi_learn_volume.as_ref() {
lines.push(format!("{} Volume: {}", track_name, fmt_binding(b)));
}
if let Some(b) = t.midi_learn_balance.as_ref() {
lines.push(format!("{} Balance: {}", track_name, fmt_binding(b)));
}
if let Some(b) = t.midi_learn_mute.as_ref() {
lines.push(format!("{} Mute: {}", track_name, fmt_binding(b)));
}
if let Some(b) = t.midi_learn_solo.as_ref() {
lines.push(format!("{} Solo: {}", track_name, fmt_binding(b)));
}
if let Some(b) = t.midi_learn_arm.as_ref() {
lines.push(format!("{} Arm: {}", track_name, fmt_binding(b)));
}
if let Some(b) = t.midi_learn_input_monitor.as_ref() {
lines.push(format!("{} InputMonitor: {}", track_name, fmt_binding(b)));
}
if let Some(b) = t.midi_learn_disk_monitor.as_ref() {
lines.push(format!("{} DiskMonitor: {}", track_name, fmt_binding(b)));
}
}
if lines.is_empty() {
lines.push("No MIDI learn mappings configured".to_string());
}
self.notify_clients(Ok(Action::MidiLearnMappingsReport { lines }))
.await;
}
Action::ClearAllMidiLearnBindings => {
self.pending_midi_learn = None;
self.pending_global_midi_learn = None;
self.global_midi_learn_play_pause = None;
self.global_midi_learn_stop = None;
self.global_midi_learn_record_toggle = None;
self.midi_cc_gate.clear();
for track in self.state.lock().tracks.values() {
let t = track.lock();
t.midi_learn_volume = None;
t.midi_learn_balance = None;
t.midi_learn_mute = None;
t.midi_learn_solo = None;
t.midi_learn_arm = None;
t.midi_learn_input_monitor = None;
t.midi_learn_disk_monitor = None;
}
}
#[cfg(all(unix, not(target_os = "macos")))]
Action::TrackLv2PluginControls { .. } => {}
#[cfg(all(unix, not(target_os = "macos")))]
Action::ClipLv2PluginControls { .. } => {}
#[cfg(all(unix, not(target_os = "macos")))]
Action::TrackLv2Midnam { .. } => {}
Action::TrackClapNoteNames { .. } => {}
Action::SessionDiagnosticsReport { .. } => {}
Action::MidiLearnMappingsReport { .. } => {}
Action::HWInfo { .. } => {}
Action::HistoryState { .. } => {}
Action::Undo => {}
Action::Redo => {}
Action::ApplyGroupedActions(_) => {}
_ => {}
}
if let Some(inverse) = inverse_actions {
if let Some(group) = self.history_group.as_mut() {
group.forward_actions.push(action_to_process.clone());
group.inverse_actions.splice(0..0, inverse);
} else {
self.history.record(UndoEntry {
forward_actions: vec![action_to_process.clone()],
inverse_actions: inverse,
});
}
}
self.notify_clients(Ok(action_to_process)).await;
}
pub async fn work(&mut self) {
while let Some(message) = self.rx.recv().await {
match message {
Message::Ready(id) => self.push_ready_worker(id),
Message::Finished {
worker_id,
task,
output_linear,
process_epoch,
parameter_updates,
} => {
tracing::debug!(
"engine received Finished from worker {} for task {:?} (epoch {} vs {})",
worker_id,
task,
process_epoch,
self.track_process_epoch
);
self.push_ready_worker(worker_id);
let task_key = Self::task_key(&task);
self.task_processing_started_at.remove(&task_key);
if process_epoch != self.track_process_epoch {
if let Some(track) = self
.state
.lock()
.tracks
.get(&Self::task_track_name(&task))
.cloned()
{
let t = track.lock();
t.audio.finished = false;
t.audio.processing = false;
}
continue;
}
self.cycle_tasks_running
.retain(|t| Self::task_key(t) != task_key);
self.cycle_tasks_finished.push(task.clone());
let track_name = Self::task_track_name(&task);
self.track_meter_linear_by_track
.insert(track_name.clone(), output_linear);
for action in parameter_updates {
self.notify_clients(Ok(action)).await;
}
self.force_stalled_task_completions();
let all_finished = self.send_tasks().await;
tracing::debug!(
"engine after Finished for {}: all_finished={}",
track_name,
all_finished
);
if all_finished {
self.on_all_tracks_finished().await;
}
}
Message::Channel(s) => {
self.clients.push(s);
}
Message::Request(a) => {
match a {
Action::TrackOfflineBounceCancel { track_name } => {
if let Some(job) = self.offline_bounce_jobs.get(&track_name) {
job.cancel.store(true, Ordering::Relaxed);
}
}
Action::TrackOfflineBounceCancelAll => {
for job in self.offline_bounce_jobs.values() {
job.cancel.store(true, Ordering::Relaxed);
}
}
_ if !self.offline_bounce_jobs.is_empty() => {
self.pending_requests.push_back(a);
}
Action::OpenAudioDevice { .. }
| Action::OpenMidiInputDevice(_)
| Action::OpenMidiOutputDevice(_)
| Action::RequestMeterSnapshot
| Action::Quit
| Action::Log { .. }
| Action::Play
| Action::Pause
| Action::Stop
| Action::TransportPosition(_)
| Action::JumpToEnd
| Action::SetLoopEnabled(_)
| Action::SetLoopRange(_)
| Action::SetPunchEnabled(_)
| Action::SetPunchRange(_)
| Action::SetMetronomeEnabled(_)
| Action::SetTempo(_)
| Action::SetTimeSignature { .. }
| Action::SetOscEnabled(_)
| Action::SetClipPlaybackEnabled(_)
| Action::SetRecordEnabled(_)
| Action::SetStepRecording(_)
| Action::StepRecordMidiNote { .. }
| Action::SetSessionPath(_)
| Action::ClearHistory
| Action::BeginSessionRestore
| Action::PianoKey { .. }
| Action::ModifyMidiNotes { .. }
| Action::ModifyMidiControllers { .. }
| Action::DeleteMidiControllers { .. }
| Action::InsertMidiControllers { .. }
| Action::DeleteMidiNotes { .. }
| Action::InsertMidiNotes { .. }
| Action::SetMidiSysExEvents { .. } => {
self.handle_request(a).await;
}
#[cfg(all(unix, not(target_os = "macos")))]
Action::ListLv2Plugins => {
self.handle_request(a).await;
}
Action::ListVst3Plugins => {
self.handle_request(a).await;
}
Action::ListClapPlugins => {
self.handle_request(a).await;
}
Action::ListClapPluginsWithCapabilities => {
self.handle_request(a).await;
}
_ => {
self.pending_requests.push_back(a);
if self.can_schedule_hw_cycle() {
self.request_hw_cycle().await;
} else {
while let Some(next) = self.pending_requests.pop_front() {
self.handle_request(next).await;
}
}
}
};
self.publish_clap_state_dirty().await;
}
Message::OfflineBounceFinished { result } => {
if let Ok(Action::TrackOfflineBounce { track_name, .. }) = &result {
self.offline_bounce_jobs.remove(track_name);
}
self.notify_clients(result).await;
if self.offline_bounce_jobs.is_empty() {
while let Some(next) = self.pending_requests.pop_front() {
self.handle_request(next).await;
}
}
}
Message::HWFinished => {
if !self.awaiting_hwfinished {
tracing::debug!("HWFinished ignored (not awaiting)");
continue;
}
tracing::debug!("HWFinished handling; playing={}", self.playing);
self.handling_hwfinished = true;
self.awaiting_hwfinished = false;
#[cfg(unix)]
{
if let Some(jack) = &self.jack_runtime {
if !self.pending_hw_midi_out_events.is_empty() {
let out_events =
std::mem::take(&mut self.pending_hw_midi_out_events);
jack.lock().write_events(&out_events);
}
let mut in_events = vec![];
jack.lock().read_events_into(&mut in_events);
if !in_events.is_empty() {
self.pending_hw_midi_events.extend(in_events);
}
}
}
#[cfg(unix)]
if self.jack_runtime.is_some() {
self.sync_from_jack_transport().await;
}
while let Some(a) = self.pending_requests.pop_front() {
self.handle_request(a).await;
}
self.apply_mute_solo_policy();
self.append_recorded_cycle();
self.flush_completed_recordings().await;
let hw_in_routes = self.midi_hw_in_routes.clone();
let pending_hw_in_by_device = self.pending_hw_midi_events_by_device.clone();
let mut reconfigured_tracks = Vec::new();
for (track_name, track) in self.state.lock().tracks.iter() {
let track_lock = track.lock();
if self.jack_runtime_is_some() {
if !self.pending_hw_midi_events.is_empty() {
track_lock.push_hw_midi_events(&self.pending_hw_midi_events);
}
} else {
for route in hw_in_routes.iter().filter(|r| &r.to_track == track_name) {
if let Some(events) = pending_hw_in_by_device.get(&route.device) {
track_lock.push_hw_midi_events_to_port(route.to_port, events);
}
}
}
if track_lock.setup() {
reconfigured_tracks.push(track_name.clone());
}
}
self.publish_track_meters().await;
self.publish_clap_state_dirty().await;
for track_name in reconfigured_tracks {
let track = self.state.lock().tracks.get(&track_name).cloned();
if let Some(track) = track {
let (plugins, connections, connectable_connections) = {
let track_lock = track.lock();
(
track_lock.plugin_graph_plugins(),
track_lock.plugin_graph_connections(),
track_lock.connectable_connections(),
)
};
self.notify_clients(Ok(Action::TrackPluginGraph {
track_name: track_name.clone(),
plugins,
connections,
connectable_connections,
}))
.await;
}
}
self.pending_hw_midi_events.clear();
self.pending_hw_midi_events_by_device.clear();
if self.playing {
if self.transport_panic_flush_pending {
self.transport_panic_flush_pending = false;
} else if self.transport_restart_pending {
self.transport_restart_pending = false;
} else {
let next = self
.transport_sample
.saturating_add(self.current_cycle_samples());
let normalized = self.normalize_transport_sample(next);
let wrapped = normalized != next;
self.transport_sample = normalized;
if wrapped {
if self.notified_loop_wrap_sample == Some(self.transport_sample) {
self.notified_loop_wrap_sample = None;
} else {
self.notify_clients(Ok(Action::TransportPosition(
self.transport_sample,
)))
.await;
}
}
}
}
{
let echoes = self.apply_modulators(self.transport_sample);
for action in echoes {
self.notify_clients(Ok(action)).await;
}
}
self.invalidate_track_cycle_state();
let all_finished = self.send_tasks().await;
tracing::debug!(
"HWFinished send_tasks finished={} hw_worker={}",
all_finished,
self.hw_worker.is_some()
);
if all_finished && self.hw_worker.is_some() {
self.request_hw_cycle().await;
}
#[cfg(unix)]
{
if self.jack_runtime.is_some() {
self.awaiting_hwfinished = true;
}
}
self.handling_hwfinished = false;
}
Message::HWMidiEvents(events) => {
for hw_event in events {
let thru_targets: Vec<String> = self
.midi_hw_thru_routes
.iter()
.filter(|route| route.from_device == hw_event.device)
.map(|route| route.to_device.clone())
.collect();
for device in thru_targets {
self.pending_hw_midi_out_events_by_device.push(HwMidiEvent {
device,
event: hw_event.event.clone(),
});
}
if hw_event.event.data.len() >= 3 {
let status = hw_event.event.data[0];
if status & 0xF0 == 0xB0 {
let channel = status & 0x0F;
let cc = hw_event.event.data[1];
let value = hw_event.event.data[2];
self.handle_incoming_hw_cc(&hw_event.device, channel, cc, value)
.await;
}
if self.step_recording_enabled && status & 0xF0 == 0x90 {
let channel = status & 0x0F;
let pitch = hw_event.event.data[1];
let velocity = hw_event.event.data[2];
if velocity > 0 {
self.notify_clients(Ok(Action::StepRecordMidiNote {
device: hw_event.device.clone(),
channel,
pitch,
velocity,
}))
.await;
}
}
}
self.pending_hw_midi_events_by_device
.entry(hw_event.device)
.or_default()
.push(hw_event.event);
}
}
_ => {}
}
}
}
fn collect_hw_midi_output_events(&self) -> Vec<MidiEvent> {
let mut events = vec![];
for track in self.state.lock().tracks.values() {
events.extend(
track
.lock()
.take_hw_midi_out_events()
.into_iter()
.map(|evt| evt.event),
);
}
events.sort_by_key(|a| a.frame);
events
}
fn collect_hw_midi_output_events_by_device(&mut self) -> Vec<HwMidiEvent> {
let mut events = Vec::<HwMidiEvent>::new();
let routes = self.midi_hw_out_routes.clone();
let mut events_by_track = HashMap::<String, Vec<crate::track::HwMidiOutEvent>>::new();
{
let state = self.state.lock();
for route in &routes {
if events_by_track.contains_key(&route.from_track) {
continue;
}
let Some(track) = state.tracks.get(&route.from_track) else {
continue;
};
events_by_track.insert(
route.from_track.clone(),
track.lock().take_hw_midi_out_events(),
);
}
}
for route in routes {
let Some(track_events) = events_by_track.get(&route.from_track) else {
continue;
};
for hw_event in track_events
.iter()
.filter(|evt| evt.port == route.from_port)
{
self.update_active_hw_notes_for_track(
&route.from_track,
&route.device,
&hw_event.event.data,
);
events.push(HwMidiEvent {
device: route.device.clone(),
event: hw_event.event.clone(),
});
}
}
events.sort_by(|a, b| {
a.event
.frame
.cmp(&b.event.frame)
.then_with(|| a.device.cmp(&b.device))
});
events
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mutex::UnsafeMutex;
use tokio::sync::mpsc::channel;
use tokio::time::{Duration as TokioDuration, timeout};
#[test]
#[cfg(unix)]
fn jack_transport_sync_decision_starts_and_syncs_position_on_external_play() {
let decision = Engine::jack_transport_sync_decision(false, 128, true, 256, 64);
assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Start));
assert_eq!(decision.position_sync, Some(256));
}
#[test]
#[cfg(unix)]
fn jack_transport_sync_decision_stops_and_syncs_position_on_external_stop() {
let decision = Engine::jack_transport_sync_decision(true, 512, false, 96, 64);
assert_eq!(decision.play_sync, Some(JackTransportPlaySync::Stop));
assert_eq!(decision.position_sync, Some(96));
}
#[test]
#[cfg(unix)]
fn jack_transport_sync_decision_ignores_small_rolling_drift() {
let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1040, 64);
assert_eq!(decision.play_sync, None);
assert_eq!(decision.position_sync, None);
}
#[test]
#[cfg(unix)]
fn jack_transport_sync_decision_syncs_large_rolling_jump() {
let decision = Engine::jack_transport_sync_decision(true, 1024, true, 1200, 64);
assert_eq!(decision.play_sync, None);
assert_eq!(decision.position_sync, Some(1200));
}
#[test]
#[cfg(unix)]
fn jack_transport_sync_decision_syncs_locate_while_stopped() {
let decision = Engine::jack_transport_sync_decision(false, 400, false, 900, 64);
assert_eq!(decision.play_sync, None);
assert_eq!(decision.position_sync, Some(900));
}
fn make_engine_with_client() -> (Engine, tokio::sync::mpsc::Receiver<Message>) {
let (engine_tx, engine_rx) = channel(16);
let mut engine = Engine::new(engine_rx, engine_tx);
let (client_tx, client_rx) = channel(16);
engine.clients.push(client_tx);
(engine, client_rx)
}
fn insert_track(engine: &mut Engine, track: Track) {
engine.state.lock().tracks.insert(
track.name.clone(),
Arc::new(UnsafeMutex::new(Box::new(track))),
);
}
fn osc_packet(address: &str) -> Vec<u8> {
fn push_padded_osc_string(packet: &mut Vec<u8>, value: &str) {
packet.extend_from_slice(value.as_bytes());
packet.push(0);
while !packet.len().is_multiple_of(4) {
packet.push(0);
}
}
let mut packet = Vec::new();
push_padded_osc_string(&mut packet, address);
push_padded_osc_string(&mut packet, ",");
packet
}
#[tokio::test]
async fn set_osc_enabled_starts_and_stops_server() {
let (mut engine, _client_rx) = make_engine_with_client();
engine
.set_osc_enabled_with(true, |tx| OscServer::start_on_addr(tx, "127.0.0.1:0"))
.expect("start osc server on ephemeral port");
assert!(engine.osc_server.is_some());
engine
.set_osc_enabled_with(false, OscServer::start)
.expect("stop osc server");
assert!(engine.osc_server.is_none());
}
#[tokio::test]
async fn osc_server_forwards_transport_packets_to_engine_channel() {
let (tx, mut rx) = channel(4);
let mut server =
OscServer::start_on_addr(tx, "127.0.0.1:0").expect("start osc test server");
let socket = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind sender socket");
let packet = osc_packet("/transport/play");
socket
.send_to(&packet, server.listen_addr())
.expect("send osc packet");
let message = timeout(TokioDuration::from_secs(1), rx.recv())
.await
.expect("packet delivery timeout")
.expect("osc message");
match message {
Message::Request(Action::Play) => {}
other => panic!("unexpected osc message: {other:?}"),
}
server.stop();
}
#[tokio::test]
async fn track_offline_bounce_rejects_zero_length_requests() {
let (mut engine, mut client_rx) = make_engine_with_client();
insert_track(
&mut engine,
Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
);
engine
.handle_request(Action::TrackOfflineBounce {
track_name: "track".to_string(),
output_path: "/tmp/out.wav".to_string(),
start_sample: 0,
length_samples: 0,
automation_lanes: vec![],
apply_fader: false,
})
.await;
match client_rx.recv().await.expect("response") {
Message::Response(Err(err)) => {
assert!(err.contains("has no renderable content for offline bounce"));
}
other => panic!("unexpected message: {other:?}"),
}
}
#[tokio::test]
async fn track_offline_bounce_rejects_when_same_track_is_active() {
let (mut engine, mut client_rx) = make_engine_with_client();
engine.offline_bounce_jobs.insert(
"other".to_string(),
OfflineBounceJob {
cancel: Arc::new(AtomicBool::new(false)),
},
);
engine
.handle_request(Action::TrackOfflineBounce {
track_name: "other".to_string(),
output_path: "/tmp/out.wav".to_string(),
start_sample: 0,
length_samples: 128,
automation_lanes: vec![],
apply_fader: false,
})
.await;
match client_rx.recv().await.expect("response") {
Message::Response(Err(err)) => {
assert!(err.contains("already in progress"));
}
other => panic!("unexpected message: {other:?}"),
}
}
#[tokio::test]
async fn track_offline_bounce_allows_different_track_concurrently() {
let (mut engine, _client_rx) = make_engine_with_client();
insert_track(
&mut engine,
Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
);
engine.offline_bounce_jobs.insert(
"other".to_string(),
OfflineBounceJob {
cancel: Arc::new(AtomicBool::new(false)),
},
);
engine
.handle_request(Action::TrackOfflineBounce {
track_name: "track".to_string(),
output_path: "/tmp/out.wav".to_string(),
start_sample: 0,
length_samples: 128,
automation_lanes: vec![],
apply_fader: false,
})
.await;
assert!(engine.offline_bounce_jobs.contains_key("other"));
assert_eq!(engine.pending_requests.len(), 1);
}
#[tokio::test]
async fn reject_if_track_frozen_sends_error_and_blocks_operation() {
let (mut engine, mut client_rx) = make_engine_with_client();
let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
track.set_frozen(true);
insert_track(&mut engine, track);
let rejected = engine
.reject_if_track_frozen("track", "arming/disarming")
.await;
assert!(rejected);
match client_rx.recv().await.expect("response") {
Message::Response(Err(err)) => {
assert_eq!(err, "Track 'track' is frozen; arming/disarming is blocked");
}
other => panic!("unexpected message: {other:?}"),
}
}
#[tokio::test]
async fn undo_restores_original_clip_bounds_after_stretch_style_group() {
let (mut engine, _client_rx) = make_engine_with_client();
let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
let mut clip = AudioClip::new("audio/original.wav".to_string(), 100, 220);
clip.offset = 12;
clip.fade_in_samples = 20;
clip.fade_out_samples = 30;
track.audio.clips.push(clip);
insert_track(&mut engine, track);
engine.handle_request(Action::BeginHistoryGroup).await;
engine
.handle_request(Action::SetClipBounds {
track_name: "track".to_string(),
clip_index: 0,
kind: Kind::Audio,
start: 120,
length: 180,
offset: 0,
})
.await;
engine
.handle_request(Action::SetClipSourceName {
track_name: "track".to_string(),
clip_index: 0,
kind: Kind::Audio,
name: "audio/stretched.wav".to_string(),
})
.await;
engine
.handle_request(Action::SetClipFade {
track_name: "track".to_string(),
clip_index: 0,
kind: Kind::Audio,
fade_enabled: true,
fade_in_samples: 12,
fade_out_samples: 12,
})
.await;
engine.handle_request(Action::EndHistoryGroup).await;
engine.handle_request(Action::Undo).await;
let state = engine.state.lock();
let track = state.tracks.get("track").expect("track exists").lock();
let clip = track.audio.clips.first().expect("clip exists");
assert_eq!(clip.name, "audio/original.wav");
assert_eq!(clip.start, 100);
assert_eq!(clip.end, 220);
assert_eq!(clip.end.saturating_sub(clip.start), 120);
assert_eq!(clip.offset, 12);
}
#[tokio::test]
async fn track_offline_bounce_queues_when_no_worker_is_ready() {
let (mut engine, _client_rx) = make_engine_with_client();
insert_track(
&mut engine,
Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
);
engine
.handle_request(Action::TrackOfflineBounce {
track_name: "track".to_string(),
output_path: "/tmp/out.wav".to_string(),
start_sample: 0,
length_samples: 128,
automation_lanes: vec![],
apply_fader: false,
})
.await;
assert!(engine.offline_bounce_jobs.is_empty());
assert_eq!(engine.pending_requests.len(), 1);
assert!(matches!(
engine.pending_requests.front(),
Some(Action::TrackOfflineBounce { track_name, length_samples, .. })
if track_name == "track" && *length_samples == 128
));
}
#[tokio::test]
async fn track_offline_bounce_returns_missing_track_error() {
let (mut engine, mut client_rx) = make_engine_with_client();
engine
.handle_request(Action::TrackOfflineBounce {
track_name: "missing".to_string(),
output_path: "/tmp/out.wav".to_string(),
start_sample: 0,
length_samples: 128,
automation_lanes: vec![],
apply_fader: false,
})
.await;
match client_rx.recv().await.expect("response") {
Message::Response(Err(err)) => {
assert_eq!(err, "Track not found: missing");
}
other => panic!("unexpected message: {other:?}"),
}
}
#[tokio::test]
async fn track_offline_bounce_clears_job_when_worker_send_fails() {
let (mut engine, mut client_rx) = make_engine_with_client();
insert_track(
&mut engine,
Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0),
);
let (worker_tx, worker_rx) = channel(1);
drop(worker_rx);
engine
.workers
.push(WorkerData::new(worker_tx, tokio::spawn(async {})));
engine.ready_workers.push(0);
engine
.handle_request(Action::TrackOfflineBounce {
track_name: "track".to_string(),
output_path: "/tmp/out.wav".to_string(),
start_sample: 0,
length_samples: 128,
automation_lanes: vec![],
apply_fader: false,
})
.await;
assert!(engine.offline_bounce_jobs.is_empty());
match client_rx.recv().await.expect("response") {
Message::Response(Err(err)) => {
assert!(err.contains("Failed to schedule offline bounce"));
}
other => panic!("unexpected message: {other:?}"),
}
}
#[tokio::test]
async fn play_stop_play_keeps_clip_output_audible() {
use crate::audio::clip::AudioClip;
use crate::audio_codec::write_wav_f32;
let (engine_tx, engine_rx) = channel(16);
let mut engine = Engine::new(engine_rx, engine_tx);
let state = engine.state();
let (client_tx, mut client_rx) = channel(16);
engine.clients.push(client_tx);
engine.init().await;
let tmp_dir = std::env::temp_dir().join("maolan_play_stop_play_test");
let _ = std::fs::create_dir_all(&tmp_dir);
let wav_path = tmp_dir.join("tone.wav");
let sample_rate = 48_000u32;
let clip_samples = sample_rate as usize;
let mut samples = Vec::with_capacity(clip_samples);
for i in 0..clip_samples {
let phase = i as f32 / sample_rate as f32 * 2.0 * std::f32::consts::PI * 440.0;
samples.push(phase.sin() * 0.5);
}
write_wav_f32(&wav_path, &samples, 1, sample_rate).expect("write wav");
let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 1024, sample_rate as f64);
let mut clip = AudioClip::new(wav_path.to_string_lossy().to_string(), 0, clip_samples);
clip.fade_enabled = false;
track.audio.clips.push(clip);
track.session_base_dir = Some(tmp_dir.clone());
insert_track(&mut engine, track);
let tx = engine.tx.clone();
let work_handle = tokio::spawn(async move {
engine.work().await;
});
tokio::time::sleep(TokioDuration::from_millis(100)).await;
async fn drain_responses(
client_rx: &mut tokio::sync::mpsc::Receiver<Message>,
count: usize,
) {
for _ in 0..count {
let _ = tokio::time::timeout(TokioDuration::from_secs(2), client_rx.recv()).await;
}
}
async fn wait_for_track_processed(
client_rx: &mut tokio::sync::mpsc::Receiver<Message>,
state: &Arc<UnsafeMutex<State>>,
) -> bool {
let deadline = Instant::now() + Duration::from_secs(5);
while Instant::now() < deadline {
let msg =
tokio::time::timeout(TokioDuration::from_millis(100), client_rx.recv()).await;
if let Ok(Some(Message::Response(Ok(Action::TransportPosition(_)))))
| Ok(Some(Message::Response(Ok(Action::Play)))) = msg
{
let track_deadline = Instant::now() + Duration::from_secs(5);
while Instant::now() < track_deadline {
if state
.lock()
.tracks
.get("track")
.map(|t| t.lock().audio.finished)
.unwrap_or(false)
{
return true;
}
tokio::time::sleep(TokioDuration::from_millis(10)).await;
}
}
}
false
}
tx.send(Message::Request(Action::SetClipPlaybackEnabled(true)))
.await
.unwrap();
tx.send(Message::Request(Action::Play)).await.unwrap();
assert!(
wait_for_track_processed(&mut client_rx, &state).await,
"track did not process on first play"
);
let first_peak = {
let state = state.lock();
let track = state.tracks.get("track").expect("track").lock();
let input = track.audio.ins[0].buffer.lock();
crate::simd::peak_abs(input)
};
assert!(
first_peak > 0.001,
"expected audible input on first play, got {first_peak}"
);
tx.send(Message::Request(Action::SetClipPlaybackEnabled(true)))
.await
.unwrap();
tx.send(Message::Request(Action::Stop)).await.unwrap();
drain_responses(&mut client_rx, 2).await;
tx.send(Message::Request(Action::SetClipPlaybackEnabled(true)))
.await
.unwrap();
tx.send(Message::Request(Action::Play)).await.unwrap();
assert!(
wait_for_track_processed(&mut client_rx, &state).await,
"track did not process on second play"
);
let second_peak = {
let state = state.lock();
let track = state.tracks.get("track").expect("track").lock();
let input = track.audio.ins[0].buffer.lock();
crate::simd::peak_abs(input)
};
assert!(
second_peak > 0.001,
"expected audible input on second play after stop, got {second_peak}"
);
let _ = tx.send(Message::Request(Action::Quit)).await;
tokio::time::sleep(TokioDuration::from_millis(200)).await;
work_handle.abort();
let _ = std::fs::remove_dir_all(&tmp_dir);
}
#[test]
fn modulator_sets_track_volume() {
let (mut engine, _client_rx) = make_engine_with_client();
let track = Track::new("vol-track".to_string(), 0, 2, 0, 0, 128, 48_000.0);
insert_track(&mut engine, track);
engine.modulators = vec![crate::modulator::Modulator {
id: 1,
name: "LFO".to_string(),
shape: crate::modulator::ModulatorShape::Sine,
rate_hz: 1.0,
phase: 0.0,
enabled: true,
targets: vec![crate::modulator::ModulatorTarget::TrackVolume {
track_name: "vol-track".to_string(),
min: -90.0,
max: 20.0,
}],
}];
let echoes = engine.apply_modulators(12_000);
let track = engine.state.lock().tracks["vol-track"].lock();
assert!(
(track.level() - 20.0).abs() < 0.01,
"expected 20 dB, got {}",
track.level()
);
assert!(
echoes
.iter()
.any(|a| matches!(a, Action::TrackAutomationLevel(name, _) if name == "vol-track"))
);
}
#[test]
fn modulator_sets_track_balance() {
let (mut engine, _client_rx) = make_engine_with_client();
let track = Track::new("pan-track".to_string(), 0, 2, 0, 0, 128, 48_000.0);
insert_track(&mut engine, track);
engine.modulators = vec![crate::modulator::Modulator {
id: 1,
name: "LFO".to_string(),
shape: crate::modulator::ModulatorShape::Sine,
rate_hz: 1.0,
phase: 0.0,
enabled: true,
targets: vec![crate::modulator::ModulatorTarget::TrackBalance {
track_name: "pan-track".to_string(),
min: -1.0,
max: 1.0,
}],
}];
let echoes = engine.apply_modulators(12_000);
let track = engine.state.lock().tracks["pan-track"].lock();
assert!(
(track.balance - 1.0).abs() < 0.01,
"expected balance 1.0, got {}",
track.balance
);
assert!(
echoes.iter().any(
|a| matches!(a, Action::TrackAutomationBalance(name, _) if name == "pan-track")
)
);
}
#[tokio::test]
async fn track_set_parent_wires_folder_input_to_child_input_and_child_output_to_folder_output()
{
let (mut engine, mut client_rx) = make_engine_with_client();
let folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
insert_track(&mut engine, folder);
insert_track(&mut engine, child);
engine
.handle_request_inner(
Action::TrackSetParent {
track_name: "child".to_string(),
parent_name: Some("folder".to_string()),
},
false,
)
.await;
while let Ok(Some(_)) =
tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
{}
let state = engine.state.lock();
let folder = state.tracks.get("folder").unwrap().lock();
let child = state.tracks.get("child").unwrap().lock();
assert!(folder.child_tracks.iter().any(|c| c.lock().name == "child"));
assert_eq!(child.parent_track.as_deref(), Some("folder"));
for (i, (parent_in, child_in)) in folder.audio.ins.iter().zip(&child.audio.ins).enumerate()
{
assert!(
child_in
.connections
.lock()
.iter()
.any(|c| Arc::ptr_eq(c, parent_in)),
"folder input {i} is not routed to child input {i}"
);
assert!(
!parent_in
.connections
.lock()
.iter()
.any(|c| Arc::ptr_eq(c, child_in)),
"folder input {i} should not read from child input {i}"
);
}
for (i, (child_out, parent_out)) in
child.audio.outs.iter().zip(&folder.audio.outs).enumerate()
{
assert!(
parent_out
.connections
.lock()
.iter()
.any(|c| Arc::ptr_eq(c, child_out)),
"child output {i} is not routed to folder output {i}"
);
}
for (i, child_out) in child.audio.outs.iter().enumerate() {
assert!(
child_out.connections.lock().iter().any(|c| {
child
.audio
.ins
.get(i)
.is_some_and(|inp| Arc::ptr_eq(c, inp))
}),
"child output {i} is not connected to child input {i}"
);
}
}
#[tokio::test]
async fn track_set_parent_to_none_restores_root_passthrough() {
let (mut engine, mut client_rx) = make_engine_with_client();
let folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
insert_track(&mut engine, folder);
insert_track(&mut engine, child);
engine
.handle_request_inner(
Action::TrackSetParent {
track_name: "child".to_string(),
parent_name: Some("folder".to_string()),
},
false,
)
.await;
engine
.handle_request_inner(
Action::TrackSetParent {
track_name: "child".to_string(),
parent_name: None,
},
false,
)
.await;
while let Ok(Some(_)) =
tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
{}
let state = engine.state.lock();
let folder = state.tracks.get("folder").unwrap().lock();
let child = state.tracks.get("child").unwrap().lock();
assert!(folder.child_tracks.is_empty());
assert!(child.parent_track.is_none());
for (i, child_out) in child.audio.outs.iter().enumerate() {
assert!(
child_out.connections.lock().iter().any(|c| {
child
.audio
.ins
.get(i)
.is_some_and(|inp| Arc::ptr_eq(c, inp))
}),
"child output {i} should be connected to child input {i} after moving to root"
);
}
}
#[tokio::test]
async fn track_set_parent_wires_folder_midi_to_child_midi() {
let (mut engine, mut client_rx) = make_engine_with_client();
let folder = Track::new_folder("folder".to_string(), 0, 0, 1, 1, 64, 48_000.0);
let child = Track::new("child".to_string(), 0, 0, 1, 1, 64, 48_000.0);
insert_track(&mut engine, folder);
insert_track(&mut engine, child);
engine
.handle_request_inner(
Action::TrackSetParent {
track_name: "child".to_string(),
parent_name: Some("folder".to_string()),
},
false,
)
.await;
while let Ok(Some(_)) =
tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
{}
let state = engine.state.lock();
let folder = state.tracks.get("folder").unwrap().lock();
let child = state.tracks.get("child").unwrap().lock();
let folder_midi_in = &folder.midi.ins[0];
let child_midi_in = &child.midi.ins[0];
assert!(
child_midi_in
.lock()
.connections
.iter()
.any(|c| Arc::ptr_eq(c, folder_midi_in)),
"folder MIDI input should be routed to child MIDI input"
);
let child_midi_out = &child.midi.outs[0];
let folder_midi_out = &folder.midi.outs[0];
assert!(
child_midi_out
.lock()
.connections
.iter()
.any(|c| Arc::ptr_eq(c, folder_midi_out)),
"child MIDI output should be routed to folder MIDI output"
);
}
#[test]
fn nested_folder_expands_in_task_graph() {
let (mut engine, _client_rx) = make_engine_with_client();
let outer = Track::new_folder("outer".to_string(), 2, 2, 0, 0, 64, 48_000.0);
let inner = Track::new_folder("inner".to_string(), 2, 2, 0, 0, 64, 48_000.0);
let leaf = Track::new("leaf".to_string(), 2, 2, 0, 0, 64, 48_000.0);
insert_track(&mut engine, outer);
insert_track(&mut engine, inner);
insert_track(&mut engine, leaf);
{
let state = engine.state.lock();
let outer = state.tracks.get("outer").unwrap().clone();
let inner = state.tracks.get("inner").unwrap().clone();
let leaf = state.tracks.get("leaf").unwrap().clone();
outer.lock().child_tracks.push(inner.clone());
inner.lock().child_tracks.push(leaf.clone());
inner.lock().parent_track = Some("outer".to_string());
leaf.lock().parent_track = Some("inner".to_string());
}
let (tasks, deps) = engine.build_task_graph();
let names: Vec<String> = tasks
.iter()
.map(|t| match t {
ProcessTask::Track(t) => format!("track:{}", t.lock().name.clone()),
ProcessTask::FolderInput(t) => format!("in:{}", t.lock().name.clone()),
ProcessTask::FolderOutput(t) => format!("out:{}", t.lock().name.clone()),
ProcessTask::Plugin { track, .. } => {
format!("plugin:{}", track.lock().name.clone())
}
})
.collect();
let expected = vec![
"in:outer",
"in:inner",
"track:leaf",
"out:inner",
"out:outer",
];
assert_eq!(names, expected, "task graph should expand nested folders");
for window in tasks.windows(2) {
let prev = &window[0];
let next = &window[1];
let prev_key = Engine::task_key(prev);
let next_key = Engine::task_key(next);
assert!(
deps.get(&next_key).is_some_and(|d| d.contains(&prev_key)),
"{:?} should depend on {:?}",
next,
prev
);
}
}
#[test]
fn child_to_plugin_to_folder_output_task_graph_has_no_cycle() {
use crate::message::ConnectableRef;
let plugin_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("daw")
.join("plugin-host")
.join("tests")
.join("test_passthrough.clap");
if !plugin_path.exists() {
return;
}
if crate::plugins::ipc::find_plugin_host_binary().is_none() {
return;
}
let (mut engine, _client_rx) = make_engine_with_client();
let mut folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
folder
.load_clap_plugin(
&format!("{}::com.maolan.test.passthrough", plugin_path.display()),
None,
)
.expect("should load CLAP plugin on folder");
folder.clap_plugins[0].processor.lock().setup_audio_ports();
let plugin_id = folder.clap_plugins[0].id;
insert_track(&mut engine, folder);
insert_track(&mut engine, child);
{
let state = engine.state.lock();
let folder = state.tracks.get("folder").unwrap().clone();
let child = state.tracks.get("child").unwrap().clone();
folder.lock().child_tracks.push(child.clone());
child.lock().parent_track = Some("folder".to_string());
folder
.lock()
.connect_audio_connectable(
ConnectableRef::ChildTrack("child".to_string()),
0,
ConnectableRef::ClapPlugin(plugin_id),
0,
)
.expect("connect child L to plugin L");
folder
.lock()
.connect_audio_connectable(
ConnectableRef::ChildTrack("child".to_string()),
1,
ConnectableRef::ClapPlugin(plugin_id),
1,
)
.expect("connect child R to plugin R");
folder
.lock()
.connect_audio_connectable(
ConnectableRef::ClapPlugin(plugin_id),
0,
ConnectableRef::TrackOutput,
0,
)
.expect("connect plugin L to folder output L");
folder
.lock()
.connect_audio_connectable(
ConnectableRef::ClapPlugin(plugin_id),
1,
ConnectableRef::TrackOutput,
1,
)
.expect("connect plugin R to folder output R");
}
let (tasks, deps) = engine.build_task_graph();
let folder_in_key = tasks
.iter()
.find(|t| matches!(t, ProcessTask::FolderInput(t) if t.lock().name == "folder"))
.map(Engine::task_key)
.expect("folder input task");
let child_key = tasks
.iter()
.find(|t| matches!(t, ProcessTask::Track(t) if t.lock().name == "child"))
.map(Engine::task_key)
.expect("child task");
let plugin_key = tasks
.iter()
.find(|t| {
matches!(
t,
ProcessTask::Plugin {
track,
kind: PluginKind::Clap,
index: 0,
} if track.lock().name == "folder"
)
})
.map(Engine::task_key)
.expect("plugin task");
let folder_out_key = tasks
.iter()
.find(|t| matches!(t, ProcessTask::FolderOutput(t) if t.lock().name == "folder"))
.map(Engine::task_key)
.expect("folder output task");
assert!(
deps.get(&child_key)
.is_some_and(|d| d.contains(&folder_in_key)),
"child task should depend on folder input"
);
assert!(
deps.get(&plugin_key)
.is_some_and(|d| d.contains(&folder_in_key) && d.contains(&child_key)),
"plugin task should depend on folder input and child"
);
assert!(
deps.get(&folder_out_key).is_some_and(|d| {
d.contains(&folder_in_key) && d.contains(&plugin_key) && d.contains(&child_key)
}),
"folder output should depend on folder input, plugin, and child"
);
fn has_cycle(deps: &HashMap<String, Vec<String>>) -> bool {
let mut state: HashMap<String, u8> = HashMap::new();
fn visit(
node: &str,
deps: &HashMap<String, Vec<String>>,
state: &mut HashMap<String, u8>,
) -> bool {
match state.get(node).copied() {
Some(1) => return true,
Some(2) => return false,
_ => {}
}
state.insert(node.to_string(), 1);
for next in deps.get(node).into_iter().flatten() {
if visit(next, deps, state) {
return true;
}
}
state.insert(node.to_string(), 2);
false
}
for node in deps.keys() {
if visit(node, deps, &mut state) {
return true;
}
}
false
}
assert!(
!has_cycle(&deps),
"task graph should not contain a cycle when a plugin reads from a child track"
);
}
#[tokio::test]
async fn track_set_parent_wires_child_io_to_folder_even_after_addtrack() {
let (mut engine, mut client_rx) = make_engine_with_client();
let folder = Track::new_folder("folder".to_string(), 2, 2, 0, 0, 64, 48_000.0);
let child = Track::new("child".to_string(), 2, 2, 0, 0, 64, 48_000.0);
insert_track(&mut engine, folder);
insert_track(&mut engine, child);
engine
.handle_request_inner(
Action::TrackSetParent {
track_name: "child".to_string(),
parent_name: Some("folder".to_string()),
},
false,
)
.await;
while let Ok(Some(_)) =
tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
{}
let state = engine.state.lock();
let folder = state.tracks.get("folder").unwrap().lock();
let child = state.tracks.get("child").unwrap().lock();
for (i, (parent_in, child_in)) in folder.audio.ins.iter().zip(&child.audio.ins).enumerate()
{
assert!(
child_in
.connections
.lock()
.iter()
.any(|c| Arc::ptr_eq(c, parent_in)),
"folder input {i} is not routed to child input {i}"
);
}
for (i, (child_out, parent_out)) in
child.audio.outs.iter().zip(&folder.audio.outs).enumerate()
{
assert!(
parent_out
.connections
.lock()
.iter()
.any(|c| Arc::ptr_eq(c, child_out)),
"child output {i} is not routed to folder output {i}"
);
}
}
#[tokio::test]
async fn folder_child_audio_passes_through() {
let (mut engine, mut client_rx) = make_engine_with_client();
let folder = Track::new_folder("folder".to_string(), 1, 1, 0, 0, 64, 48_000.0);
let child = Track::new("child".to_string(), 1, 1, 0, 0, 64, 48_000.0);
insert_track(&mut engine, folder);
insert_track(&mut engine, child);
engine
.handle_request_inner(
Action::TrackSetParent {
track_name: "child".to_string(),
parent_name: Some("folder".to_string()),
},
false,
)
.await;
while let Ok(Some(_)) =
tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
{}
{
let state = engine.state.lock();
let folder = state.tracks.get("folder").unwrap().clone();
let child = state.tracks.get("child").unwrap().clone();
folder.lock().input_monitor = vec![true];
child.lock().input_monitor = vec![true];
let source = Arc::new(crate::audio::io::AudioIO::new(64));
for sample in source.buffer.lock().iter_mut() {
*sample = 0.75;
}
crate::audio::io::AudioIO::connect(&source, &folder.lock().audio.ins[0]);
folder.lock().process_folder_input();
child.lock().process();
folder.lock().process_folder_output();
let output = folder.lock().audio.outs[0].buffer.lock();
assert!(
output.iter().any(|s| (*s - 0.75).abs() < 1e-5),
"folder output should contain the child-processed folder input signal, got {:?}",
output.iter().take(8).collect::<Vec<_>>()
);
}
}
#[tokio::test]
async fn remove_folder_track_deletes_descendants_recursively() {
let (mut engine, mut client_rx) = make_engine_with_client();
let folder = Track::new_folder("folder".to_string(), 1, 1, 0, 0, 64, 48_000.0);
let child = Track::new_folder("child".to_string(), 1, 1, 0, 0, 64, 48_000.0);
let grandchild = Track::new("grandchild".to_string(), 1, 1, 0, 0, 64, 48_000.0);
insert_track(&mut engine, folder);
insert_track(&mut engine, child);
insert_track(&mut engine, grandchild);
engine
.handle_request(Action::TrackSetParent {
track_name: "child".to_string(),
parent_name: Some("folder".to_string()),
})
.await;
engine
.handle_request(Action::TrackSetParent {
track_name: "grandchild".to_string(),
parent_name: Some("child".to_string()),
})
.await;
while let Ok(Some(_)) =
tokio::time::timeout(TokioDuration::from_millis(10), client_rx.recv()).await
{}
engine
.handle_request(Action::RemoveTrack("folder".to_string()))
.await;
{
let state = engine.state.lock();
assert!(
!state.tracks.contains_key("folder"),
"folder should have been removed"
);
assert!(
!state.tracks.contains_key("child"),
"child should have been removed"
);
assert!(
!state.tracks.contains_key("grandchild"),
"grandchild should have been removed"
);
}
let mut removed_names = Vec::new();
for _ in 0..3 {
let msg = tokio::time::timeout(TokioDuration::from_millis(100), client_rx.recv()).await;
if let Ok(Some(Message::Response(Ok(Action::RemoveTrack(name))))) = msg {
removed_names.push(name);
}
}
assert_eq!(
removed_names,
vec!["grandchild", "child", "folder"],
"descendants should be removed before the folder and clients notified"
);
}
}