Files
soundcraft/crates/engine/tests/clap_e2e.rs
2026-10-15 04:07:58 +02:00

130 lines
6.3 KiB
Rust

//! A hosted CLAP plugin through the whole stack: `mix.insert` / `mix.insert_param` accept a
//! `clap:` id and the mix engine renders through it. Uses the test plugin built from
//! `crates/clap-host/tests/fixture` (a gain whose param "7" is linear gain).
use serde_json::json;
use soundcraft_audio_io::AudioBuffer;
use soundcraft_engine::Engine;
use soundcraft_model::{ChannelFormat, Clip, SourceAudio, SourceId, TrackKind};
use soundcraft_time::Range;
use std::path::Path;
use std::process::Command;
use std::sync::Arc;
fn install_fixture() {
static ONCE: std::sync::Once = std::sync::Once::new();
ONCE.call_once(install_fixture_now);
}
fn install_fixture_now() {
let manifest = Path::new(env!("CARGO_MANIFEST_DIR")).join("../clap-host/tests/fixture/Cargo.toml");
let target = Path::new(env!("CARGO_TARGET_TMPDIR")).join("clap-fixture-target");
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
let st = Command::new(cargo)
.args(["build", "--quiet", "--manifest-path"])
.arg(&manifest)
.arg("--target-dir")
.arg(&target)
.env_remove("RUSTFLAGS")
.status()
.unwrap();
assert!(st.success());
let lib = if cfg!(target_os = "macos") {
"libclap_fixture.dylib"
} else if cfg!(windows) {
"clap_fixture.dll"
} else {
"libclap_fixture.so"
};
let dir = Path::new(env!("CARGO_TARGET_TMPDIR")).join(format!("engine-clap-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::copy(target.join("debug").join(lib), dir.join("Fixture.clap")).unwrap();
assert_eq!(soundcraft_clap_host::add_search_dir(&dir), 5);
}
#[test]
fn clap_insert_renders_through_the_mix() {
install_fixture();
let mut e = Engine::default();
let list = e.execute("engine.clap_plugins", &json!({})).unwrap();
assert!(list.as_array().unwrap().iter().any(|d| d["id"] == "clap:org.soundcraft.test.gain"));
let s = e.session_mut();
let t = s.add_track(TrackKind::Audio, ChannelFormat::Mono, None);
s.pool.insert(SourceId(77), Arc::new(SourceAudio::new(AudioBuffer { sample_rate: 48_000, channels: vec![vec![0.25; 4800]] })));
let id = s.new_clip_id();
s.track_mut(t).unwrap().playlist_mut().unwrap().clips.push(Clip::audio(id, "dc", SourceId(77), 0, 0, 4800));
let dry = soundcraft_mix::render_range(e.session(), Range::new(0, 4800), 512)[0][2000];
assert!(dry.abs() > 0.01);
let r = e.execute("mix.insert", &json!({"track": t.0, "plugin": "clap:org.soundcraft.test.gain", "params": {"7": 2.0}})).unwrap();
assert_eq!(r["plugin"], "clap:org.soundcraft.test.gain");
let wet = soundcraft_mix::render_range(e.session(), Range::new(0, 4800), 512)[0][2000];
assert!((wet - 2.0 * dry).abs() < 1e-4, "dry {dry} wet {wet}");
e.execute("mix.insert_param", &json!({"track": t.0, "slot": 0, "param": "7", "value": 0.5})).unwrap();
let half = soundcraft_mix::render_range(e.session(), Range::new(0, 4800), 512)[0][2000];
assert!((half - 0.5 * dry).abs() < 1e-4, "dry {dry} half {half}");
assert!(e.execute("mix.insert_param", &json!({"track": t.0, "slot": 0, "param": "Gain", "value": 1.0})).is_err());
}
#[test]
fn a_refused_state_is_reported() {
install_fixture();
let gain = "clap:org.soundcraft.test.gain";
let make = |state: Option<&[u8]>| soundcraft_mix::create_instance(gain, state, 48_000.0, 512, 2).unwrap();
let saved = make(None).plugin.save_state().unwrap();
assert!(!make(None).refused_state);
assert!(!make(Some(&saved)).refused_state);
assert!(make(Some(&[0xff; 16])).refused_state, "NaN gain");
}
#[test]
fn clap_state_is_captured_stored_and_restored_before_the_first_block() {
install_fixture();
let gain = "clap:org.soundcraft.test.gain";
let mut e = Engine::default();
let s = e.session_mut();
let t = s.add_track(TrackKind::Audio, ChannelFormat::Mono, None);
s.pool.insert(SourceId(78), Arc::new(SourceAudio::new(AudioBuffer { sample_rate: 48_000, channels: vec![vec![0.25; 4800]] })));
let id = s.new_clip_id();
s.track_mut(t).unwrap().playlist_mut().unwrap().clips.push(Clip::audio(id, "dc", SourceId(78), 0, 0, 4800));
e.execute("mix.insert", &json!({"track": t.0, "plugin": gain, "params": {"7": 2.0}})).unwrap();
// A realtime-style engine: the instance is created "on the UI thread" from the specs and
// adopted; its live state (gain 1.5, changed behind the session's back as a plugin GUI
// would) is captured and stored on the insert.
let mut eng = soundcraft_mix::MixEngine::new(48_000.0, 512);
eng.set_external_instances(true);
let specs = soundcraft_mix::instance_specs(e.session());
assert_eq!(specs.len(), 1);
let mut p = soundcraft_mix::create_instance(&specs[0].id, None, 48_000.0, 512, specs[0].ch).unwrap().plugin;
p.set_param("7", 1.5);
let mut io = vec![vec![0.0f32; 64]];
p.process(&mut io, 64);
let mut reader = p.state_reader().unwrap();
eng.adopt(vec![soundcraft_mix::PreparedInstance { track: t, slot: 0, id: gain.into(), ch: 1, plugin: p }]);
eng.ensure_synced(e.session());
let blob = reader.save_state().unwrap();
e.execute("mix.insert_state", &json!({"track": t.0, "slot": 0, "state": soundcraft_model::b64::encode(&blob)})).unwrap();
assert_eq!(eng.take_retired().len(), 0);
// Offline render (synchronous creation): the state is restored before the first block.
// With the gain absent from the session's params, the state's 1.5 is what plays.
let gain_of = |s: &soundcraft_model::Session| soundcraft_mix::render_range(s, Range::new(0, 4800), 512)[0][2000];
let mut s2 = e.session().clone();
s2.track_mut(t).unwrap().mixer.inserts[0].as_mut().unwrap().params.remove("7");
let restored = gain_of(&s2);
let mut s3 = e.session().clone();
let ins = s3.track_mut(t).unwrap().mixer.inserts[0].as_mut().unwrap();
ins.state = None;
ins.params.insert("7".into(), 1.5);
let reference = gain_of(&s3);
assert!((restored - reference).abs() < 1e-5, "state {restored} vs param 1.5 {reference}");
// Session params apply on top of the state.
e.execute("mix.insert_param", &json!({"track": t.0, "slot": 0, "param": "7", "value": 0.5})).unwrap();
let half = gain_of(e.session());
assert!((half - reference / 3.0).abs() < 1e-5, "{half} vs {}", reference / 3.0);
}