mirror of
https://github.com/storytold/soundcraft.git
synced 2026-11-11 16:35:42 -06:00
Merge pull request #1178 from luigiPinna/fix/vst3-bus-limits-904
Reject VST3 bus layouts beyond host limits instead of truncating
This commit is contained in:
@@ -35,6 +35,11 @@ pub(crate) struct Layout {
|
||||
pub event_input: bool,
|
||||
}
|
||||
|
||||
/// `"input"` or `"output"`, for error messages.
|
||||
fn dir_name(dir: sv::BusDirections) -> &'static str {
|
||||
if dir == sv::BusDirections_::kInput { "input" } else { "output" }
|
||||
}
|
||||
|
||||
pub(super) fn apply_transport(c: &mut sv::ProcessContext, t: &Transport) {
|
||||
use sv::ProcessContext_::StatesAndFlags_::{
|
||||
kBarPositionValid, kContTimeValid, kPlaying, kProjectTimeMusicValid, kRecording, kTempoValid, kTimeSigValid,
|
||||
@@ -248,7 +253,17 @@ impl Instance {
|
||||
|
||||
fn bus_count(&self, media: sv::MediaTypes, dir: sv::BusDirections) -> i32 {
|
||||
// SAFETY: valid component.
|
||||
unsafe { self.component.getBusCount(media as sv::MediaType, dir as sv::BusDirection) }.clamp(0, MAX_BUSES)
|
||||
unsafe { self.component.getBusCount(media as sv::MediaType, dir as sv::BusDirection) }.max(0)
|
||||
}
|
||||
|
||||
/// The plugin's audio buses in one direction. More than `MAX_BUSES` is an error: we never
|
||||
/// hand a plugin fewer buses than it advertised.
|
||||
fn audio_bus_count(&self, dir: sv::BusDirections) -> Result<i32, Vst3Error> {
|
||||
let n = self.bus_count(sv::MediaTypes_::kAudio, dir);
|
||||
if n > MAX_BUSES {
|
||||
return Err(Vst3Error::Process(format!("{n} audio {} buses (at most {MAX_BUSES} supported)", dir_name(dir))));
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
fn arrangement(&self, dir: sv::BusDirections, index: i32) -> Option<sv::SpeakerArrangement> {
|
||||
@@ -258,30 +273,41 @@ impl Instance {
|
||||
ok(unsafe { p.getBusArrangement(dir as sv::BusDirection, index, &mut arr) }).then_some(arr)
|
||||
}
|
||||
|
||||
fn bus_channels(&self, dir: sv::BusDirections, index: i32) -> u32 {
|
||||
if let Some(arr) = self.arrangement(dir, index) {
|
||||
return arr.count_ones().min(MAX_BUS_CHANNELS);
|
||||
}
|
||||
// SAFETY: plain data; all-zero is valid.
|
||||
let mut info: sv::BusInfo = unsafe { std::mem::zeroed() };
|
||||
// SAFETY: valid component; index below the bus count; `info` is writable.
|
||||
if ok(unsafe { self.component.getBusInfo(sv::MediaTypes_::kAudio as sv::MediaType, dir as sv::BusDirection, index, &mut info) }) {
|
||||
u32::try_from(info.channelCount).unwrap_or(0).min(MAX_BUS_CHANNELS)
|
||||
/// Channels of one audio bus. More than `MAX_BUS_CHANNELS` is an error, as for the bus count.
|
||||
fn bus_channels(&self, dir: sv::BusDirections, index: i32) -> Result<u32, Vst3Error> {
|
||||
let channels = if let Some(arr) = self.arrangement(dir, index) {
|
||||
arr.count_ones()
|
||||
} else {
|
||||
0
|
||||
// SAFETY: plain data; all-zero is valid.
|
||||
let mut info: sv::BusInfo = unsafe { std::mem::zeroed() };
|
||||
// SAFETY: valid component; index below the bus count; `info` is writable.
|
||||
if ok(unsafe { self.component.getBusInfo(sv::MediaTypes_::kAudio as sv::MediaType, dir as sv::BusDirection, index, &mut info) }) {
|
||||
u32::try_from(info.channelCount).unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
};
|
||||
if channels > MAX_BUS_CHANNELS {
|
||||
return Err(Vst3Error::Process(format!(
|
||||
"audio {} bus {index} has {channels} channels (at most {MAX_BUS_CHANNELS} supported)",
|
||||
dir_name(dir)
|
||||
)));
|
||||
}
|
||||
Ok(channels)
|
||||
}
|
||||
|
||||
/// Stops processing, deactivates, negotiates the bus layout (main buses mono or stereo as
|
||||
/// asked, falling back to stereo and then to the plugin's own choice), activates the main
|
||||
/// buses, sets up 32-bit processing and activates the component.
|
||||
/// buses, sets up 32-bit processing and activates the component. A layout beyond
|
||||
/// `MAX_BUSES` buses or `MAX_BUS_CHANNELS` channels per bus is an error, and the component
|
||||
/// stays inactive.
|
||||
pub fn configure(&mut self, sample_rate: f64, max_frames: usize, mono: bool) -> Result<Layout, Vst3Error> {
|
||||
self.shutdown();
|
||||
let audio = sv::MediaTypes_::kAudio;
|
||||
let event = sv::MediaTypes_::kEvent;
|
||||
let (din, dout) = (sv::BusDirections_::kInput, sv::BusDirections_::kOutput);
|
||||
let nin = self.bus_count(audio, din);
|
||||
let nout = self.bus_count(audio, dout);
|
||||
let nin = self.audio_bus_count(din)?;
|
||||
let nout = self.audio_bus_count(dout)?;
|
||||
let current = |s: &Self, dir: sv::BusDirections, n: i32| -> Vec<sv::SpeakerArrangement> {
|
||||
(0..n).map(|i| s.arrangement(dir, i).unwrap_or(sv::SpeakerArr::kStereo)).collect()
|
||||
};
|
||||
@@ -311,8 +337,8 @@ impl Instance {
|
||||
log::debug!("vst3: plugin refused mono/stereo bus arrangements; using its own");
|
||||
}
|
||||
let layout = Layout {
|
||||
inputs: (0..nin).map(|i| self.bus_channels(din, i)).collect(),
|
||||
outputs: (0..nout).map(|i| self.bus_channels(dout, i)).collect(),
|
||||
inputs: (0..nin).map(|i| self.bus_channels(din, i)).collect::<Result<_, _>>()?,
|
||||
outputs: (0..nout).map(|i| self.bus_channels(dout, i)).collect::<Result<_, _>>()?,
|
||||
event_input: self.bus_count(event, din) > 0,
|
||||
};
|
||||
// Main buses on, aux buses off (VST3 still gets buffers for every bus).
|
||||
|
||||
@@ -35,7 +35,7 @@ pub fn fixture_dir() -> &'static Path {
|
||||
let bin = dir.join("vendor/Fixture.vst3/Contents").join(folder);
|
||||
std::fs::create_dir_all(&bin).unwrap();
|
||||
std::fs::copy(&built, bin.join(exe)).unwrap();
|
||||
assert_eq!(soundcraft_vst3_host::add_search_dir(&dir), 2);
|
||||
assert_eq!(soundcraft_vst3_host::add_search_dir(&dir), 5);
|
||||
dir
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Test-only VST3 plugin for soundcraft-vst3-host (two audio module classes):
|
||||
//! Test-only VST3 plugin for soundcraft-vst3-host (five audio module classes):
|
||||
//!
|
||||
//! - "Fixture Gain" (`Fx`): stereo or mono in → out × gain, with a separate edit controller
|
||||
//! (connected through IConnectionPoint, synced through the component state). Params: 7 "Gain"
|
||||
@@ -11,6 +11,11 @@
|
||||
//! its edit controller). No audio input, one stereo output, one event input; every output
|
||||
//! sample is the velocity of the held note (sample-accurate), 0 when none. Poly pressure, and
|
||||
//! MIDI CC 7 through `IMidiMapping` (hidden parameter 1), set it to their value too.
|
||||
//! - "Fixture Wide 16x64", "Fixture Wide 17 buses", "Fixture Wide 65 channels" (`Fx`): effects
|
||||
//! advertising wide layouts (16 buses of 64 channels, 17 buses of 2 channels, one bus of 65
|
||||
//! channels per direction), through `getBusInfo` only. `process` writes 1.0 into every channel
|
||||
//! of output bus 0 when the host's buffers match the advertised layout, -1.0 when they do not.
|
||||
//! Report 3 samples of latency.
|
||||
//!
|
||||
//! The module checks the host's module lifecycle the way real plugins depend on it (see "module
|
||||
//! lifecycle" below) and aborts the process when it is broken.
|
||||
@@ -28,6 +33,9 @@ use vst3::{Class, ComRef, ComWrapper, uid};
|
||||
const GAIN_CID: TUID = uid(0x5C0A1D00, 0x11112222, 0x33334444, 0x55556666);
|
||||
const GAIN_CTRL_CID: TUID = uid(0x5C0A1D01, 0x11112222, 0x33334444, 0x55556666);
|
||||
const SYNTH_CID: TUID = uid(0x5C0A1D02, 0x11112222, 0x33334444, 0x55556666);
|
||||
const WIDE16_CID: TUID = uid(0x5C0A1D03, 0x11112222, 0x33334444, 0x55556666);
|
||||
const WIDE17_CID: TUID = uid(0x5C0A1D04, 0x11112222, 0x33334444, 0x55556666);
|
||||
const WIDE65_CID: TUID = uid(0x5C0A1D05, 0x11112222, 0x33334444, 0x55556666);
|
||||
|
||||
fn copy_c8(src: &str, dst: &mut [c_char]) {
|
||||
for (d, s) in dst.iter_mut().zip(src.bytes().chain(std::iter::once(0))) {
|
||||
@@ -626,6 +634,98 @@ impl IEditControllerTrait for Synth {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- wide layouts --------------------------------------------------------------------------
|
||||
|
||||
struct Wide {
|
||||
buses: i32,
|
||||
channels: i32,
|
||||
_live: Live,
|
||||
}
|
||||
|
||||
impl Class for Wide {
|
||||
type Interfaces = (IComponent, IAudioProcessor);
|
||||
}
|
||||
|
||||
impl IPluginBaseTrait for Wide {
|
||||
unsafe fn initialize(&self, _context: *mut FUnknown) -> tresult {
|
||||
kResultOk
|
||||
}
|
||||
unsafe fn terminate(&self) -> tresult {
|
||||
kResultOk
|
||||
}
|
||||
}
|
||||
|
||||
impl IComponentTrait for Wide {
|
||||
unsafe fn getControllerClassId(&self, _class_id: *mut TUID) -> tresult {
|
||||
kNotImplemented
|
||||
}
|
||||
unsafe fn setIoMode(&self, _mode: IoMode) -> tresult {
|
||||
kResultOk
|
||||
}
|
||||
unsafe fn getBusCount(&self, media: MediaType, _dir: BusDirection) -> i32 {
|
||||
if media == MediaTypes_::kAudio as MediaType { self.buses } else { 0 }
|
||||
}
|
||||
unsafe fn getBusInfo(&self, media: MediaType, dir: BusDirection, index: i32, bus: *mut BusInfo) -> tresult {
|
||||
if media != MediaTypes_::kAudio as MediaType || !(0..self.buses).contains(&index) {
|
||||
return kInvalidArgument;
|
||||
}
|
||||
audio_bus(bus, dir, "Wide");
|
||||
unsafe { (*bus).channelCount = self.channels };
|
||||
kResultOk
|
||||
}
|
||||
unsafe fn getRoutingInfo(&self, _i: *mut RoutingInfo, _o: *mut RoutingInfo) -> tresult {
|
||||
kNotImplemented
|
||||
}
|
||||
unsafe fn activateBus(&self, _m: MediaType, _d: BusDirection, _i: i32, _s: TBool) -> tresult {
|
||||
kResultOk
|
||||
}
|
||||
unsafe fn setActive(&self, _state: TBool) -> tresult {
|
||||
kResultOk
|
||||
}
|
||||
unsafe fn setState(&self, _state: *mut IBStream) -> tresult {
|
||||
kResultOk
|
||||
}
|
||||
unsafe fn getState(&self, _state: *mut IBStream) -> tresult {
|
||||
kResultOk
|
||||
}
|
||||
}
|
||||
|
||||
impl IAudioProcessorTrait for Wide {
|
||||
unsafe fn setBusArrangements(&self, _i: *mut SpeakerArrangement, _n_in: i32, _o: *mut SpeakerArrangement, _n_out: i32) -> tresult {
|
||||
kResultFalse
|
||||
}
|
||||
unsafe fn getBusArrangement(&self, _dir: BusDirection, _index: i32, _arr: *mut SpeakerArrangement) -> tresult {
|
||||
kNotImplemented
|
||||
}
|
||||
unsafe fn canProcessSampleSize(&self, size: i32) -> tresult {
|
||||
if size == SymbolicSampleSizes_::kSample32 as i32 { kResultOk } else { kNotImplemented }
|
||||
}
|
||||
unsafe fn getLatencySamples(&self) -> u32 {
|
||||
3
|
||||
}
|
||||
unsafe fn setupProcessing(&self, _setup: *mut ProcessSetup) -> tresult {
|
||||
kResultOk
|
||||
}
|
||||
unsafe fn setProcessing(&self, _state: TBool) -> tresult {
|
||||
kResultOk
|
||||
}
|
||||
unsafe fn process(&self, data: *mut ProcessData) -> tresult {
|
||||
let d = unsafe { &*data };
|
||||
let fits =
|
||||
|bufs: *mut AudioBusBuffers, n: i32| n == self.buses && (0..n as usize).all(|i| unsafe { &*bufs.add(i) }.numChannels == self.channels);
|
||||
let ok = fits(d.inputs, d.numInputs) && fits(d.outputs, d.numOutputs);
|
||||
if d.numOutputs > 0 {
|
||||
for ch in unsafe { channels(&*d.outputs, d.numSamples.max(0) as usize) } {
|
||||
ch.fill(if ok { 1.0 } else { -1.0 });
|
||||
}
|
||||
}
|
||||
kResultOk
|
||||
}
|
||||
unsafe fn getTailSamples(&self) -> u32 {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
// ---- factory -------------------------------------------------------------------------------
|
||||
|
||||
struct Factory {
|
||||
@@ -636,10 +736,13 @@ impl Class for Factory {
|
||||
type Interfaces = (IPluginFactory2,);
|
||||
}
|
||||
|
||||
const CLASSES: [(TUID, &str, &str, &str); 3] = [
|
||||
const CLASSES: [(TUID, &str, &str, &str); 6] = [
|
||||
(GAIN_CID, "Audio Module Class", "Fixture Gain", "Fx"),
|
||||
(GAIN_CTRL_CID, "Component Controller Class", "Fixture Gain", ""),
|
||||
(SYNTH_CID, "Audio Module Class", "Fixture Synth", "Instrument|Synth"),
|
||||
(WIDE16_CID, "Audio Module Class", "Fixture Wide 16x64", "Fx"),
|
||||
(WIDE17_CID, "Audio Module Class", "Fixture Wide 17 buses", "Fx"),
|
||||
(WIDE65_CID, "Audio Module Class", "Fixture Wide 65 channels", "Fx"),
|
||||
];
|
||||
|
||||
impl IPluginFactoryTrait for Factory {
|
||||
@@ -682,6 +785,10 @@ impl IPluginFactoryTrait for Factory {
|
||||
.to_com_ptr::<FUnknown>()
|
||||
} else if cid == SYNTH_CID {
|
||||
ComWrapper::new(Synth { level: AtomicU32::new(0), _live: Live::new(&OBJECTS) }).to_com_ptr::<FUnknown>()
|
||||
} else if let Some((buses, channels)) =
|
||||
[(WIDE16_CID, (16, 64)), (WIDE17_CID, (17, 2)), (WIDE65_CID, (1, 65))].into_iter().find_map(|(c, layout)| (c == cid).then_some(layout))
|
||||
{
|
||||
ComWrapper::new(Wide { buses, channels, _live: Live::new(&OBJECTS) }).to_com_ptr::<FUnknown>()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ fn id_of(name: &str) -> String {
|
||||
fn scan_lists_both_audio_modules_but_not_the_controller_class() {
|
||||
let dir = fixture_dir();
|
||||
let found = soundcraft_vst3_host::scan_paths(&[dir.to_path_buf()]);
|
||||
assert_eq!(found.len(), 2, "{found:?}");
|
||||
assert_eq!(found.len(), 5, "{found:?}");
|
||||
let g = found.iter().find(|d| d.name == "Fixture Gain").unwrap();
|
||||
// `uid()` lays out the first 8 bytes differently on Windows (COM GUID order).
|
||||
let want = if cfg!(windows) { "vst3:001D0A5C111122223333444455556666" } else { "vst3:5C0A1D00111122223333444455556666" };
|
||||
@@ -311,6 +311,26 @@ fn host_set_controller_param_survives_state_round_trip() {
|
||||
assert!((r.param("12").unwrap() - 200.0).abs() < 0.01, "a loaded state replaces earlier edits: {:?}", r.param("12"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layouts_beyond_the_host_limits_are_rejected_not_truncated() {
|
||||
// Control: exactly at the limits (16 buses of 64 channels) activates and gets its full layout.
|
||||
let mut p = soundcraft_vst3_host::create(&id_of("Fixture Wide 16x64")).unwrap();
|
||||
p.prepare(44_100.0, 64, 2);
|
||||
assert_eq!(p.latency(), 3);
|
||||
let mut io = vec![vec![0.5f32; 64]; 2];
|
||||
p.process(&mut io, 64);
|
||||
assert!(io.iter().flatten().all(|&x| x == 1.0), "16x64 layout delivered as advertised: {:?}", &io[0][..4]);
|
||||
// 17 buses, or a 65-channel bus: never activated, never processed with a cut-down layout.
|
||||
for name in ["Fixture Wide 17 buses", "Fixture Wide 65 channels"] {
|
||||
let mut p = soundcraft_vst3_host::create(&id_of(name)).unwrap();
|
||||
p.prepare(44_100.0, 64, 2);
|
||||
assert_eq!(p.latency(), 0, "{name}: not activated");
|
||||
let mut io = vec![vec![0.5f32; 64]; 2];
|
||||
p.process(&mut io, 64);
|
||||
assert!(io.iter().flatten().all(|&x| x == 0.5), "{name}: the plugin never ran: {:?}", &io[0][..4]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_view_is_probed_and_edits_come_back_as_plain_values() {
|
||||
let id = id_of("Fixture Gain");
|
||||
|
||||
Reference in New Issue
Block a user