mirror of
https://github.com/storytold/soundcraft.git
synced 2026-10-24 02:26:46 -06:00
Merge pull request #680 from marcoarnulfo/fix/vst3-param-units
VST3: show the plugin's own text for identity-mapped parameters
This commit is contained in:
@@ -275,6 +275,12 @@ pub trait PluginEditor: Send {
|
||||
/// Tells the editor that a parameter changed elsewhere (generic editor, automation), so the
|
||||
/// GUI shows it. Values are in SoundCraft units.
|
||||
fn set_param(&mut self, _id: &str, _value: f32) {}
|
||||
/// The plugin's own text for a parameter value (SoundCraft units) when SoundCraft's
|
||||
/// [`ParamInfo::format`] would be wrong for it. Main thread only; `None` means "format it
|
||||
/// yourself".
|
||||
fn value_text(&mut self, _id: &str, _value: f32) -> Option<String> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads a plugin's state on the main (UI) thread while the instance processes on the audio
|
||||
|
||||
@@ -726,6 +726,12 @@ impl Player {
|
||||
}
|
||||
}
|
||||
|
||||
/// The plugin's own text for a parameter value when SoundCraft's format would be wrong for it
|
||||
/// (see `PluginEditor::value_text`). Main thread only.
|
||||
pub fn editor_value_text(&self, track: TrackId, slot: usize, id: &str, value: f32) -> Option<String> {
|
||||
self.side().editors.get_mut(&(track, slot))?.value_text(id, value)
|
||||
}
|
||||
|
||||
/// Start playback at `from`, stopping at `end`, or looping `looped`.
|
||||
pub fn play(&self, from: Samples, end: Option<Samples>, looped: Option<Range>) {
|
||||
self.shared.position.store(from, Ordering::Relaxed);
|
||||
|
||||
@@ -544,7 +544,11 @@ fn plugin_windows(app: &mut SoundApp, ctx: &egui::Context) {
|
||||
}
|
||||
}
|
||||
let _ = resp;
|
||||
let shown = if instrument { crate::sampler_window::format_param(&ins.plugin, p, x) } else { p.format(x) };
|
||||
let shown = if instrument {
|
||||
crate::sampler_window::format_param(&ins.plugin, p, x)
|
||||
} else {
|
||||
app.player.as_ref().and_then(|pl| pl.editor_value_text(tid, slot, p.id, x)).unwrap_or_else(|| p.format(x))
|
||||
};
|
||||
ui.label(egui::RichText::new(shown).font(mono(11.0)));
|
||||
ui.end_row();
|
||||
}
|
||||
|
||||
@@ -1788,6 +1788,20 @@ impl EditorLink {
|
||||
v.is_finite().then(|| v.clamp(0.0, 1.0))
|
||||
}
|
||||
|
||||
/// The plugin's own text for a normalized value (`getParamStringByValue`). Main thread only
|
||||
/// (`None` elsewhere, and once the instance is gone).
|
||||
pub fn value_text(&self, id: u32, n: f64) -> Option<String> {
|
||||
if !window::on_main_thread() || !n.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let g = self.lock();
|
||||
let c = g.controller.as_ref()?;
|
||||
let mut s: sv::String128 = [0; 128];
|
||||
// SAFETY: valid controller (the instance clears it under this lock before it terminates),
|
||||
// main thread; `s` is a writable String128.
|
||||
ok(unsafe { c.getParamStringByValue(id, n.clamp(0.0, 1.0), &mut s) }).then(|| read_c16(&s)).filter(|t| !t.is_empty())
|
||||
}
|
||||
|
||||
/// Shows a value change in the editor (`setParamNormalized`, main thread).
|
||||
pub fn set_normalized(&self, id: u32, n: f64) {
|
||||
if !window::on_main_thread() || !n.is_finite() {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use crate::ffi::{AFTERTOUCH, BusBuffers, EVENT_CAP, EditorLink, HostContext, Instance, NoteEvent, PARAM_QUEUE_CAP, PITCH_BEND};
|
||||
use soundcraft_dsp::{Plugin, PluginEditor, PluginInfo, Transport, Unit};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use vst3::ComWrapper;
|
||||
@@ -399,7 +400,16 @@ impl Plugin for Vst3Plugin {
|
||||
|
||||
fn editor(&mut self) -> Option<Box<dyn PluginEditor>> {
|
||||
let link = self.inst.editor()?;
|
||||
Some(Box::new(Vst3Editor { link, host: self.inst.host_ref(), info: self.info, ids: self.ids.clone(), maps: self.maps.clone() }))
|
||||
let mut units: HashMap<u32, String> = self.inst.params().into_iter().map(|p| (p.id, p.units)).collect();
|
||||
Some(Box::new(Vst3Editor {
|
||||
link,
|
||||
host: self.inst.host_ref(),
|
||||
info: self.info,
|
||||
index: self.info.params.iter().enumerate().map(|(i, p)| (p.id, i)).collect(),
|
||||
units: self.ids.iter().map(|id| units.remove(id).unwrap_or_default()).collect(),
|
||||
ids: self.ids.clone(),
|
||||
maps: self.maps.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn state_reader(&mut self) -> Option<Box<dyn soundcraft_dsp::PluginStateReader>> {
|
||||
@@ -423,6 +433,10 @@ struct Vst3Editor {
|
||||
link: Arc<EditorLink>,
|
||||
host: ComWrapper<HostContext>,
|
||||
info: &'static PluginInfo,
|
||||
/// Position of each SoundCraft param id in `info.params` (looked up once per row and frame).
|
||||
index: HashMap<&'static str, usize>,
|
||||
/// The plugin's own unit label of each param (same order as `ids`).
|
||||
units: Vec<String>,
|
||||
ids: Vec<u32>,
|
||||
maps: Vec<Mapping>,
|
||||
}
|
||||
@@ -487,4 +501,19 @@ impl PluginEditor for Vst3Editor {
|
||||
self.link.set_normalized(vid, n);
|
||||
}
|
||||
}
|
||||
|
||||
fn value_text(&mut self, id: &str, value: f32) -> Option<String> {
|
||||
let &i = self.index.get(id)?;
|
||||
let pi = self.info.params.get(i)?;
|
||||
// A plugin whose plain mapping is the identity (JUCE, the SDK's default `Parameter`) leaves
|
||||
// a 0..1 range with the units of its real one: only its own text says what the value is.
|
||||
if !matches!(self.maps.get(i)?, Mapping::Plain) || pi.min != 0.0 || pi.max != 1.0 {
|
||||
return None;
|
||||
}
|
||||
let text = self.link.value_text(*self.ids.get(i)?, self.normalized(i, value)?)?;
|
||||
// VST3 keeps the units apart from the text (JUCE's label, the SDK's `RangeParameter`),
|
||||
// but some plugins put them in both.
|
||||
let units = self.units.get(i).map_or("", |u| u.trim());
|
||||
if units.is_empty() || text.trim_end().to_lowercase().ends_with(&units.to_lowercase()) { Some(text) } else { Some(format!("{text} {units}")) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,16 @@ fn main() {
|
||||
ed.set_param("7", 1.5);
|
||||
ed.set_param("nope", 1.0);
|
||||
assert!(ed.idle().is_empty(), "set_param is not reported back as an edit");
|
||||
// #599: an identity-mapped (JUCE-style) parameter shows the plugin's own text, verbatim, with
|
||||
// its units appended once; correctly mapped parameters and choices keep SoundCraft's format.
|
||||
assert_eq!(ed.value_text("13", 0.375).as_deref(), Some("\u{2212}6.0 dB"));
|
||||
assert_eq!(ed.value_text("13", 1.0).as_deref(), Some("24.0 dB"));
|
||||
assert_eq!(ed.value_text("14", 0.25).as_deref(), Some("3.0 dB"), "units already in the text");
|
||||
assert_eq!(ed.value_text("7", 1.5), None);
|
||||
assert_eq!(ed.value_text("3", 1.0), None);
|
||||
assert_eq!(ed.value_text("nope", 0.5), None);
|
||||
drop(p);
|
||||
assert_eq!(ed.value_text("13", 0.375), None, "the plugin is gone");
|
||||
assert!(!ed.is_open());
|
||||
assert!(ed.open().is_err(), "the plugin is gone");
|
||||
println!("editor_main_thread: ok");
|
||||
|
||||
@@ -298,12 +298,17 @@ impl IConnectionPointTrait for GainController {
|
||||
}
|
||||
}
|
||||
|
||||
const GAIN_PARAMS: [(u32, &str, &str, i32, f64, i32); 5] = [
|
||||
const GAIN_PARAMS: [(u32, &str, &str, i32, f64, i32); 7] = [
|
||||
(7, "Gain", "dB", 0, 0.5, ParameterInfo_::ParameterFlags_::kCanAutomate),
|
||||
(9, "Secret", "", 0, 0.0, ParameterInfo_::ParameterFlags_::kIsHidden),
|
||||
(3, "Mode", "", 2, 0.0, ParameterInfo_::ParameterFlags_::kIsList),
|
||||
(11, "Bypass", "", 1, 0.0, ParameterInfo_::ParameterFlags_::kIsBypass),
|
||||
(12, "Freq", "Hz", 0, 0.5, 0),
|
||||
// Like JUCE and the SDK's default `Parameter`: no plain mapping (identity), so the real range
|
||||
// is only in the plugin's own text (-24..+24, the default 0.375 is -6), without the units.
|
||||
(13, "Level", "dB", 0, 0.375, ParameterInfo_::ParameterFlags_::kCanAutomate),
|
||||
// Identity too, but its text already carries the units (0..12 dB).
|
||||
(14, "Drive", "dB", 0, 0.0, ParameterInfo_::ParameterFlags_::kCanAutomate),
|
||||
];
|
||||
|
||||
impl IEditControllerTrait for GainController {
|
||||
@@ -351,6 +356,8 @@ impl IEditControllerTrait for GainController {
|
||||
let s = unsafe { &mut *string };
|
||||
let text = match id {
|
||||
3 => ["A", "B", "C"][((n * 2.0).round() as usize).min(2)].to_string(),
|
||||
13 => format!("{:.1}", 48.0 * n - 24.0).replace('-', "\u{2212}"),
|
||||
14 => format!("{:.1} dB", 12.0 * n),
|
||||
_ => format!("{:.2}", unsafe { self.normalizedParamToPlain(id, n) }),
|
||||
};
|
||||
copy_c16(&text, s);
|
||||
|
||||
@@ -42,7 +42,7 @@ fn info_exposes_visible_params_with_plain_ranges() {
|
||||
assert_eq!(info.id, gain);
|
||||
assert_eq!(info.short_name, "Fixture ");
|
||||
let ids: Vec<&str> = info.params.iter().map(|p| p.id).collect();
|
||||
assert_eq!(ids, vec!["7", "3", "12"], "hidden 9 and bypass 11 are skipped");
|
||||
assert_eq!(ids, vec!["7", "3", "12", "13", "14"], "hidden 9 and bypass 11 are skipped");
|
||||
let g = info.param("7").unwrap();
|
||||
assert_eq!((g.name, g.min, g.max, g.default, g.unit, g.taper), ("Gain", 0.0, 2.0, 1.0, Unit::Db, Taper::Linear));
|
||||
let mode = info.param("3").unwrap();
|
||||
@@ -55,6 +55,23 @@ fn info_exposes_visible_params_with_plain_ranges() {
|
||||
assert!(std::ptr::eq(info, soundcraft_vst3_host::plugin_info(&gain).unwrap()));
|
||||
}
|
||||
|
||||
/// #599: a plugin whose `normalizedParamToPlain` is the identity (JUCE, the SDK's default
|
||||
/// `Parameter`) gives SoundCraft a 0..1 range; its real values are only in its own text, which the
|
||||
/// editor handle reads from the controller on the main thread only (see `editor_main_thread`).
|
||||
#[test]
|
||||
fn identity_mapped_param_text_is_read_on_the_main_thread_only() {
|
||||
let id = id_of("Fixture Gain");
|
||||
let level = soundcraft_vst3_host::plugin_info(&id).unwrap().param("13").unwrap();
|
||||
assert_eq!((level.min, level.max, level.unit), (0.0, 1.0, Unit::Db));
|
||||
let mut p = soundcraft_vst3_host::instantiate_plugin(&id).unwrap();
|
||||
let mut ed = soundcraft_dsp::Plugin::editor(&mut p).expect("editor handle");
|
||||
if cfg!(any(target_os = "macos", windows)) {
|
||||
assert_eq!(ed.value_text("13", 0.375), None, "test threads are not the main thread");
|
||||
} else {
|
||||
assert_eq!(ed.value_text("13", 0.375).as_deref(), Some("\u{2212}6.0 dB"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gain_processes_audio_and_follows_params() {
|
||||
let mut p = soundcraft_vst3_host::create(&id_of("Fixture Gain")).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user