1
0
Fork 0

Add transport information for VST3 and CLAP

This is available through the process context.
This commit is contained in:
Robbert van der Helm 2022-03-04 15:05:00 +01:00
parent 713e778117
commit e2605c8cee
7 changed files with 155 additions and 15 deletions

View file

@ -15,6 +15,9 @@ use crate::plugin::NoteEvent;
// The implementing wrapper needs to be able to handle concurrent requests, and it should perform
// the actual callback within [MainThreadQueue::do_maybe_async].
pub trait ProcessContext {
/// Get information about the current transport position and status.
fn transport(&self) -> &Transport;
/// Return the next note event, if there is one. The event contains the timing
///
/// TODO: Rethink this API, both in terms of ergonomics, and if we can do this in a way that
@ -82,7 +85,7 @@ pub trait GuiContext: Send + Sync + 'static {
/// Information about the plugin's transport. Depending on the plugin API and the host not all
/// fields may be available.
#[derive(Debug, Default)]
#[derive(Debug)]
pub struct Transport {
/// Whether the transport is currently running.
pub playing: bool,
@ -144,6 +147,30 @@ pub struct ParamSetter<'a> {
// TODO: These conversions have not really been tested yet, there might be an error in there somewhere
impl Transport {
/// Initialize the transport struct without any information.
pub(crate) fn new(sample_rate: f32) -> Self {
Self {
playing: false,
recording: false,
preroll_active: None,
sample_rate,
tempo: None,
time_sig_numerator: None,
time_sig_denominator: None,
pos_samples: None,
pos_seconds: None,
pos_beats: None,
bar_start_pos_beats: None,
bar_number: None,
loop_range_samples: None,
loop_range_seconds: None,
loop_range_beats: None,
}
}
/// The position in the song in samples. Will be calculated from other information if needed.
pub fn pos_samples(&self) -> Option<i64> {
match (

View file

@ -18,7 +18,6 @@ use crate::param::internals::Params;
/// - Sidechain inputs
/// - Multiple output busses
/// - Special handling for offline processing
/// - Transport and other context information in the process call
/// - Sample accurate automation (this would be great, but sadly few hosts even support it so until
/// they do we'll ignore that it's a thing)
/// - Parameter hierarchies/groups

View file

@ -4,7 +4,7 @@ use std::sync::atomic::Ordering;
use std::sync::Arc;
use super::wrapper::{OutputParamChange, Task, Wrapper};
use crate::context::{GuiContext, ProcessContext};
use crate::context::{GuiContext, ProcessContext, Transport};
use crate::event_loop::EventLoop;
use crate::param::internals::ParamPtr;
use crate::plugin::{ClapPlugin, NoteEvent};
@ -22,6 +22,7 @@ pub(crate) struct WrapperGuiContext<P: ClapPlugin> {
pub(crate) struct WrapperProcessContext<'a, P: ClapPlugin> {
pub(super) wrapper: &'a Wrapper<P>,
pub(super) input_events_guard: AtomicRefMut<'a, VecDeque<NoteEvent>>,
pub(super) transport: Transport,
}
impl<P: ClapPlugin> GuiContext for WrapperGuiContext<P> {
@ -71,6 +72,10 @@ impl<P: ClapPlugin> GuiContext for WrapperGuiContext<P> {
}
impl<P: ClapPlugin> ProcessContext for WrapperProcessContext<'_, P> {
fn transport(&self) -> &Transport {
&self.transport
}
fn next_midi_event(&mut self) -> Option<NoteEvent> {
self.input_events_guard.pop_front()
}

View file

@ -7,7 +7,10 @@ use clap_sys::events::{
clap_event_header, clap_event_note, clap_event_param_mod, clap_event_param_value,
clap_input_events, clap_output_events, CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_MIDI,
CLAP_EVENT_NOTE_EXPRESSION, CLAP_EVENT_NOTE_OFF, CLAP_EVENT_NOTE_ON, CLAP_EVENT_PARAM_MOD,
CLAP_EVENT_PARAM_VALUE, CLAP_EVENT_SHOULD_RECORD,
CLAP_EVENT_PARAM_VALUE, CLAP_EVENT_SHOULD_RECORD, CLAP_TRANSPORT_HAS_BEATS_TIMELINE,
CLAP_TRANSPORT_HAS_SECONDS_TIMELINE, CLAP_TRANSPORT_HAS_TEMPO,
CLAP_TRANSPORT_HAS_TIME_SIGNATURE, CLAP_TRANSPORT_IS_LOOP_ACTIVE, CLAP_TRANSPORT_IS_PLAYING,
CLAP_TRANSPORT_IS_RECORDING, CLAP_TRANSPORT_IS_WITHIN_PRE_ROLL,
};
use clap_sys::ext::audio_ports::{
clap_audio_port_info, clap_plugin_audio_ports, CLAP_AUDIO_PORT_IS_MAIN, CLAP_EXT_AUDIO_PORTS,
@ -24,6 +27,7 @@ use clap_sys::ext::params::{
};
use clap_sys::ext::state::{clap_plugin_state, CLAP_EXT_STATE};
use clap_sys::ext::thread_check::{clap_host_thread_check, CLAP_EXT_THREAD_CHECK};
use clap_sys::fixedpoint::{CLAP_BEATTIME_FACTOR, CLAP_SECTIME_FACTOR};
use clap_sys::host::clap_host;
use clap_sys::id::{clap_id, CLAP_INVALID_ID};
use clap_sys::plugin::clap_plugin;
@ -59,6 +63,7 @@ use super::context::{WrapperGuiContext, WrapperProcessContext};
use super::descriptor::PluginDescriptor;
use super::util::ClapPtr;
use crate::buffer::Buffer;
use crate::context::Transport;
use crate::event_loop::{EventLoop, MainThreadExecutor, TASK_QUEUE_CAPACITY};
use crate::param::internals::ParamPtr;
use crate::plugin::{
@ -487,10 +492,11 @@ impl<P: ClapPlugin> Wrapper<P> {
Arc::new(WrapperGuiContext { wrapper: self })
}
fn make_process_context(&self) -> WrapperProcessContext<'_, P> {
fn make_process_context(&self, transport: Transport) -> WrapperProcessContext<'_, P> {
WrapperProcessContext {
wrapper: self,
input_events_guard: self.input_events.borrow_mut(),
transport,
}
}
@ -731,7 +737,7 @@ impl<P: ClapPlugin> Wrapper<P> {
if wrapper.plugin.write().initialize(
&bus_config,
&buffer_config,
&mut wrapper.make_process_context(),
&mut wrapper.make_process_context(Transport::new(buffer_config.sample_rate)),
) {
// Preallocate enough room in the output slices vector so we can convert a `*mut *mut
// f32` to a `&mut [&mut f32]` in the process call
@ -864,8 +870,63 @@ impl<P: ClapPlugin> Wrapper<P> {
}
}
// Some of the fields are left empty because CLAP does not provide this information, but
// the methods on [`Transport`] can reconstruct these values from the other fields
let sample_rate = wrapper
.current_buffer_config
.load()
.expect("Process call without prior initialization call")
.sample_rate;
let mut transport = Transport::new(sample_rate);
if !process.transport.is_null() {
let context = &*process.transport;
transport.playing = context.flags & CLAP_TRANSPORT_IS_PLAYING != 0;
transport.recording = context.flags & CLAP_TRANSPORT_IS_RECORDING != 0;
transport.preroll_active =
Some(context.flags & CLAP_TRANSPORT_IS_WITHIN_PRE_ROLL != 0);
if context.flags & CLAP_TRANSPORT_HAS_TEMPO != 0 {
transport.tempo = Some(context.tempo);
}
if context.flags & CLAP_TRANSPORT_HAS_TIME_SIGNATURE != 0 {
transport.time_sig_numerator = Some(context.tsig_num as i32);
transport.time_sig_denominator = Some(context.tsig_denom as i32);
}
if context.flags & CLAP_TRANSPORT_HAS_BEATS_TIMELINE != 0 {
transport.pos_beats =
Some(context.song_pos_beats as f64 / CLAP_BEATTIME_FACTOR as f64);
}
if context.flags & CLAP_TRANSPORT_HAS_SECONDS_TIMELINE != 0 {
transport.pos_seconds =
Some(context.song_pos_seconds as f64 / CLAP_SECTIME_FACTOR as f64);
}
// TODO: CLAP does not mention whether this is behind a flag or not
transport.bar_start_pos_beats =
Some(context.bar_start as f64 / CLAP_BEATTIME_FACTOR as f64);
transport.bar_number = Some(context.bar_number);
// TODO: They also aren't very clear about this, but presumably if the loop is
// active and the corresponding song transport information is available then
// this is also available
if context.flags & CLAP_TRANSPORT_IS_LOOP_ACTIVE != 0
&& context.flags & CLAP_TRANSPORT_HAS_BEATS_TIMELINE != 0
{
transport.loop_range_beats = Some((
context.loop_start_beats as f64 / CLAP_BEATTIME_FACTOR as f64,
context.loop_end_beats as f64 / CLAP_BEATTIME_FACTOR as f64,
));
}
if context.flags & CLAP_TRANSPORT_IS_LOOP_ACTIVE != 0
&& context.flags & CLAP_TRANSPORT_HAS_SECONDS_TIMELINE != 0
{
transport.loop_range_seconds = Some((
context.loop_start_seconds as f64 / CLAP_SECTIME_FACTOR as f64,
context.loop_end_seconds as f64 / CLAP_SECTIME_FACTOR as f64,
));
}
}
let mut plugin = wrapper.plugin.write();
let mut context = wrapper.make_process_context();
let mut context = wrapper.make_process_context(transport);
let result = match plugin.process(&mut output_buffer, &mut context) {
ProcessStatus::Error(err) => {
nih_debug_assert_failure!("Process error: {}", err);
@ -1606,7 +1667,7 @@ impl<P: ClapPlugin> Wrapper<P> {
wrapper.plugin.write().initialize(
&bus_config,
&buffer_config,
&mut wrapper.make_process_context(),
&mut wrapper.make_process_context(Transport::new(buffer_config.sample_rate)),
);
}

View file

@ -5,7 +5,7 @@ use std::sync::Arc;
use vst3_sys::vst::IComponentHandler;
use super::inner::{Task, WrapperInner};
use crate::context::{GuiContext, ProcessContext};
use crate::context::{GuiContext, ProcessContext, Transport};
use crate::event_loop::EventLoop;
use crate::param::internals::ParamPtr;
use crate::plugin::{NoteEvent, Vst3Plugin};
@ -23,6 +23,7 @@ pub(crate) struct WrapperGuiContext<P: Vst3Plugin> {
pub(crate) struct WrapperProcessContext<'a, P: Vst3Plugin> {
pub(super) inner: &'a WrapperInner<P>,
pub(super) input_events_guard: AtomicRefMut<'a, VecDeque<NoteEvent>>,
pub(super) transport: Transport,
}
impl<P: Vst3Plugin> GuiContext for WrapperGuiContext<P> {
@ -91,6 +92,10 @@ impl<P: Vst3Plugin> GuiContext for WrapperGuiContext<P> {
}
impl<P: Vst3Plugin> ProcessContext for WrapperProcessContext<'_, P> {
fn transport(&self) -> &Transport {
&self.transport
}
fn next_midi_event(&mut self) -> Option<NoteEvent> {
self.input_events_guard.pop_front()
}

View file

@ -12,6 +12,7 @@ use super::context::{WrapperGuiContext, WrapperProcessContext};
use super::util::{ObjectPtr, VstPtr, BYPASS_PARAM_HASH, BYPASS_PARAM_ID};
use super::view::WrapperView;
use crate::buffer::Buffer;
use crate::context::Transport;
use crate::event_loop::{EventLoop, MainThreadExecutor, OsEventLoop};
use crate::param::internals::ParamPtr;
use crate::plugin::{BufferConfig, BusConfig, Editor, NoteEvent, ProcessStatus, Vst3Plugin};
@ -193,10 +194,11 @@ impl<P: Vst3Plugin> WrapperInner<P> {
Arc::new(WrapperGuiContext { inner: self })
}
pub fn make_process_context(&self) -> WrapperProcessContext<'_, P> {
pub fn make_process_context(&self, transport: Transport) -> WrapperProcessContext<'_, P> {
WrapperProcessContext {
inner: self,
input_events_guard: self.input_events.borrow_mut(),
transport,
}
}

View file

@ -17,6 +17,7 @@ use widestring::U16CStr;
use super::inner::WrapperInner;
use super::util::{VstPtr, BYPASS_PARAM_HASH, BYPASS_PARAM_ID};
use super::view::WrapperView;
use crate::context::Transport;
use crate::plugin::{BufferConfig, BusConfig, NoteEvent, ProcessStatus, Vst3Plugin};
use crate::wrapper::state;
use crate::wrapper::util::{process_wrapper, u16strlcpy};
@ -236,7 +237,9 @@ impl<P: Vst3Plugin> IComponent for Wrapper<P> {
self.inner.plugin.write().initialize(
&bus_config,
&buffer_config,
&mut self.inner.make_process_context(),
&mut self
.inner
.make_process_context(Transport::new(buffer_config.sample_rate)),
);
}
@ -593,7 +596,9 @@ impl<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
if self.inner.plugin.write().initialize(
&bus_config,
&buffer_config,
&mut self.inner.make_process_context(),
&mut self
.inner
.make_process_context(Transport::new(buffer_config.sample_rate)),
) {
// Preallocate enough room in the output slices vector so we can convert a `*mut *mut
// f32` to a `&mut [&mut f32]` in the process call
@ -634,7 +639,8 @@ impl<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
.inner
.current_buffer_config
.load()
.map(|c| c.sample_rate);
.expect("Process call without prior setup call")
.sample_rate;
if let Some(param_changes) = data.input_param_changes.upgrade() {
let num_param_queues = param_changes.get_parameter_count();
for change_queue_idx in 0..num_param_queues {
@ -658,7 +664,7 @@ impl<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
self.inner.set_normalized_value_by_hash(
param_hash,
value as f32,
sample_rate,
Some(sample_rate),
);
}
}
@ -771,8 +777,43 @@ impl<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
}
}
// Some of the fields are left empty because VST3 does not provide this information, but
// the methods on [`Transport`] can reconstruct these values from the other fields
let mut transport = Transport::new(sample_rate);
if !data.context.is_null() {
let context = &*data.context;
// These constants are missing from vst3-sys, see:
// https://steinbergmedia.github.io/vst3_doc/vstinterfaces/structSteinberg_1_1Vst_1_1ProcessContext.html
transport.playing = context.state & (1 << 1) != 0; // kPlaying
transport.recording = context.state & (1 << 3) != 0; // kRecording
if context.state & (1 << 10) != 0 {
// kTempoValid
transport.tempo = Some(context.tempo);
}
if context.state & (1 << 13) != 0 {
// kTimeSigValid
transport.time_sig_numerator = Some(context.time_sig_num);
transport.time_sig_denominator = Some(context.time_sig_den);
}
transport.pos_samples = Some(context.project_time_samples);
if context.state & (1 << 9) != 0 {
// kProjectTimeMusicValid
transport.pos_beats = Some(context.project_time_music);
}
if context.state & (1 << 11) != 0 {
// kBarPositionValid
transport.bar_start_pos_beats = Some(context.bar_position_music);
}
if context.state & (1 << 2) != 0 && context.state & (1 << 12) != 0 {
// kCycleActive && kCycleValid
transport.loop_range_beats =
Some((context.cycle_start_music, context.cycle_end_music));
}
}
let mut plugin = self.inner.plugin.write();
let mut context = self.inner.make_process_context();
let mut context = self.inner.make_process_context(transport);
match plugin.process(&mut output_buffer, &mut context) {
ProcessStatus::Error(err) => {
nih_debug_assert_failure!("Process error: {}", err);