#![forbid(unsafe_code)]
pub mod cache;
mod midi_takes;
mod plugins;
mod recorder;
#[cfg(test)]
mod tests;
use std::collections::{BTreeMap, HashMap};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, Instant};
use dawkey_backend_pipewire::{
BackendStatus, MidiCaptureRoute, MidiRoutes, PipeWireError, PipeWireOutput,
};
use dawkey_core::{
AddClip, AddMidiClip, AutomationTarget, Command, ControlSpec, Controller, CoreError,
MeterChange, MeterMap, NoteSpec, Outcome, PluginId, Project, Session, SetTempoMap,
SetTimeSignatureMap, SourceId, TempoChange, TimeRange, TimeSignature, TrackId, TrackKind,
};
use dawkey_engine::{
EngineBusy, EngineController, EngineProcessor, Graph, InputFilter, MediaPool, MeterReading,
MidiEvent, MidiRecord, PluginProcessor, RecordShared, TrackMeter, device_tag, new_engine,
render_offline_keeping,
};
use dawkey_media::{Analysis, AudioData, DecodedFile, MidiFile, Peaks, analyze};
use dawkey_plugins::{Catalog, PluginFormat};
use crate::midi_takes::{MidiTake, Span};
use crate::plugins::Plugins;
pub use crate::plugins::{PluginProblem, default_cache as default_plugin_cache};
use crate::recorder::{Recorder, Take, Target};
pub use dawkey_plugins::{
BridgeProgram, Catalog as PluginCatalog, ParamChange, PluginInfo, Sandbox, SearchPath,
run_plugin_host,
};
use serde::Serialize;
pub use dawkey_backend_pipewire::Options as PipeWireOptions;
const VIRTUAL_BLOCK: usize = 512;
pub const MAX_RENDER_S: f64 = 600.0;
pub const MAX_EXPORT_S: f64 = 6.0 * 3600.0;
#[derive(Debug, thiserror::Error)]
pub enum StudioError {
#[error(transparent)]
Core(#[from] CoreError),
#[error(transparent)]
PipeWire(#[from] PipeWireError),
#[error(transparent)]
Engine(#[from] EngineBusy),
#[error("{0}")]
Invalid(String),
}
#[derive(Debug, Clone)]
pub enum AudioBackend {
Virtual,
PipeWire(PipeWireOptions),
}
enum Backend {
Virtual {
processor: Box<EngineProcessor>,
buffers: [Vec<f32>; 2],
},
PipeWire(PipeWireOutput),
}
#[derive(Debug)]
pub struct LoadedProject {
session: Session,
media: MediaPool,
peaks: HashMap<SourceId, Arc<Peaks>>,
missing: BTreeMap<SourceId, String>,
}
impl LoadedProject {
pub fn new(project: Project) -> Self {
Self {
session: Session::new(project),
media: MediaPool::new(),
peaks: HashMap::new(),
missing: BTreeMap::new(),
}
}
pub fn open(path: &Path) -> Result<Self, CoreError> {
Ok(Self::decode(Session::open(path)?))
}
pub fn recover(recovery: &Recovery) -> Result<Self, CoreError> {
Ok(Self::decode(Session::recover(
&recovery.file,
recovery.bundle.clone(),
)?))
}
fn decode(session: Session) -> Self {
let rate = session.project().sample_rate;
let mut media = MediaPool::new();
let mut peaks = HashMap::new();
let mut missing = BTreeMap::new();
for source in &session.project().sources {
let path = session.source_path(source.id).expect("source exists");
match dawkey_media::decode_file(&path, rate) {
Ok(decoded) => {
peaks.insert(source.id, Arc::new(Peaks::compute(&decoded.audio)));
media.insert(source.id, Arc::new(decoded.audio));
}
Err(e) => {
tracing::warn!("source {}: {e}", source.id);
missing.insert(source.id, e.to_string());
}
}
}
Self {
session,
media,
peaks,
missing,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Recovery {
pub file: PathBuf,
pub bundle: Option<PathBuf>,
pub dir: Option<PathBuf>,
#[serde(skip)]
pub written: std::time::SystemTime,
}
impl Recovery {
pub fn age(&self) -> Duration {
self.written.elapsed().unwrap_or_default()
}
}
#[derive(Debug, Clone, Copy, Serialize)]
pub struct TransportState {
pub playing: bool,
pub recording: bool,
pub position_frames: u64,
pub position_s: f64,
}
#[derive(Debug, Clone, Serialize)]
pub struct MissingMedia {
pub source: SourceId,
pub error: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct EngineStatus {
pub backend: &'static str,
pub sample_rate: u32,
pub transport: TransportState,
pub cycles: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub pipewire: Option<BackendStatus>,
pub missing_media: Vec<MissingMedia>,
pub record_latency_ms: f64,
pub record_offset_ms: f64,
pub capturing: bool,
pub midi_capturing: bool,
pub midi_inputs: Vec<String>,
pub midi_outputs: Vec<String>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub midi_output_latency_ms: BTreeMap<String, f64>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub midi_input_latency_ms: BTreeMap<String, f64>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub midi_record_latency_ms: BTreeMap<String, f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_recording: Option<RecordingReport>,
pub plugin_latency_frames: u32,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub plugin_problems: Vec<PluginProblem>,
}
#[derive(Debug, Clone, Serialize)]
pub struct PluginDetails {
pub plugin: PluginId,
pub track: TrackId,
pub id: String,
pub name: String,
pub vendor: String,
pub format: Option<PluginFormat>,
pub instrument: bool,
pub bypass: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub has_sidechain: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub sidechain: Option<TrackId>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub outputs: Vec<OutputState>,
#[serde(skip_serializing_if = "Option::is_none")]
pub state_bytes: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub process_id: Option<u32>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub sandboxed: bool,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub has_gui: bool,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub gui_visible: bool,
pub param_count: usize,
#[serde(skip_serializing_if = "is_zero")]
pub first_param: usize,
pub params: Vec<ParamState>,
}
#[derive(Debug, Clone, Serialize)]
pub struct OutputState {
pub output: u32,
pub name: String,
pub channels: u32,
pub bus: Option<TrackId>,
}
fn is_zero(n: &usize) -> bool {
*n == 0
}
#[derive(Debug, Clone, Serialize)]
pub struct ParamState {
pub id: String,
pub name: String,
pub min: f64,
pub max: f64,
pub default: f64,
pub value: f64,
pub text: String,
pub stepped: bool,
pub automated: bool,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct MidiImport {
pub tracks: Vec<TrackId>,
pub clips: Vec<dawkey_core::ClipId>,
pub notes: usize,
pub used_file_tempo: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct SentMidi {
pub at_s: f64,
pub device: String,
pub data: [u8; 3],
#[serde(skip_serializing_if = "Option::is_none")]
pub ump: Option<[u32; 2]>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct RecordingReport {
pub clips: Vec<dawkey_core::ClipId>,
pub dropped_frames: u64,
pub dropped_midi: u64,
}
struct Recording {
audio: Option<Recorder>,
midi: Option<MidiCapture>,
shared: Arc<RecordShared>,
start: u64,
loop_range: Option<TimeRange>,
punch: Option<TimeRange>,
cached: bool,
}
struct MidiCapture {
ring: rtrb::Consumer<MidiRecord>,
records: Vec<MidiRecord>,
tracks: Vec<(TrackId, InputFilter)>,
}
impl MidiCapture {
fn drain(&mut self) {
while let Ok(record) = self.ring.pop() {
self.records.push(record);
}
}
}
#[derive(Debug)]
pub struct RenderJob {
job: u64,
graph: Graph,
sample_rate: u32,
start: u64,
frames: usize,
}
#[derive(Debug)]
pub struct ExportJob {
job: u64,
render: dawkey_engine::OfflineRender,
sample_rate: u32,
frames: usize,
}
struct Blocks<'a>(&'a mut dawkey_engine::OfflineRender);
impl dawkey_media::BlockSource for Blocks<'_> {
fn read(&mut self, left: &mut [f32], right: &mut [f32]) -> usize {
self.0.render(left, right)
}
}
impl ExportJob {
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
pub fn frames(&self) -> usize {
self.frames
}
pub fn run(
mut self,
spec: &dawkey_media::ExportSpec,
path: &Path,
progress: &dawkey_media::ExportProgress,
) -> (Result<Analysis, dawkey_media::MediaError>, RenderLeftovers) {
let mut blocks = Blocks(&mut self.render);
let result = dawkey_media::export(&mut blocks, self.sample_rate, spec, path, progress);
let mut graph = self.render.into_graph();
let leftovers = RenderLeftovers {
job: self.job,
processors: graph.take_plugins(),
};
(result, leftovers)
}
}
#[derive(Debug)]
pub struct RenderLeftovers {
job: u64,
processors: Vec<(u64, Box<dyn PluginProcessor>)>,
}
impl RenderJob {
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
pub fn run(self) -> (Vec<Vec<f32>>, RenderLeftovers) {
let (audio, mut graph) =
render_offline_keeping(self.graph, self.sample_rate, self.start, self.frames);
let leftovers = RenderLeftovers {
job: self.job,
processors: graph.take_plugins(),
};
(audio, leftovers)
}
}
pub struct Studio {
session: Session,
media: MediaPool,
peaks: HashMap<SourceId, Arc<Peaks>>,
missing: BTreeMap<SourceId, String>,
backend_kind: AudioBackend,
engine: EngineController,
backend: Backend,
synced_revision: Option<u64>,
meters: MeterAccumulator,
recording: Option<Recording>,
last_recording: Option<RecordingReport>,
virtual_input: Option<(AudioData, usize)>,
virtual_midi: Vec<(u64, [u8; 3], u32, u32)>,
sent_midi: Vec<SentMidi>,
virtual_devices: (Vec<String>, Vec<String>),
virtual_midi_latency: BTreeMap<String, f64>,
virtual_midi_input_latency: BTreeMap<String, f64>,
device_latency: BTreeMap<String, f64>,
midi_routes: MidiRoutes,
virtual_clock: u64,
record_offset_ms: f64,
capturing: bool,
midi_capturing: bool,
recording_dir: Option<PathBuf>,
adopted: Option<PathBuf>,
autosaved_revision: Option<u64>,
cached_takes: Vec<PathBuf>,
plugins: Plugins,
plugin_latency: u32,
next_render_job: u64,
}
impl std::fmt::Debug for Studio {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Studio")
.field("session", &self.session)
.field("backend", &self.backend_kind)
.finish_non_exhaustive()
}
}
impl Drop for Studio {
fn drop(&mut self) {
if let Some(recording) = self.recording.take() {
let _ = self.engine.stop_recording();
self.pump();
if let (true, Some(recorder)) = (recording.cached, recording.audio) {
match recorder.finish(Duration::from_millis(200)) {
Ok(takes) => self.cached_takes.extend(takes.into_iter().map(|t| t.path)),
Err(e) => tracing::warn!("finishing the recording: {e}"),
}
}
}
cache::remove(std::mem::take(&mut self.cached_takes));
self.discard_autosave();
if let Some(dir) = self.adopted.take() {
let _ = std::fs::remove_file(dir.join(cache::OWNER));
let _ = std::fs::remove_dir(dir);
}
}
}
#[derive(Debug, Clone, Default)]
pub struct StudioOptions {
pub plugin_scanner: Option<(PathBuf, Vec<String>)>,
pub background_scan: bool,
pub plugin_search: Option<(SearchPath, Option<PathBuf>)>,
pub plugin_bridge: Option<BridgeProgram>,
}
impl Studio {
pub fn new(loaded: LoadedProject, backend: AudioBackend) -> Result<Self, StudioError> {
Self::with_options(loaded, backend, StudioOptions::default())
}
pub fn with_options(
loaded: LoadedProject,
backend: AudioBackend,
options: StudioOptions,
) -> Result<Self, StudioError> {
let rate = loaded.session.project().sample_rate;
let mut plugins = Plugins::new(rate);
if let Some((search, cache)) = options.plugin_search {
plugins.set_search(search, cache);
}
plugins.set_bridge(options.plugin_bridge);
plugins.set_scanner(options.plugin_scanner);
plugins.set_background(options.background_scan);
let (engine, running) = start_backend(rate, &backend)?;
let mut studio = Self {
session: loaded.session,
media: loaded.media,
peaks: loaded.peaks,
missing: loaded.missing,
backend_kind: backend,
engine,
backend: running,
synced_revision: None,
meters: MeterAccumulator::default(),
recording: None,
last_recording: None,
virtual_input: None,
virtual_midi: Vec::new(),
sent_midi: Vec::new(),
virtual_devices: Default::default(),
virtual_midi_latency: BTreeMap::new(),
virtual_midi_input_latency: BTreeMap::new(),
device_latency: BTreeMap::new(),
midi_routes: MidiRoutes::default(),
virtual_clock: 0,
record_offset_ms: 0.0,
capturing: false,
midi_capturing: false,
recording_dir: None,
adopted: None,
autosaved_revision: None,
cached_takes: Vec::new(),
plugins,
plugin_latency: 0,
next_render_job: 1,
};
studio.sync();
Ok(studio)
}
pub fn session(&self) -> &Session {
&self.session
}
pub fn sample_rate(&self) -> u32 {
self.engine.sample_rate()
}
pub fn replace(&mut self, loaded: LoadedProject) -> Result<(), StudioError> {
self.discard_autosave();
if self.recording.is_some()
&& let Err(e) = self.stop()
{
tracing::warn!("finishing the recording: {e}");
}
let rate = loaded.session.project().sample_rate;
self.plugins.reset(rate);
if rate != self.sample_rate() {
let (engine, backend) = start_backend(rate, &self.backend_kind)?;
self.engine = engine;
self.backend = backend;
} else {
if let Err(e) = self.engine.stop().and_then(|()| self.engine.locate(0)) {
tracing::warn!("resetting the transport: {e}");
}
}
self.session = loaded.session;
self.media = loaded.media;
self.peaks = loaded.peaks;
self.missing = loaded.missing;
self.synced_revision = None;
self.sync();
cache::remove(std::mem::take(&mut self.cached_takes));
Ok(())
}
pub fn execute(&mut self, command: Command) -> Result<Outcome, CoreError> {
let command = self.plugins.resolve(self.session.project(), command)?;
self.plugins.check(self.session.project(), &command)?;
let result = self.session.execute(command);
self.sync();
result
}
pub fn execute_merging(
&mut self,
command: Command,
merge_key: &str,
) -> Result<Outcome, CoreError> {
let command = self.plugins.resolve(self.session.project(), command)?;
self.plugins.check(self.session.project(), &command)?;
let result = self.session.execute_merging(command, merge_key);
self.sync();
result
}
pub fn execute_merging_all(
&mut self,
commands: Vec<Command>,
merge_key: &str,
) -> Result<(), CoreError> {
let mut result = Ok(());
for command in commands {
result = self
.plugins
.resolve(self.session.project(), command)
.and_then(|command| {
self.plugins.check(self.session.project(), &command)?;
self.session.execute_merging(command, merge_key)
})
.map(|_| ());
if result.is_err() {
break;
}
}
self.sync();
result
}
pub fn end_merge(&mut self) {
self.session.end_merge();
}
pub fn peaks(&self, source: SourceId) -> Option<Arc<Peaks>> {
self.peaks.get(&source).cloned()
}
pub fn undo(&mut self) -> Result<Option<String>, CoreError> {
let result = self.session.undo();
self.sync();
result
}
pub fn redo(&mut self) -> Result<Option<String>, CoreError> {
let result = self.session.redo();
self.sync();
result
}
pub fn autosave_file(&self) -> PathBuf {
let name = dawkey_core::project_file::AUTOSAVE_NAME;
match self.session.path() {
Some(bundle) => bundle
.join(dawkey_core::project_file::BACKUP_DIR)
.join(name),
None => self.recording_dir().join(name),
}
}
pub fn autosave(&mut self) -> Result<bool, CoreError> {
let revision = self.session.revision();
if !self.session.is_dirty() || self.autosaved_revision == Some(revision) {
return Ok(false);
}
self.capture_plugin_states(false);
self.keep_plugin_files();
self.session.write_autosave(&self.autosave_file())?;
self.autosaved_revision = Some(revision);
Ok(true)
}
pub fn discard_autosave(&mut self) {
let file = self.autosave_file();
match std::fs::remove_file(&file) {
Ok(()) => {
if let Some(dir) = file.parent() {
let _ = std::fs::remove_dir(dir);
}
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => tracing::warn!("removing {}: {e}", file.display()),
}
self.autosaved_revision = None;
}
pub fn bundle_recovery(&self) -> Option<Recovery> {
let bundle = self.session.path()?;
let file = self.autosave_file();
let modified = |p: &Path| std::fs::metadata(p).and_then(|m| m.modified()).ok();
let written = modified(&file)?;
let saved = modified(&bundle.join(dawkey_core::project_file::DOCUMENT_NAME));
(saved.is_none_or(|saved| written >= saved)).then(|| Recovery {
file,
bundle: Some(bundle.to_owned()),
dir: None,
written,
})
}
pub fn orphan_recovery(&self) -> Option<Recovery> {
let root = self
.recording_dir
.as_deref()
.and_then(Path::parent)
.map_or_else(cache::recordings_root, Path::to_owned);
let (dir, written) = cache::orphaned_autosaves(&root).into_iter().next()?;
Some(Recovery {
file: dir.join(dawkey_core::project_file::AUTOSAVE_NAME),
bundle: None,
dir: Some(dir),
written,
})
}
pub fn recover(&mut self, recovery: &Recovery) -> Result<(), StudioError> {
let loaded = LoadedProject::recover(recovery)?;
self.replace(loaded)?;
if let Some(dir) = &recovery.dir {
if let Err(e) = cache::adopt(dir) {
tracing::warn!("taking over {}: {e}", dir.display());
}
let sources = &self.session.project().sources;
self.cached_takes.extend(
sources
.iter()
.filter(|s| s.path.starts_with(dir))
.map(|s| s.path.clone()),
);
self.recording_dir = Some(dir.clone());
self.adopted = Some(dir.clone());
self.sync();
}
self.autosave()?;
Ok(())
}
pub fn discard_recovery(recovery: &Recovery) {
let result = match &recovery.dir {
Some(dir) => std::fs::remove_dir_all(dir),
None => std::fs::remove_file(&recovery.file),
};
if let Err(e) = result {
tracing::warn!("discarding {}: {e}", recovery.file.display());
}
}
pub fn save(&mut self, path: Option<&Path>) -> Result<PathBuf, CoreError> {
if let Some(path) = path {
let bundle = dawkey_core::project_file::bundle_path(path);
self.set_plugin_state_dirs(Some(&bundle));
}
self.capture_plugin_states(false);
let autosave = self.autosave_file();
let saved = self.session.save(path)?;
self.keep_plugin_files();
cache::remove(std::mem::take(&mut self.cached_takes));
let _ = std::fs::remove_file(autosave);
self.discard_autosave();
Ok(saved)
}
pub fn add_source(&mut self, path: &Path, decoded: DecodedFile) -> SourceId {
let name = path
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "Audio".into());
let audio = decoded.audio;
let id = self.session.add_source(
name,
path,
audio.channel_count() as u16,
audio.frames() as u64,
);
self.peaks.insert(id, Arc::new(Peaks::compute(&audio)));
self.media.insert(id, Arc::new(audio));
self.sync();
id
}
pub fn play(&mut self) -> Result<(), StudioError> {
self.engine.play()?;
self.pump();
Ok(())
}
pub fn stop(&mut self) -> Result<(), StudioError> {
let recording = self.recording.take();
if recording.is_some() {
self.engine.stop_recording()?;
}
self.engine.stop()?;
self.pump();
if let Some(recording) = recording {
self.finish_recording(recording)?;
}
Ok(())
}
pub fn record(&mut self) -> Result<(), StudioError> {
if self.recording.is_some() {
return Ok(());
}
let project = self.session.project();
let targets: Vec<Target> = project
.tracks
.iter()
.filter(|t| t.armed && t.kind == TrackKind::Audio)
.map(|t| Target {
track: t.id,
name: t.name.clone(),
channels: t.input,
})
.collect();
let midi_tracks: Vec<(TrackId, InputFilter)> = project
.tracks
.iter()
.filter(|t| t.armed && t.kind == TrackKind::Midi)
.map(|t| {
let filter = InputFilter::new(t.midi_input.as_deref(), t.midi_input_channel);
(t.id, filter)
})
.collect();
if targets.is_empty() && midi_tracks.is_empty() {
return Err(StudioError::Invalid(
"no track is armed for recording (set_track_arm)".into(),
));
}
let channels: Vec<_> = targets.iter().map(|t| t.channels).collect();
let buffer = project.sample_rate as usize * 20;
let punch = project.punch.active();
let loop_range = project.loop_region.active();
let (session, taps) =
dawkey_engine::record_session(&channels, !midi_tracks.is_empty(), punch, buffer);
let audio = if targets.is_empty() {
None
} else {
Some(
Recorder::start(
targets,
taps.audio,
Arc::clone(&taps.shared),
self.recording_dir(),
project.sample_rate,
)
.map_err(StudioError::Invalid)?,
)
};
let midi = taps.midi.map(|ring| MidiCapture {
ring,
records: Vec::new(),
tracks: midi_tracks,
});
self.engine.start_recording(session)?;
self.engine.play()?;
self.recording = Some(Recording {
audio,
midi,
shared: taps.shared,
start: self.engine.transport().position,
loop_range,
punch,
cached: self.session.path().is_none(),
});
self.pump();
Ok(())
}
fn recording_dir(&self) -> PathBuf {
match self.session.path() {
Some(bundle) => bundle.join(dawkey_core::project_file::AUDIO_DIR),
None if self.recording_dir.is_some() => self.recording_dir.clone().expect("checked"),
None => cache::own_dir(),
}
}
fn finish_recording(&mut self, recording: Recording) -> Result<(), StudioError> {
let Recording {
audio,
midi,
shared,
start,
loop_range,
punch,
cached,
} = recording;
let takes = match audio {
Some(recorder) => recorder
.finish(Duration::from_millis(500))
.map_err(StudioError::Invalid)?,
None => {
wait_finished(&shared, Duration::from_millis(500));
Vec::new()
}
};
let dropped = shared.dropped_frames();
if dropped > 0 {
tracing::warn!("{dropped} recorded frames were lost (the disk fell behind)");
}
let dropped_midi = shared.dropped_midi();
if dropped_midi > 0 {
tracing::warn!("{dropped_midi} MIDI messages were lost while recording");
}
let midi_takes = match midi {
Some(mut capture) => {
capture.drain();
let span = Span {
start,
stop: self.engine.transport().position,
loop_range,
punch,
};
let mut inputs: Vec<InputFilter> = Vec::new();
for (_, input) in &capture.tracks {
if !inputs.contains(input) {
inputs.push(*input);
}
}
inputs
.into_iter()
.map(|input| {
let tracks = capture
.tracks
.iter()
.filter(|(_, i)| *i == input)
.map(|(t, _)| *t)
.collect();
(tracks, midi_takes::takes(&capture.records, span, input))
})
.collect()
}
None => Vec::new(),
};
if cached {
self.cached_takes
.extend(takes.iter().map(|t| t.path.clone()));
}
let clips = self.place_takes(takes, midi_takes)?;
self.last_recording = Some(RecordingReport {
clips,
dropped_frames: dropped,
dropped_midi,
});
Ok(())
}
fn place_takes(
&mut self,
takes: Vec<Take>,
midi: Vec<(Vec<TrackId>, Vec<MidiTake>)>,
) -> Result<Vec<dawkey_core::ClipId>, StudioError> {
let rate = self.sample_rate();
let latency = self.record_latency_frames();
let midi_latency = self.midi_record_latency_frames();
let mut commands = Vec::new();
for (i, take) in takes.iter().enumerate() {
if take.frames == 0 {
continue;
}
let decoded = dawkey_media::decode_file(&take.path, rate)
.map_err(|e| StudioError::Invalid(e.to_string()))?;
let source = self.add_source(&take.path, decoded);
let start = take.start as i64 - latency;
let (position, offset) = if start >= 0 {
(start as u64, 0)
} else {
(0, (-start) as u64)
};
if offset >= take.frames {
continue;
}
let end = take.start + take.frames;
let overlapped = takes[i + 1..].iter().any(|later| {
later.track == take.track
&& later.start < end
&& take.start < later.start + later.frames
});
let project = self.session.project();
commands.push(Command::from(AddClip {
muted: overlapped,
take: true,
offset_s: project.frames_to_seconds(offset),
..AddClip::new(
source,
Some(take.track),
project.frames_to_seconds(position),
)
}));
}
let project = self.session.project();
for (tracks, takes) in &midi {
for (i, take) in takes.iter().enumerate() {
let overlapped = takes[i + 1..]
.iter()
.any(|later| later.start < take.end && take.start < later.end);
let Some(clip) = midi_clip(project, take, &midi_latency, overlapped) else {
continue;
};
commands.extend(tracks.iter().map(|&track| {
Command::from(AddMidiClip {
track: Some(track),
..clip.clone()
})
}));
}
}
if commands.is_empty() {
return Ok(Vec::new());
}
let outcomes = self.session.execute_batch("record", commands)?;
self.sync();
Ok(outcomes.into_iter().filter_map(|o| o.clip).collect())
}
pub fn import_midi(
&mut self,
file: &MidiFile,
position_s: f64,
use_file_tempo: Option<bool>,
) -> Result<MidiImport, StudioError> {
if file.tracks.is_empty() {
return Err(StudioError::Invalid("the MIDI file has no notes".into()));
}
let project = self.session.project();
let empty = project.tracks.iter().all(|t| t.clips.is_empty());
let adopt = use_file_tempo.unwrap_or(empty);
let mut commands = Vec::new();
if adopt {
let first = file
.tempo
.first()
.filter(|(beat, _)| *beat <= 0.0)
.map_or(120.0, |&(_, bpm)| bpm);
let offset = position_s * first / 60.0;
commands.push(Command::from(SetTempoMap {
bpm: first,
changes: file
.tempo
.iter()
.filter(|(beat, _)| *beat > 0.0)
.map(|&(beat, bpm)| TempoChange::new(beat + offset, bpm))
.collect(),
}));
commands.push(meter_map_of(file, offset).into());
}
for track in &file.tracks {
let notes = track
.notes
.iter()
.map(|n| NoteSpec {
pitch: n.pitch.min(127),
start_beats: n.start_beats,
length_beats: n.length_beats,
velocity: n.velocity.clamp(1, 127),
})
.collect();
let controls = track
.controls
.iter()
.filter_map(|c| {
let (controller, value) = Controller::from_message(c.data)?;
Some(ControlSpec {
controller,
time_beats: c.beat,
value,
})
})
.collect();
commands.push(
AddMidiClip {
track_name: Some(track.name.clone()),
position_s,
notes,
controls,
..Default::default()
}
.into(),
);
}
let outcomes = self.session.execute_batch("import_midi", commands)?;
self.sync();
let mut import = MidiImport {
used_file_tempo: adopt,
..MidiImport::default()
};
for outcome in outcomes {
if let Some(clip) = outcome.clip {
import.clips.push(clip);
import.tracks.extend(outcome.track);
import.notes += outcome.notes.len();
}
}
Ok(import)
}
pub fn set_recording_dir(&mut self, dir: PathBuf) {
self.recording_dir = Some(dir);
}
pub fn record_start_s(&self) -> Option<f64> {
self.recording
.as_ref()
.map(|r| self.session.project().frames_to_seconds(r.start))
}
fn record_latency_frames(&self) -> i64 {
let estimate = match &self.backend {
Backend::Virtual { .. } => 0,
Backend::PipeWire(output) => i64::from(output.round_trip_estimate()),
};
estimate + self.record_correction_frames()
}
fn midi_record_latency_ms(&self) -> BTreeMap<String, f64> {
let latency = self.midi_record_latency_frames();
let rate = f64::from(self.sample_rate());
self.midi_devices()
.0
.into_iter()
.map(|device| {
let ms = latency(device_tag(&device)) as f64 * 1000.0 / rate;
(device, ms)
})
.collect()
}
fn record_correction_frames(&self) -> i64 {
i64::from(self.plugin_latency) + self.ms_to_frames(self.record_offset_ms)
}
fn ms_to_frames(&self, ms: f64) -> i64 {
(ms * f64::from(self.sample_rate()) / 1000.0).round() as i64
}
fn midi_record_latency_frames(&self) -> impl Fn(u32) -> i64 + use<> {
let (playback, fallback) = match &self.backend {
Backend::Virtual { .. } => (0, 0),
Backend::PipeWire(output) => (
i64::from(output.playback_latency()),
i64::from(output.quantum()),
),
};
let correction = self.record_correction_frames();
let reported: HashMap<u32, i64> = self
.reported_midi_input_latency()
.iter()
.map(|(device, &ms)| (device_tag(device), self.ms_to_frames(ms)))
.collect();
move |source| playback + correction + reported.get(&source).copied().unwrap_or(fallback)
}
pub fn set_record_offset_ms(&mut self, ms: f64) -> Result<(), StudioError> {
if !(-1000.0..=1000.0).contains(&ms) {
return Err(StudioError::Invalid(format!(
"offset must be between -1000 and 1000 ms, got {ms}"
)));
}
self.record_offset_ms = ms;
Ok(())
}
pub fn set_virtual_input(&mut self, audio: Option<AudioData>) -> Result<(), StudioError> {
if !matches!(self.backend, Backend::Virtual { .. }) {
return Err(StudioError::Invalid(
"input can only be injected with the virtual backend".into(),
));
}
self.virtual_input = audio.map(|a| (a, 0));
Ok(())
}
pub fn queue_virtual_midi(
&mut self,
device: Option<&str>,
events: &[(f64, [u8; 3], u32)],
) -> Result<(), StudioError> {
let source = device.map_or(0, device_tag);
if !matches!(self.backend, Backend::Virtual { .. }) {
return Err(StudioError::Invalid(
"MIDI can only be injected with the virtual backend".into(),
));
}
let rate = f64::from(self.sample_rate());
for &(at_s, data, value) in events {
if !(0.0..=MAX_RENDER_S).contains(&at_s) {
return Err(StudioError::Invalid(format!(
"MIDI times must be between 0 and {MAX_RENDER_S} s, got {at_s}"
)));
}
let at = self.virtual_clock + (at_s * rate).round() as u64;
let index = self.virtual_midi.partition_point(|&(t, ..)| t <= at);
self.virtual_midi.insert(index, (at, data, source, value));
}
Ok(())
}
pub fn locate(&mut self, seconds: f64) -> Result<(), StudioError> {
if !(0.0..=86_400.0).contains(&seconds) {
return Err(StudioError::Invalid(format!(
"position_s must be between 0 and 86400, got {seconds}"
)));
}
let frame = self.session.project().seconds_to_frames(seconds);
self.engine.locate(frame)?;
self.pump();
Ok(())
}
pub fn transport(&self) -> TransportState {
let t = self.engine.transport();
TransportState {
playing: t.playing,
recording: t.recording,
position_frames: t.position,
position_s: self.session.project().frames_to_seconds(t.position),
}
}
pub fn take_meters(&mut self) -> MeterReading {
self.drain_meters();
self.meters.take_since_last_call()
}
pub fn display_meters(&mut self) -> DisplayMeters {
self.drain_meters();
self.meters.display(Instant::now())
}
fn drain_meters(&mut self) {
let reading = self.engine.take_meters();
self.meters.add(&reading);
}
pub fn engine_status(&self) -> EngineStatus {
let (backend, pipewire) = match &self.backend {
Backend::Virtual { .. } => ("virtual", None),
Backend::PipeWire(output) => ("pipewire", Some(output.status())),
};
EngineStatus {
backend,
sample_rate: self.sample_rate(),
transport: self.transport(),
cycles: self.engine.transport().cycles,
pipewire,
missing_media: self
.missing
.iter()
.map(|(&source, error)| MissingMedia {
source,
error: error.clone(),
})
.collect(),
record_latency_ms: self.record_latency_frames() as f64 * 1000.0
/ f64::from(self.sample_rate()),
record_offset_ms: self.record_offset_ms,
capturing: self.capturing,
midi_capturing: self.midi_capturing,
midi_inputs: self.midi_devices().0,
midi_outputs: self.midi_devices().1,
midi_output_latency_ms: self.reported_midi_latency(),
midi_input_latency_ms: self.reported_midi_input_latency(),
midi_record_latency_ms: self.midi_record_latency_ms(),
last_recording: self.last_recording.clone(),
plugin_latency_frames: self.plugin_latency,
plugin_problems: self.plugins.problems(),
}
}
pub fn render_job(&mut self, start_s: f64, end_s: f64) -> Result<RenderJob, StudioError> {
let (job, graph, start, frames) = self.offline_graph(start_s, end_s, MAX_RENDER_S)?;
Ok(RenderJob {
job,
graph,
sample_rate: self.session.project().sample_rate,
start,
frames,
})
}
pub fn export_job(&mut self, start_s: f64, end_s: f64) -> Result<ExportJob, StudioError> {
let (job, graph, start, frames) = self.offline_graph(start_s, end_s, MAX_EXPORT_S)?;
let sample_rate = self.session.project().sample_rate;
Ok(ExportJob {
job,
render: dawkey_engine::OfflineRender::new(graph, sample_rate, start, frames),
sample_rate,
frames,
})
}
fn offline_graph(
&mut self,
start_s: f64,
end_s: f64,
max_s: f64,
) -> Result<(u64, Graph, u64, usize), StudioError> {
let project = self.session.project();
if !(start_s >= 0.0 && end_s > start_s && end_s - start_s <= max_s) {
return Err(StudioError::Invalid(format!(
"need 0 <= start_s < end_s and at most {max_s} s, got {start_s}..{end_s}"
)));
}
let start = project.seconds_to_frames(start_s);
let frames = (project.seconds_to_frames(end_s) - start) as usize;
self.capture_plugin_states(false);
let project = self.session.project();
let job = self.next_render_job;
self.next_render_job += 1;
let mut provider = self.plugins.offline_provider(job, &project.plugin_states);
let graph =
Graph::compile_with_devices(project, &self.media, &mut provider, &self.device_latency);
Ok((job, graph, start, frames))
}
pub fn finish_render(&mut self, leftovers: RenderLeftovers) {
self.plugins
.finish_offline(leftovers.job, leftovers.processors);
}
pub fn plugin_catalog(&mut self) -> &Catalog {
self.plugins.catalog()
}
pub fn plugin_catalog_now(&mut self) -> Option<&Catalog> {
self.plugins.catalog_now()
}
pub fn scanning_plugins(&self) -> bool {
self.plugins.scanning()
}
pub fn set_background_scan(&mut self, background: bool) {
self.plugins.set_background(background);
}
pub fn rescan_plugins_in_background(&mut self) {
self.plugins.rescan_in_background();
if !self.plugins.scanning() {
self.synced_revision = None;
self.sync();
}
}
pub fn cached_catalog(&self) -> Option<&Catalog> {
self.plugins.cached()
}
pub fn rescan_plugins(&mut self) -> &Catalog {
self.plugins.rescan();
self.synced_revision = None;
self.sync();
self.plugins.catalog()
}
pub fn set_plugin_search(&mut self, search: SearchPath, cache: Option<PathBuf>) {
self.plugins.set_search(search, cache);
}
pub fn set_plugin_bridge(&mut self, program: Option<BridgeProgram>) {
self.plugins.set_bridge(program);
}
pub fn show_plugin_gui(&mut self, plugin: PluginId, show: bool) -> Result<(), StudioError> {
Ok(self.plugins.show_gui(plugin, show)?)
}
pub fn take_plugin_param_changes(&mut self) -> Vec<(PluginId, ParamChange)> {
self.plugins.take_param_changes()
}
pub fn plugin_param_id(&mut self, plugin: PluginId, index: usize) -> Option<String> {
let (_, slot) = self.session.project().plugin(plugin)?;
let id = slot.plugin.clone();
Some(self.plugins.info(&id)?.params.get(index)?.id.clone())
}
pub fn reload_plugin(&mut self, plugin: PluginId) -> Result<(), StudioError> {
if self.session.project().plugin(plugin).is_none() {
return Err(StudioError::Invalid(format!(
"plugin {plugin} does not exist"
)));
}
self.plugins.reload(plugin);
self.synced_revision = None;
self.sync();
Ok(())
}
pub fn change_plugin_search(&mut self, search: SearchPath) {
self.plugins.change_search(search);
self.rescan_plugins_in_background();
}
pub fn set_plugin_scanner(&mut self, program: PathBuf, args: Vec<String>) {
self.plugins.set_scanner(Some((program, args)));
}
pub fn plugin_details(&mut self, id: PluginId) -> Option<PluginDetails> {
self.plugin_details_window(id, 0, usize::MAX)
}
pub fn plugin_param(&mut self, id: PluginId, param: &str) -> Option<ParamState> {
let (_, slot) = self.session.project().plugin(id)?;
let index = self
.plugins
.info(&slot.plugin.clone())?
.param_index(param)?;
self.plugin_details_window(id, index, 1)?.params.pop()
}
pub fn plugin_details_window(
&mut self,
id: PluginId,
first: usize,
count: usize,
) -> Option<PluginDetails> {
let (track, slot) = self.session.project().plugin(id)?;
let (track_id, slot) = (track.id, slot.clone());
let now = self.engine.transport().position;
let automated: Vec<(String, Option<f64>)> = track
.automation
.iter()
.filter_map(|l| match &l.target {
AutomationTarget::Param { plugin, param } if *plugin == id => {
Some((param.clone(), l.value_at(now)))
}
_ => None,
})
.collect();
let error = self
.plugins
.problems()
.into_iter()
.find(|p| p.plugin == id)
.map(|p| p.error);
let info = self.plugins.info(&slot.plugin).cloned();
let params = info
.as_ref()
.map(|i| {
i.params
.iter()
.enumerate()
.skip(first)
.take(count)
.map(|(index, p)| {
let lane = automated.iter().find(|(param, _)| *param == p.id);
let value = lane
.and_then(|(_, v)| *v)
.or_else(|| slot.params.get(&p.id).copied())
.unwrap_or(p.default);
ParamState {
id: p.id.clone(),
name: p.name.clone(),
min: p.min,
max: p.max,
default: p.default,
value,
text: self.plugins.param_text(id, i, index, value),
stepped: p.stepped,
automated: lane.is_some(),
}
})
.collect()
})
.unwrap_or_default();
Some(PluginDetails {
plugin: id,
track: track_id,
id: slot.plugin.clone(),
name: info
.as_ref()
.map_or_else(|| slot.plugin.clone(), |i| i.name.clone()),
vendor: info.as_ref().map(|i| i.vendor.clone()).unwrap_or_default(),
format: info.as_ref().map(|i| i.format),
instrument: info.as_ref().is_some_and(|i| i.instrument),
bypass: slot.bypass,
error: error.or_else(|| info.is_none().then(|| "not installed".to_owned())),
has_sidechain: info.as_ref().is_some_and(|i| i.sidechain),
sidechain: slot.sidechain,
outputs: info
.as_ref()
.map(|i| {
i.outputs
.iter()
.enumerate()
.map(|(n, o)| OutputState {
output: n as u32,
name: o.name.clone(),
channels: o.channels,
bus: slot.outputs.get(&(n as u32)).copied(),
})
.collect()
})
.unwrap_or_default(),
state_bytes: self
.session
.project()
.plugin_states
.get(&id)
.map(|s| s.bytes().len()),
process_id: self.plugins.process_id(id),
sandboxed: self.plugins.sandboxed(id),
has_gui: self.plugins.gui(id).0,
gui_visible: self.plugins.gui(id).1,
param_count: info.as_ref().map_or(0, |i| i.params.len()),
first_param: first,
params,
})
}
pub fn advance(&mut self, seconds: f64) -> Result<Analysis, StudioError> {
if !(seconds > 0.0 && seconds <= MAX_RENDER_S) {
return Err(StudioError::Invalid(format!(
"seconds must be > 0 and at most {MAX_RENDER_S}, got {seconds}"
)));
}
let rate = self.sample_rate();
let frames = self.session.project().seconds_to_frames(seconds).max(1) as usize;
let Backend::Virtual { processor, buffers } = &mut self.backend else {
return Err(StudioError::Invalid(
"the clock only advances by hand with the virtual backend; \
this instance plays through PipeWire"
.into(),
));
};
for buffer in buffers.iter_mut() {
buffer.clear();
buffer.resize(frames, 0.0);
}
let mut input = [vec![0.0; frames], vec![0.0; frames]];
if let Some((audio, cursor)) = &mut self.virtual_input {
let available = audio.frames().saturating_sub(*cursor).min(frames);
let right = if audio.channel_count() > 1 { 1 } else { 0 };
input[0][..available].copy_from_slice(&audio.channel(0)[*cursor..*cursor + available]);
input[1][..available]
.copy_from_slice(&audio.channel(right)[*cursor..*cursor + available]);
*cursor += frames;
}
let [left, right] = buffers;
let chunks = left
.chunks_mut(VIRTUAL_BLOCK)
.zip(right.chunks_mut(VIRTUAL_BLOCK))
.zip(
input[0]
.chunks(VIRTUAL_BLOCK)
.zip(input[1].chunks(VIRTUAL_BLOCK)),
);
let mut midi = Vec::new();
let devices: HashMap<u32, String> = self
.session
.project()
.tracks
.iter()
.filter_map(|t| t.midi_output.as_ref())
.map(|o| (device_tag(&o.device), o.device.clone()))
.collect();
let started = self.virtual_clock;
self.sent_midi.clear();
for ((l, r), (in_l, in_r)) in chunks {
let (from, to) = (self.virtual_clock, self.virtual_clock + l.len() as u64);
let due = self.virtual_midi.partition_point(|&(t, ..)| t < to);
midi.clear();
midi.extend(
self.virtual_midi
.drain(..due)
.map(|(t, data, source, value)| MidiEvent {
offset: (t.max(from) - from) as u32,
data,
source,
value,
}),
);
processor.process_with_input([in_l, in_r], &midi, l, r);
for sent in processor.midi_output() {
let frame = from - started + u64::from(sent.offset);
self.sent_midi.push(SentMidi {
at_s: frame as f64 / f64::from(rate),
device: devices.get(&sent.dest).cloned().unwrap_or_default(),
data: sent.data,
ump: sent.ump(),
});
}
processor.clear_midi_output();
self.virtual_clock = to;
}
self.engine.collect_garbage();
if let Some(capture) = self.recording.as_mut().and_then(|r| r.midi.as_mut()) {
capture.drain();
}
Ok(analyze(&buffers[..], rate))
}
pub fn midi_devices(&self) -> (Vec<String>, Vec<String>) {
match &self.backend {
Backend::Virtual { .. } => self.virtual_devices.clone(),
Backend::PipeWire(output) => {
let status = output.status();
(status.midi_inputs, status.midi_outputs)
}
}
}
pub fn set_virtual_midi_devices(&mut self, inputs: Vec<String>, outputs: Vec<String>) {
self.virtual_devices = (inputs, outputs);
}
pub fn reported_midi_latency(&self) -> BTreeMap<String, f64> {
match &self.backend {
Backend::Virtual { .. } => self.virtual_midi_latency.clone(),
Backend::PipeWire(output) => output.midi_output_latency_ms(),
}
}
pub fn reported_midi_input_latency(&self) -> BTreeMap<String, f64> {
match &self.backend {
Backend::Virtual { .. } => self.virtual_midi_input_latency.clone(),
Backend::PipeWire(output) => output.midi_input_latency_ms(),
}
}
pub fn set_virtual_midi_input_latency(&mut self, device: &str, ms: Option<f64>) {
match ms {
Some(ms) => self
.virtual_midi_input_latency
.insert(device.to_owned(), ms),
None => self.virtual_midi_input_latency.remove(device),
};
}
pub fn set_virtual_midi_latency(&mut self, device: &str, ms: Option<f64>) {
match ms {
Some(ms) => self.virtual_midi_latency.insert(device.to_owned(), ms),
None => self.virtual_midi_latency.remove(device),
};
self.sync();
}
pub fn sent_midi(&self) -> &[SentMidi] {
&self.sent_midi
}
pub fn tick(&mut self) {
if let Some(capture) = self.recording.as_mut().and_then(|r| r.midi.as_mut()) {
capture.drain();
}
if self.plugins.idle() {
self.synced_revision = None;
self.sync();
}
self.capture_plugin_states(true);
if self.plugins.poll() {
self.synced_revision = None;
}
self.sync();
}
fn capture_plugin_states(&mut self, dirty_only: bool) {
for (id, state) in self.plugins.capture(dirty_only) {
self.session.set_plugin_state(id, Some(state));
}
}
fn set_plugin_state_dirs(&mut self, bundle: Option<&Path>) {
let absolute = |p: &Path| std::path::absolute(p).unwrap_or_else(|_| p.to_owned());
let base = bundle.map(absolute);
let files = absolute(&self.plugin_files_dir());
self.plugins.set_state_dirs(base, files);
}
fn plugin_files_dir(&self) -> PathBuf {
self.recording_dir
.clone()
.unwrap_or_else(cache::own_dir)
.join(dawkey_core::project_file::PLUGIN_DIR)
}
fn keep_plugin_files(&self) {
let Some(bundle) = self.session.path() else {
return;
};
let to = bundle.join(dawkey_core::project_file::PLUGIN_DIR);
if let Err(e) = copy_tree(&self.plugin_files_dir(), &to) {
tracing::warn!("copying plugins' files into {}: {e}", to.display());
}
}
fn sync(&mut self) {
let bundle = self.session.path().map(Path::to_owned);
self.set_plugin_state_dirs(bundle.as_deref());
let block = match &self.backend {
Backend::Virtual { .. } => VIRTUAL_BLOCK as u32,
Backend::PipeWire(output) => match output.quantum() {
0 => 1024,
quantum => quantum,
},
};
self.plugins.set_bridge_block(block);
self.plugins
.set_real_time(!matches!(self.backend, Backend::Virtual { .. }));
let armed = |kind| {
self.session
.project()
.tracks
.iter()
.any(|t| t.armed && t.kind == kind)
};
let (audio, midi) = (armed(TrackKind::Audio), armed(TrackKind::Midi));
if audio != self.capturing {
self.capturing = audio;
if let Backend::PipeWire(output) = &self.backend {
output.set_capture(audio);
}
}
self.midi_capturing = midi;
let routes = midi_routes(self.session.project());
if routes != self.midi_routes {
if let Backend::PipeWire(output) = &self.backend {
output.set_midi_routes(routes.clone());
}
self.midi_routes = routes;
}
if self.plugins.take_back(self.engine.take_retired_plugins()) {
self.synced_revision = None;
}
let reported = self.reported_midi_latency();
if reported != self.device_latency {
self.device_latency = reported;
self.synced_revision = None;
}
let revision = self.session.revision();
if self.synced_revision == Some(revision) || self.engine.graphs_in_flight() > 0 {
return;
}
let project = self.session.project();
let graph = Graph::compile_with_devices(
project,
&self.media,
&mut self.plugins.provider(&project.plugin_states),
&self.device_latency,
);
let left = self.plugins.forget_missing(project);
for (id, state) in left {
self.session.set_plugin_state(id, Some(state));
}
let latency = graph.latency();
match self.engine.set_graph(graph) {
Ok(()) => {
self.synced_revision = Some(revision);
self.plugin_latency = latency;
}
Err(e) => tracing::warn!("engine graph not updated: {e}"),
}
self.pump();
if self.plugins.take_back(self.engine.take_retired_plugins()) {
self.synced_revision = None;
}
}
fn pump(&mut self) {
if let Backend::Virtual { processor, .. } = &mut self.backend {
processor.process(&mut [], &mut []);
self.engine.collect_garbage();
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct DisplayMeters {
pub master: [f32; 2],
pub tracks: HashMap<TrackId, [f32; 2]>,
}
#[derive(Debug, Default)]
struct MeterAccumulator {
since_last_call: Option<MeterReading>,
display: DisplayMeters,
display_at: Option<Instant>,
}
impl MeterAccumulator {
fn add(&mut self, reading: &MeterReading) {
match &mut self.since_last_call {
None => self.since_last_call = Some(reading.clone()),
Some(acc) => {
acc.master = max_db(acc.master, reading.master);
for meter in &reading.tracks {
match acc.tracks.iter_mut().find(|m| m.track == meter.track) {
Some(m) => m.peak_dbfs = max_db(m.peak_dbfs, meter.peak_dbfs),
None => acc.tracks.push(meter.clone()),
}
}
}
}
let linear =
|db: [Option<f64>; 2]| db.map(|d| d.map_or(0.0, |d| 10f32.powf(d as f32 / 20.0)));
let max = |a: [f32; 2], b: [f32; 2]| [a[0].max(b[0]), a[1].max(b[1])];
self.display.master = max(self.display.master, linear(reading.master));
for TrackMeter { track, peak_dbfs } in &reading.tracks {
let level = self.display.tracks.entry(*track).or_default();
*level = max(*level, linear(*peak_dbfs));
}
}
fn take_since_last_call(&mut self) -> MeterReading {
self.since_last_call.take().unwrap_or(MeterReading {
master: [None, None],
tracks: Vec::new(),
})
}
fn display(&mut self, now: Instant) -> DisplayMeters {
let shown = self.display.clone();
let dt = self
.display_at
.map_or(0.0, |t| now.duration_since(t).as_secs_f32());
self.display_at = Some(now);
let factor = 10f32.powf(-dt);
let decay = |l: &mut [f32; 2]| l.iter_mut().for_each(|v| *v *= factor);
decay(&mut self.display.master);
self.display.tracks.values_mut().for_each(decay);
shown
}
}
fn wait_finished(shared: &RecordShared, wait: Duration) {
let deadline = Instant::now() + wait;
while !shared.finished() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(1));
}
if !shared.finished() {
tracing::warn!("the audio thread didn't confirm the end of recording");
shared.finish();
}
}
fn midi_clip(
project: &Project,
take: &MidiTake,
latency: &impl Fn(u32) -> i64,
muted: bool,
) -> Option<AddMidiClip> {
let sources = take
.notes
.iter()
.map(|n| n.source)
.chain(take.controls.iter().map(|c| c.source));
let (most, least) = sources.fold((i64::MIN, i64::MAX), |(most, least), source| {
let latency = latency(source);
(most.max(latency), least.min(latency))
});
let start = (take.start as i64 - most).max(0) as u64;
let end = take.end as i64 - least;
if end <= start as i64 {
return None;
}
let shift = |frame: u64, source: u32| frame as i64 - latency(source);
let map = project.tempo_map();
let notes: Vec<NoteSpec> = take
.notes
.iter()
.filter(|n| shift(n.start, n.source) >= start as i64)
.map(|n| {
let from = shift(n.start, n.source) as u64 - start;
let to = (shift(n.end, n.source).max(shift(n.start, n.source) + 1)) as u64 - start;
let start_beats = map.clip_beats(start, from);
NoteSpec {
pitch: n.pitch,
start_beats,
length_beats: map.clip_beats(start, to) - start_beats,
velocity: n.velocity.max(1),
}
})
.collect();
if notes.is_empty() {
return None;
}
let controls = take
.controls
.iter()
.filter(|c| shift(c.frame, c.source) >= start as i64)
.map(|c| ControlSpec {
controller: c.controller,
time_beats: map.clip_beats(start, shift(c.frame, c.source) as u64 - start),
value: c.value,
})
.collect();
Some(AddMidiClip {
track: None,
track_name: None,
position_s: project.frames_to_seconds(start),
length_s: Some(project.frames_to_seconds(end as u64 - start)),
notes,
controls,
muted,
take: true,
})
}
fn meter_map_of(file: &MidiFile, offset: f64) -> SetTimeSignatureMap {
let (numerator, denominator) = file
.meters
.first()
.filter(|(beat, ..)| *beat <= 0.0)
.map_or((4, 4), |&(_, n, d)| (n, d));
let start = TimeSignature {
numerator,
denominator,
};
let mut changes: Vec<MeterChange> = Vec::new();
for &(beat, numerator, denominator) in file.meters.iter().filter(|(b, ..)| *b > 0.0) {
let map = MeterMap::new(start, &changes);
let at = beat + offset;
let bar = map.bar_at(at + 1e-9);
let bar = if (map.bar_start(bar) - at).abs() < 1e-6 {
bar
} else {
bar + 1
};
if bar == 0 {
continue;
}
let change = MeterChange {
bar: bar + 1,
numerator,
denominator,
};
changes.retain(|c| c.bar != change.bar);
changes.push(change);
}
SetTimeSignatureMap {
numerator,
denominator,
changes,
}
}
fn midi_routes(project: &Project) -> MidiRoutes {
let midi = || project.tracks.iter().filter(|t| t.kind == TrackKind::Midi);
let armed: Vec<_> = midi().filter(|t| t.armed).collect();
let capture = if armed.is_empty() {
MidiCaptureRoute::Off
} else if armed.iter().any(|t| t.midi_input.is_none()) {
MidiCaptureRoute::All
} else {
let mut devices: Vec<String> = armed.iter().filter_map(|t| t.midi_input.clone()).collect();
devices.sort();
devices.dedup();
MidiCaptureRoute::Devices(devices)
};
let mut outputs: Vec<String> = midi()
.filter_map(|t| t.midi_output.as_ref().map(|o| o.device.clone()))
.collect();
outputs.sort();
outputs.dedup();
MidiRoutes { capture, outputs }
}
fn max_db(a: [Option<f64>; 2], b: [Option<f64>; 2]) -> [Option<f64>; 2] {
[0, 1].map(|i| match (a[i], b[i]) {
(Some(x), Some(y)) => Some(x.max(y)),
(x, y) => x.or(y),
})
}
fn start_backend(
sample_rate: u32,
kind: &AudioBackend,
) -> Result<(EngineController, Backend), StudioError> {
let (engine, processor) = new_engine(sample_rate, Graph::empty());
let backend = match kind {
AudioBackend::Virtual => Backend::Virtual {
processor: Box::new(processor),
buffers: [Vec::new(), Vec::new()],
},
AudioBackend::PipeWire(options) => {
Backend::PipeWire(PipeWireOutput::start(processor, options)?)
}
};
Ok((engine, backend))
}
fn copy_tree(from: &Path, to: &Path) -> std::io::Result<()> {
let entries = match std::fs::read_dir(from) {
Ok(entries) => entries,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(e),
};
std::fs::create_dir_all(to)?;
for entry in entries {
let entry = entry?;
let target = to.join(entry.file_name());
if entry.file_type()?.is_dir() {
copy_tree(&entry.path(), &target)?;
continue;
}
let (source, kept) = (entry.metadata()?, std::fs::metadata(&target).ok());
let current = kept.is_some_and(|kept| {
kept.len() == source.len()
&& matches!((kept.modified(), source.modified()), (Ok(k), Ok(s)) if k >= s)
});
if !current {
std::fs::copy(entry.path(), target)?;
}
}
Ok(())
}