mirror of
https://github.com/storytold/soundcraft.git
synced 2026-10-27 03:56:45 -06:00
Fixes #435 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
130 lines
6.3 KiB
Rust
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);
|
|
}
|