use std::path::{Path, PathBuf};
use dawkey_core::{AddTrack, Project, SetTrackArm};
use dawkey_media::AudioData;
use crate::{AudioBackend, LoadedProject, Studio, StudioOptions};
fn recording_studio(dir: &Path) -> Studio {
let mut studio = Studio::new(
LoadedProject::new(Project::default()),
AudioBackend::Virtual,
)
.unwrap();
studio.set_recording_dir(dir.to_owned());
let track = studio
.execute(AddTrack::default().into())
.unwrap()
.track
.unwrap();
studio
.execute(SetTrackArm { track, armed: true }.into())
.unwrap();
studio
.set_virtual_input(Some(AudioData::new(vec![vec![0.25; 96_000]])))
.unwrap();
studio
}
fn record(studio: &mut Studio) -> PathBuf {
studio.record().unwrap();
studio.advance(0.25).unwrap();
studio.stop().unwrap();
let source = studio.session().project().sources.last().unwrap().id;
studio.session().source_path(source).unwrap()
}
#[test]
fn saving_deletes_cached_takes() {
let dir = tempfile::tempdir().unwrap();
let cache = dir.path().join("cache");
let mut studio = recording_studio(&cache);
let take = record(&mut studio);
assert!(
take.starts_with(&cache) && take.exists(),
"{}",
take.display()
);
let bundle = studio.save(Some(&dir.path().join("Song"))).unwrap();
assert!(!take.exists() && !cache.exists(), "the cache copy is gone");
let source = studio.session().project().sources[0].id;
let saved = studio.session().source_path(source).unwrap();
assert!(saved.starts_with(&bundle) && saved.exists());
let second = record(&mut studio);
assert!(second.starts_with(&bundle));
drop(studio);
assert!(second.exists() && saved.exists());
}
#[test]
fn replacing_or_closing_an_unsaved_project_deletes_its_takes() {
let dir = tempfile::tempdir().unwrap();
let mut studio = recording_studio(dir.path());
let first = record(&mut studio);
studio
.replace(LoadedProject::new(Project::default()))
.unwrap();
assert!(!first.exists());
let mut studio = recording_studio(dir.path());
let second = record(&mut studio);
studio.record().unwrap();
studio.advance(0.1).unwrap();
drop(studio);
assert!(!second.exists());
assert!(!dir.path().exists() || dir.path().read_dir().unwrap().next().is_none());
}
#[test]
fn reported_midi_device_latency_is_compensated() {
let mut studio = Studio::new(
LoadedProject::new(Project::default()),
AudioBackend::Virtual,
)
.unwrap();
let track = studio
.execute(
AddTrack {
kind: dawkey_core::TrackKind::Midi,
..Default::default()
}
.into(),
)
.unwrap()
.track
.unwrap();
let mut set = dawkey_core::SetTrackMidiOutput::new(track, Some("Synth".into()));
set.latency_ms = Some(5.0);
studio.execute(set.into()).unwrap();
assert_eq!(studio.engine_status().plugin_latency_frames, 240);
studio.set_virtual_midi_latency("Synth", Some(10.0));
let status = studio.engine_status();
assert_eq!(status.plugin_latency_frames, 720);
assert_eq!(status.midi_output_latency_ms["Synth"], 10.0);
studio.set_virtual_midi_latency("Synth", None);
assert_eq!(studio.engine_status().plugin_latency_frames, 240);
}
#[test]
fn reported_midi_input_latency_places_recorded_notes_earlier() {
let dir = tempfile::tempdir().unwrap();
let mut studio = Studio::new(
LoadedProject::new(Project::default()),
AudioBackend::Virtual,
)
.unwrap();
studio.set_recording_dir(dir.path().to_owned());
studio.set_virtual_midi_devices(vec!["Keys".into(), "Pads".into()], Vec::new());
let track = studio
.execute(
AddTrack {
kind: dawkey_core::TrackKind::Midi,
..Default::default()
}
.into(),
)
.unwrap()
.track
.unwrap();
studio
.execute(SetTrackArm { track, armed: true }.into())
.unwrap();
studio.set_virtual_midi_input_latency("Keys", Some(10.0));
let status = studio.engine_status();
assert_eq!(status.midi_input_latency_ms["Keys"], 10.0);
assert_eq!(status.midi_record_latency_ms["Keys"], 10.0);
assert_eq!(status.midi_record_latency_ms["Pads"], 0.0);
studio.record().unwrap();
let note = |pitch: u8| [(0.1, [0x90, pitch, 100], 0), (0.2, [0x80, pitch, 0], 0)];
studio.queue_virtual_midi(Some("Keys"), ¬e(60)).unwrap();
studio.queue_virtual_midi(Some("Pads"), ¬e(64)).unwrap();
studio.advance(0.3).unwrap();
studio.stop().unwrap();
let project = studio.session().project();
let clip = &project.track(track).unwrap().clips[0];
let at = |pitch: u8| {
let n = clip.notes().iter().find(|n| n.pitch == pitch).unwrap();
(clip.start + n.start, n.length)
};
assert_eq!(at(60), (4800 - 480, 4800));
assert_eq!(at(64), (4800, 4800));
assert_eq!(clip.start, 0);
}
fn peak_db(studio: &mut Studio, seconds: f64) -> f64 {
let job = studio.render_job(0.0, seconds).unwrap();
let rate = job.sample_rate();
let (audio, leftovers) = job.run();
studio.finish_render(leftovers);
dawkey_media::analyze(&audio, rate).peak_dbfs[0].unwrap()
}
#[test]
fn plugins_scan_in_the_background() {
let dir = tempfile::tempdir().unwrap();
let installed = dawkey_test_plugins::install(dir.path()).unwrap();
let search = dawkey_plugins::SearchPath {
clap: vec![installed.clap_path],
lv2: Some(vec![installed.lv2_path]),
vst3: vec![installed.vst3_path],
};
let wav = dir.path().join("half.wav");
dawkey_media::write_wav(&wav, &[vec![0.5; 4800]], 48_000).unwrap();
let mut session = dawkey_core::Session::new(Project::default());
let track = session
.execute(AddTrack::default().into())
.unwrap()
.track
.unwrap();
let plugin = session
.execute(
dawkey_core::AddPlugin {
track,
plugin: "clap:dawkey.test.gain".into(),
index: None,
}
.into(),
)
.unwrap()
.plugin
.unwrap();
session
.execute(dawkey_core::SetPluginParam::new(plugin, "1", -6.0).into())
.unwrap();
let options = StudioOptions {
background_scan: true,
plugin_search: Some((search, None)),
..StudioOptions::default()
};
let loaded = LoadedProject::new(session.project().clone());
let mut studio = Studio::with_options(loaded, AudioBackend::Virtual, options).unwrap();
assert!(studio.scanning_plugins());
let decoded = dawkey_media::decode_file(&wav, 48_000).unwrap();
let source = studio.add_source(&wav, decoded);
studio
.execute(
dawkey_core::AddClip {
source,
track: Some(track),
position_s: 0.0,
offset_s: 0.0,
length_s: None,
muted: false,
take: false,
}
.into(),
)
.unwrap();
wait_for_scan(&mut studio);
assert!(
studio
.plugin_catalog_now()
.unwrap()
.get("clap:dawkey.test.gain")
.is_some()
);
assert!((peak_db(&mut studio, 0.1) - (-9.0309 - 6.0)).abs() < 0.01);
studio.rescan_plugins_in_background();
assert!(studio.scanning_plugins());
assert!(studio.plugin_catalog_now().is_some());
assert!(studio.engine_status().plugin_problems.is_empty());
wait_for_scan(&mut studio);
assert!((peak_db(&mut studio, 0.1) - (-9.0309 - 6.0)).abs() < 0.01);
}
fn wait_for_scan(studio: &mut Studio) {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
while studio.scanning_plugins() {
assert!(std::time::Instant::now() < deadline, "the scan never ended");
std::thread::sleep(std::time::Duration::from_millis(10));
studio.tick();
}
}
#[test]
fn autosaves_recover_a_never_saved_project() {
let root = tempfile::tempdir().unwrap();
let dead = root.path().join("4294967295");
let mut crashed = recording_studio(&dead);
let take = record(&mut crashed);
assert!(crashed.autosave().unwrap());
assert!(!crashed.autosave().unwrap(), "nothing new to keep");
let autosave = crashed.autosave_file();
assert_eq!(autosave, dead.join("autosave.json"));
std::mem::forget(crashed);
let own = root.path().join(std::process::id().to_string());
let mut studio = Studio::new(
LoadedProject::new(Project::default()),
AudioBackend::Virtual,
)
.unwrap();
studio.set_recording_dir(own);
let recovery = studio.orphan_recovery().expect("an autosave to recover");
assert_eq!(recovery.dir.as_deref(), Some(dead.as_path()));
assert!(recovery.age() < std::time::Duration::from_secs(60));
studio.recover(&recovery).unwrap();
let project = studio.session().project();
assert_eq!(project.tracks.len(), 1);
assert_eq!(
studio.session().source_path(project.sources[0].id),
Some(take.clone())
);
assert!(studio.session().is_dirty() && studio.session().path().is_none());
assert!(crate::cache::orphaned_autosaves(root.path()).is_empty());
assert!(dead.join("autosave.json").exists());
studio.save(Some(&root.path().join("Song"))).unwrap();
assert!(!take.exists() && !dead.join("autosave.json").exists());
drop(studio);
assert!(!dead.exists());
}
#[test]
fn autosaves_recover_a_bundles_unsaved_changes() {
let dir = tempfile::tempdir().unwrap();
let bundle = dir.path().join("Song.dawkey");
let mut studio = Studio::new(
LoadedProject::new(Project::default()),
AudioBackend::Virtual,
)
.unwrap();
studio.save(Some(&bundle)).unwrap();
assert!(!studio.autosave().unwrap(), "no changes to keep");
studio.execute(AddTrack::default().into()).unwrap();
assert!(studio.autosave().unwrap());
assert_eq!(studio.autosave_file(), bundle.join("backups/autosave.json"));
std::mem::forget(studio);
let open =
|| Studio::new(LoadedProject::open(&bundle).unwrap(), AudioBackend::Virtual).unwrap();
let mut studio = open();
assert!(studio.session().project().tracks.is_empty());
let recovery = studio.bundle_recovery().expect("newer than the project");
studio.recover(&recovery).unwrap();
assert_eq!(studio.session().project().tracks.len(), 1);
assert!(studio.session().is_dirty());
drop(studio);
assert!(!bundle.join("backups").exists());
assert!(open().bundle_recovery().is_none());
let mut studio = open();
studio.execute(AddTrack::default().into()).unwrap();
studio.autosave().unwrap();
std::mem::forget(studio);
let recovery = open().bundle_recovery().unwrap();
Studio::discard_recovery(&recovery);
assert!(open().bundle_recovery().is_none());
}