diff --git a/src/context.rs b/src/context.rs index fd98fee26..3e2326760 100644 --- a/src/context.rs +++ b/src/context.rs @@ -5,6 +5,7 @@ use std::fmt::Display; pub mod gui; pub mod init; pub mod process; +pub mod track_info; // Contexts for more plugin-API specific features pub mod remote_controls; diff --git a/src/context/gui.rs b/src/context/gui.rs index 35d7d159f..29371fb80 100644 --- a/src/context/gui.rs +++ b/src/context/gui.rs @@ -2,6 +2,7 @@ use std::sync::Arc; +use super::track_info::TrackInfo; use super::PluginApi; use crate::prelude::{Param, ParamPtr, Plugin, PluginState}; @@ -63,6 +64,11 @@ pub trait GuiContext: Send + Sync + 'static { /// host. If the plugin is currently processing audio, then the parameter values will be /// restored at the end of the current processing cycle. fn set_state(&self, state: PluginState); + + /// Get information about the track the plugin is on. Not all hosts support this, and the + /// information may not be available until the host provides it. Returns `None` if the host + /// does not provide track information. + fn track_info(&self) -> Option; } /// An way to run background tasks from the plugin's GUI, equivalent to the diff --git a/src/context/init.rs b/src/context/init.rs index e1934b573..3f7f2cadf 100644 --- a/src/context/init.rs +++ b/src/context/init.rs @@ -1,5 +1,6 @@ //! A context passed during plugin initialization. +use super::track_info::TrackInfo; use super::PluginApi; use crate::prelude::Plugin; @@ -34,4 +35,9 @@ pub trait InitContext { /// runtime allows the host to better optimize polyphonic modulation, or to switch to strictly /// monophonic modulation when dropping the capacity down to 1. fn set_current_voice_capacity(&self, capacity: u32); + + /// Get information about the track the plugin is on. Not all hosts support this, and the + /// information may not be available until the host provides it. Returns `None` if the host + /// does not provide track information. + fn track_info(&self) -> Option; } diff --git a/src/context/process.rs b/src/context/process.rs index 66687aa5b..dbefa699a 100644 --- a/src/context/process.rs +++ b/src/context/process.rs @@ -1,5 +1,6 @@ //! A context passed during the process function. +use super::track_info::TrackInfo; use super::PluginApi; use crate::prelude::{Plugin, PluginNoteEvent}; @@ -40,6 +41,11 @@ pub trait ProcessContext { /// Get information about the current transport position and status. fn transport(&self) -> &Transport; + /// Get information about the track the plugin is on. Not all hosts support this, and the + /// information may not be available until the host provides it. Returns `None` if the host + /// does not provide track information. + fn track_info(&self) -> Option; + /// Returns the next note event, if there is one. Use /// [`NoteEvent::timing()`][crate::prelude::NoteEvent::timing()] to get the event's timing /// within the buffer. Only available when diff --git a/src/context/track_info.rs b/src/context/track_info.rs new file mode 100644 index 000000000..20db755ae --- /dev/null +++ b/src/context/track_info.rs @@ -0,0 +1,43 @@ +//! Information about the track/channel the plugin is inserted on. + +/// Information about the track the plugin is on. Not all hosts and plugin APIs support all fields, +/// so most of them are optional. +/// +/// This is queried from the host and may change at any time. It can be accessed from +/// [`InitContext`][super::init::InitContext], +/// [`ProcessContext`][super::process::ProcessContext], and +/// [`GuiContext`][super::gui::GuiContext]. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct TrackInfo { + /// The name of the track, if available. + pub name: Option, + /// The color assigned to the track, if available. + pub color: Option, + /// The number of audio channels on the track, if available. + pub audio_channel_count: Option, + /// The type of track the plugin is on. + pub track_type: TrackType, +} + +/// An RGBA color value. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct TrackColor { + pub red: u8, + pub green: u8, + pub blue: u8, + pub alpha: u8, +} + +/// The type of track the plugin is inserted on. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub enum TrackType { + /// A regular track. + #[default] + Regular, + /// A return/FX track. + Return, + /// A bus/group track. + Bus, + /// The master/main output track. + Master, +} diff --git a/src/prelude.rs b/src/prelude.rs index c37599d2e..f7440704c 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -23,6 +23,7 @@ pub use crate::context::process::{ProcessContext, Transport}; pub use crate::context::remote_controls::{ RemoteControlsContext, RemoteControlsPage, RemoteControlsSection, }; +pub use crate::context::track_info::{TrackColor, TrackInfo, TrackType}; pub use crate::context::PluginApi; // This also includes the derive macro pub use crate::editor::{Editor, ParentWindowHandle}; diff --git a/src/wrapper/clap/context.rs b/src/wrapper/clap/context.rs index cf3668f2a..2a6fbe062 100644 --- a/src/wrapper/clap/context.rs +++ b/src/wrapper/clap/context.rs @@ -1,7 +1,5 @@ use atomic_refcell::AtomicRefMut; -use clap_sys::ext::remote_controls::{ - clap_remote_controls_page, CLAP_REMOTE_CONTROLS_COUNT, -}; +use clap_sys::ext::remote_controls::{clap_remote_controls_page, CLAP_REMOTE_CONTROLS_COUNT}; use clap_sys::id::{clap_id, CLAP_INVALID_ID}; use clap_sys::string_sizes::CLAP_NAME_SIZE; use std::cell::Cell; @@ -9,6 +7,7 @@ use std::collections::{HashMap, VecDeque}; use std::sync::Arc; use super::wrapper::{OutputParamEvent, Task, Wrapper}; +use crate::context::track_info::TrackInfo; use crate::event_loop::EventLoop; use crate::prelude::{ ClapPlugin, GuiContext, InitContext, ParamPtr, PluginApi, PluginNoteEvent, ProcessContext, @@ -90,6 +89,10 @@ impl InitContext

for WrapperInitContext<'_, P> { fn set_current_voice_capacity(&self, capacity: u32) { self.wrapper.set_current_voice_capacity(capacity) } + + fn track_info(&self) -> Option { + self.wrapper.get_track_info() + } } impl ProcessContext

for WrapperProcessContext<'_, P> { @@ -112,6 +115,10 @@ impl ProcessContext

for WrapperProcessContext<'_, P> { &self.transport } + fn track_info(&self) -> Option { + self.wrapper.get_track_info() + } + fn next_event(&mut self) -> Option> { self.input_events_guard.pop_front() } @@ -240,6 +247,10 @@ impl GuiContext for WrapperGuiContext

{ fn set_state(&self, state: crate::wrapper::state::PluginState) { self.wrapper.set_state_object_from_gui(state) } + + fn track_info(&self) -> Option { + self.wrapper.get_track_info() + } } /// A remote control section. The plugin can fill this with information for one or more pages. diff --git a/src/wrapper/clap/wrapper.rs b/src/wrapper/clap/wrapper.rs index 1ff8e8a66..b328f748c 100644 --- a/src/wrapper/clap/wrapper.rs +++ b/src/wrapper/clap/wrapper.rs @@ -22,9 +22,6 @@ use clap_sys::ext::audio_ports::{ use clap_sys::ext::audio_ports_config::{ clap_audio_ports_config, clap_plugin_audio_ports_config, CLAP_EXT_AUDIO_PORTS_CONFIG, }; -use clap_sys::ext::remote_controls::{ - clap_plugin_remote_controls, clap_remote_controls_page, CLAP_EXT_REMOTE_CONTROLS, -}; use clap_sys::ext::gui::{ clap_gui_resize_hints, clap_host_gui, clap_plugin_gui, clap_window, CLAP_EXT_GUI, CLAP_WINDOW_API_COCOA, CLAP_WINDOW_API_WIN32, CLAP_WINDOW_API_X11, @@ -40,6 +37,9 @@ use clap_sys::ext::params::{ CLAP_PARAM_IS_MODULATABLE, CLAP_PARAM_IS_MODULATABLE_PER_NOTE_ID, CLAP_PARAM_IS_READONLY, CLAP_PARAM_IS_STEPPED, CLAP_PARAM_RESCAN_VALUES, }; +use clap_sys::ext::remote_controls::{ + clap_plugin_remote_controls, clap_remote_controls_page, CLAP_EXT_REMOTE_CONTROLS, +}; use clap_sys::ext::render::{ clap_plugin_render, clap_plugin_render_mode, CLAP_EXT_RENDER, CLAP_RENDER_OFFLINE, CLAP_RENDER_REALTIME, @@ -47,6 +47,12 @@ use clap_sys::ext::render::{ use clap_sys::ext::state::{clap_plugin_state, CLAP_EXT_STATE}; use clap_sys::ext::tail::{clap_plugin_tail, CLAP_EXT_TAIL}; use clap_sys::ext::thread_check::{clap_host_thread_check, CLAP_EXT_THREAD_CHECK}; +use clap_sys::ext::track_info::{ + clap_host_track_info, clap_plugin_track_info, CLAP_EXT_TRACK_INFO, CLAP_EXT_TRACK_INFO_COMPAT, + CLAP_TRACK_INFO_HAS_AUDIO_CHANNEL, CLAP_TRACK_INFO_HAS_TRACK_COLOR, + CLAP_TRACK_INFO_HAS_TRACK_NAME, CLAP_TRACK_INFO_IS_FOR_BUS, CLAP_TRACK_INFO_IS_FOR_MASTER, + CLAP_TRACK_INFO_IS_FOR_RETURN_TRACK, +}; use clap_sys::ext::voice_info::{ clap_host_voice_info, clap_plugin_voice_info, clap_voice_info, CLAP_EXT_VOICE_INFO, CLAP_VOICE_INFO_SUPPORTS_OVERLAPPING_NOTES, @@ -63,7 +69,7 @@ use clap_sys::stream::{clap_istream, clap_ostream}; use crossbeam::atomic::AtomicCell; use crossbeam::channel::{self, SendTimeoutError}; use crossbeam::queue::ArrayQueue; -use parking_lot::Mutex; +use parking_lot::{Mutex, RwLock}; use std::any::Any; use std::borrow::Borrow; use std::collections::{HashMap, HashSet, VecDeque}; @@ -80,6 +86,7 @@ use std::time::Duration; use super::context::{WrapperGuiContext, WrapperInitContext, WrapperProcessContext}; use super::descriptor::PluginDescriptor; use super::util::ClapPtr; +use crate::context::track_info::{TrackColor, TrackInfo, TrackType}; use crate::event_loop::{BackgroundThread, EventLoop, MainThreadExecutor, TASK_QUEUE_CAPACITY}; use crate::midi::MidiResult; use crate::prelude::{ @@ -242,6 +249,11 @@ pub struct Wrapper { /// context. This defaults to the maximum number of voices. current_voice_capacity: AtomicU32, + clap_plugin_track_info: clap_plugin_track_info, + host_track_info: AtomicRefCell>>, + /// The current track information, as reported by the host through the track-info extension. + track_info: RwLock>, + /// A queue of tasks that still need to be performed. Because CLAP lets the plugin request a /// host callback directly, we don't need to use the OsEventLoop we use in our other plugin /// implementations. Instead, we'll post tasks to this queue, ask the host to call @@ -681,6 +693,12 @@ impl Wrapper

{ .unwrap_or(1), ), + clap_plugin_track_info: clap_plugin_track_info { + changed: Some(Self::ext_track_info_changed), + }, + host_track_info: AtomicRefCell::new(None), + track_info: RwLock::new(None), + tasks: ArrayQueue::new(TASK_QUEUE_CAPACITY), main_thread_id: thread::current().id(), // Initialized later as it needs a reference to the wrapper for the executor @@ -1855,6 +1873,18 @@ impl Wrapper

{ &wrapper.host_callback, CLAP_EXT_THREAD_CHECK, ); + *wrapper.host_track_info.borrow_mut() = query_host_extension::( + &wrapper.host_callback, + CLAP_EXT_TRACK_INFO, + ) + .or_else(|| { + query_host_extension::( + &wrapper.host_callback, + CLAP_EXT_TRACK_INFO_COMPAT, + ) + }); + // Fetch initial track info if available + wrapper.update_track_info(); true } @@ -2337,6 +2367,8 @@ impl Wrapper

{ &wrapper.clap_plugin_tail as *const _ as *const c_void } else if id == CLAP_EXT_VOICE_INFO && P::CLAP_POLY_MODULATION_CONFIG.is_some() { &wrapper.clap_plugin_voice_info as *const _ as *const c_void + } else if id == CLAP_EXT_TRACK_INFO || id == CLAP_EXT_TRACK_INFO_COMPAT { + &wrapper.clap_plugin_track_info as *const _ as *const c_void } else { nih_trace!("Host tried to query unknown extension {:?}", id); std::ptr::null() @@ -3210,6 +3242,79 @@ impl Wrapper

{ None => false, } } + + unsafe extern "C" fn ext_track_info_changed(plugin: *const clap_plugin) { + check_null_ptr!((), plugin, (*plugin).plugin_data); + let wrapper = &*((*plugin).plugin_data as *const Self); + + wrapper.update_track_info(); + } + + /// Query the host for the current track information and store it. + fn update_track_info(&self) { + let host_track_info = self.host_track_info.borrow(); + let host_track_info = match host_track_info.as_ref() { + Some(h) => h, + None => return, + }; + + let mut info = std::mem::MaybeUninit::uninit(); + let success = unsafe { + clap_call! { host_track_info=>get(&*self.host_callback, info.as_mut_ptr()) } + }; + + if success { + let info = unsafe { info.assume_init() }; + + let name = if info.flags & CLAP_TRACK_INFO_HAS_TRACK_NAME != 0 { + let name_cstr = unsafe { CStr::from_ptr(info.name.as_ptr()) }; + name_cstr.to_str().ok().map(|s| s.to_string()) + } else { + None + }; + + let color = if info.flags & CLAP_TRACK_INFO_HAS_TRACK_COLOR != 0 { + Some(TrackColor { + red: info.color.red, + green: info.color.green, + blue: info.color.blue, + alpha: info.color.alpha, + }) + } else { + None + }; + + let audio_channel_count = if info.flags & CLAP_TRACK_INFO_HAS_AUDIO_CHANNEL != 0 { + u32::try_from(info.audio_channel_count).ok() + } else { + None + }; + + let track_type = if info.flags & CLAP_TRACK_INFO_IS_FOR_MASTER != 0 { + TrackType::Master + } else if info.flags & CLAP_TRACK_INFO_IS_FOR_RETURN_TRACK != 0 { + TrackType::Return + } else if info.flags & CLAP_TRACK_INFO_IS_FOR_BUS != 0 { + TrackType::Bus + } else { + TrackType::Regular + }; + + *self.track_info.write() = Some(TrackInfo { + name, + color, + audio_channel_count, + track_type, + }); + } else { + *self.track_info.write() = None; + } + } + + /// Get the current track info, if available. + pub fn get_track_info(&self) -> Option { + self.track_info.read().clone() + } } /// Convenience function to query an extension from the host. diff --git a/src/wrapper/standalone/context.rs b/src/wrapper/standalone/context.rs index 86bd639f9..35ddda9ae 100644 --- a/src/wrapper/standalone/context.rs +++ b/src/wrapper/standalone/context.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use super::backend::Backend; use super::wrapper::{Task, Wrapper}; +use crate::context::track_info::TrackInfo; use crate::prelude::{ GuiContext, InitContext, ParamPtr, Plugin, PluginApi, PluginNoteEvent, ProcessContext, Transport, @@ -52,6 +53,10 @@ impl> InitContext

for WrapperInitContext<'_, P, B> { fn set_current_voice_capacity(&self, _capacity: u32) { // This is only supported by CLAP } + + fn track_info(&self) -> Option { + None + } } impl> ProcessContext

for WrapperProcessContext<'_, P, B> { @@ -97,6 +102,10 @@ impl> ProcessContext

for WrapperProcessContext<'_, P fn set_current_voice_capacity(&self, _capacity: u32) { // This is only supported by CLAP } + + fn track_info(&self) -> Option { + None + } } impl> GuiContext for WrapperGuiContext { @@ -159,4 +168,8 @@ impl> GuiContext for WrapperGuiContext { fn set_state(&self, state: crate::wrapper::state::PluginState) { self.wrapper.set_state_object_from_gui(state) } + + fn track_info(&self) -> Option { + None + } } diff --git a/src/wrapper/vst3/context.rs b/src/wrapper/vst3/context.rs index 75b5b5036..c22f7a3b7 100644 --- a/src/wrapper/vst3/context.rs +++ b/src/wrapper/vst3/context.rs @@ -5,6 +5,7 @@ use std::sync::atomic::Ordering; use std::sync::Arc; use vst3_sys::vst::IComponentHandler; +use crate::context::track_info::TrackInfo; use crate::prelude::{ GuiContext, InitContext, ParamPtr, PluginApi, PluginNoteEvent, PluginState, ProcessContext, Transport, Vst3Plugin, @@ -79,6 +80,10 @@ impl InitContext

for WrapperInitContext<'_, P> { fn set_current_voice_capacity(&self, _capacity: u32) { // This is only supported by CLAP } + + fn track_info(&self) -> Option { + self.inner.get_track_info() + } } impl ProcessContext

for WrapperProcessContext<'_, P> { @@ -101,6 +106,10 @@ impl ProcessContext

for WrapperProcessContext<'_, P> { &self.transport } + fn track_info(&self) -> Option { + self.inner.get_track_info() + } + fn next_event(&mut self) -> Option> { self.input_events_guard.pop_front() } @@ -228,4 +237,8 @@ impl GuiContext for WrapperGuiContext

{ fn set_state(&self, state: PluginState) { self.inner.set_state_object_from_gui(state) } + + fn track_info(&self) -> Option { + self.inner.get_track_info() + } } diff --git a/src/wrapper/vst3/inner.rs b/src/wrapper/vst3/inner.rs index add0a1a1f..d95f727c8 100644 --- a/src/wrapper/vst3/inner.rs +++ b/src/wrapper/vst3/inner.rs @@ -1,3 +1,4 @@ +use crate::context::track_info::TrackInfo; use atomic_refcell::AtomicRefCell; use crossbeam::atomic::AtomicCell; use crossbeam::channel::{self, SendTimeoutError}; @@ -137,6 +138,9 @@ pub(crate) struct WrapperInner { /// having to add a setter function to the parameter (or even worse, have it be completely /// untyped). pub param_ptr_to_hash: HashMap, + + /// The current track information, as reported by the host through `IInfoListener`. + pub track_info: RwLock>, } /// Tasks that can be sent from the plugin to be executed on the main thread in a non-blocking @@ -318,6 +322,8 @@ impl WrapperInner

{ param_units, param_id_to_hash, param_ptr_to_hash, + + track_info: RwLock::new(None), }); // FIXME: Right now this is safe, but if we are going to have a singleton main thread queue @@ -430,6 +436,11 @@ impl WrapperInner

{ .map(|s| s.as_str()) } + /// Get the current track info, if available. + pub fn get_track_info(&self) -> Option { + self.track_info.read().clone() + } + /// Convenience function for setting a value for a parameter as triggered by a VST3 parameter /// update. The same rate is for updating parameter smoothing. /// diff --git a/src/wrapper/vst3/wrapper.rs b/src/wrapper/vst3/wrapper.rs index f284af54a..7d428d13d 100644 --- a/src/wrapper/vst3/wrapper.rs +++ b/src/wrapper/vst3/wrapper.rs @@ -10,11 +10,12 @@ use vst3_sys::base::{kInvalidArgument, kNoInterface, kResultFalse, kResultOk, tr use vst3_sys::base::{IBStream, IPluginBase}; use vst3_sys::utils::SharedVstPtr; use vst3_sys::vst::{ - kNoParamId, kNoParentUnitId, kNoProgramListId, kRootUnitId, Event, EventTypes, IAudioProcessor, - IComponent, IEditController, IEventList, IMidiMapping, INoteExpressionController, - IParamValueQueue, IParameterChanges, IProcessContextRequirements, IUnitInfo, - LegacyMidiCCOutEvent, NoteExpressionTypeInfo, NoteExpressionValueDescription, NoteOffEvent, - NoteOnEvent, ParameterFlags, PolyPressureEvent, ProgramListInfo, TChar, UnitInfo, + kChannelColorKey, kChannelNameKey, kNoParamId, kNoParentUnitId, kNoProgramListId, kRootUnitId, + Event, EventTypes, IAttributeList, IAudioProcessor, IComponent, IEditController, IEventList, + IInfoListener, IMidiMapping, INoteExpressionController, IParamValueQueue, IParameterChanges, + IProcessContextRequirements, IUnitInfo, LegacyMidiCCOutEvent, NoteExpressionTypeInfo, + NoteExpressionValueDescription, NoteOffEvent, NoteOnEvent, ParameterFlags, PolyPressureEvent, + ProgramListInfo, TChar, UnitInfo, }; use vst3_sys::VST3; use widestring::U16CStr; @@ -26,6 +27,7 @@ use super::util::{ }; use super::util::{VST3_MIDI_CHANNELS, VST3_MIDI_PARAMS_END}; use super::view::WrapperView; +use crate::context::track_info::{TrackColor, TrackInfo, TrackType}; use crate::prelude::{ AuxiliaryBuffers, BufferConfig, MidiConfig, NoteEvent, ParamFlags, ProcessMode, ProcessStatus, SysExMessage, Transport, Vst3Plugin, @@ -45,7 +47,8 @@ use vst3_sys as vst3_com; IMidiMapping, INoteExpressionController, IProcessContextRequirements, - IUnitInfo + IUnitInfo, + IInfoListener ))] pub struct Wrapper { inner: Arc>, @@ -1893,3 +1896,74 @@ impl IUnitInfo for Wrapper

{ kInvalidArgument } } + +impl IInfoListener for Wrapper

{ + unsafe fn set_channel_context_infos(&self, list: *mut c_void) -> tresult { + if list.is_null() { + *self.inner.track_info.write() = None; + return kResultOk; + } + + // The list parameter is an IAttributeList* passed as c_void*. In the vst3-sys COM + // model, an interface pointer is `*mut *mut VTable`, so we cast accordingly. + let attr_list: vst3_sys::VstPtr = match vst3_sys::VstPtr::shared( + list as *mut *mut ::VTable, + ) { + Some(ptr) => ptr, + None => return kResultOk, + }; + + // Read channel name + let mut name_buf = [0i16; 128]; + let name = if attr_list.get_string( + kChannelNameKey, + name_buf.as_mut_ptr(), + name_buf.len() as u32, + ) == kResultOk + { + // Ensure null termination in case a buggy host fills the entire buffer + name_buf[name_buf.len() - 1] = 0; + U16CStr::from_ptr_str(name_buf.as_ptr() as *const u16) + .to_string() + .ok() + } else { + None + }; + + // Read channel color (ColorSpec is a u32 packed as ARGB) + let mut color_value: i64 = 0; + let color = if attr_list.get_int(kChannelColorKey, &mut color_value) == kResultOk { + let cs = color_value as u32; + Some(TrackColor { + alpha: ((cs >> 24) & 0xFF) as u8, + red: ((cs >> 16) & 0xFF) as u8, + green: ((cs >> 8) & 0xFF) as u8, + blue: (cs & 0xFF) as u8, + }) + } else { + None + }; + + // Merge with existing track info rather than replacing, since some hosts (e.g. + // Ableton Live) may send partial updates with name and color in separate calls + let mut track_info = self.inner.track_info.write(); + if let Some(existing) = track_info.as_mut() { + if name.is_some() { + existing.name = name; + } + if color.is_some() { + existing.color = color; + } + } else { + *track_info = Some(TrackInfo { + name, + color, + // VST3's IInfoListener doesn't provide channel count or track type + audio_channel_count: None, + track_type: TrackType::Regular, + }); + } + + kResultOk + } +}