mirror of
https://github.com/robbert-vdh/nih-plug.git
synced 2026-07-01 02:36:54 +00:00
554 lines
18 KiB
Rust
554 lines
18 KiB
Rust
//! This plugin demonstrates how to "bring your own GUI toolkit" using a raw OpenGL context.
|
|
|
|
use baseview::{gl::GlConfig, WindowHandle, WindowOpenOptions, WindowScalePolicy};
|
|
use crossbeam::atomic::AtomicCell;
|
|
use nih_plug::params::persist::PersistentField;
|
|
use nih_plug::prelude::*;
|
|
use raw_window_handle::{HasRawWindowHandle, RawWindowHandle};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::sync::{
|
|
atomic::{AtomicBool, Ordering},
|
|
Arc,
|
|
};
|
|
|
|
/// The time it takes for the peak meter to decay by 12 dB after switching to complete silence.
|
|
const PEAK_METER_DECAY_MS: f64 = 150.0;
|
|
|
|
pub struct CustomGlWindow {
|
|
gui_context: Arc<dyn GuiContext>,
|
|
gl: Arc<glow::Context>,
|
|
|
|
vertex_array: glow::NativeVertexArray,
|
|
program: glow::NativeProgram,
|
|
|
|
#[allow(unused)]
|
|
params: Arc<MyPluginParams>,
|
|
#[allow(unused)]
|
|
peak_meter: Arc<AtomicF32>,
|
|
}
|
|
|
|
impl Drop for CustomGlWindow {
|
|
fn drop(&mut self) {
|
|
use glow::HasContext as _;
|
|
|
|
unsafe {
|
|
self.gl.delete_program(self.program);
|
|
self.gl.delete_vertex_array(self.vertex_array);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CustomGlWindow {
|
|
fn new(
|
|
window: &mut baseview::Window<'_>,
|
|
gui_context: Arc<dyn GuiContext>,
|
|
params: Arc<MyPluginParams>,
|
|
peak_meter: Arc<AtomicF32>,
|
|
_scaling_factor: f32,
|
|
) -> Self {
|
|
use glow::HasContext as _;
|
|
|
|
// TODO: Return an error instead of panicking once baseview gets thats
|
|
// ability.
|
|
let gl_context = window
|
|
.gl_context()
|
|
.expect("failed to get baseview gl context");
|
|
|
|
let (gl, vertex_array, program) = unsafe {
|
|
gl_context.make_current();
|
|
|
|
#[allow(clippy::arc_with_non_send_sync)]
|
|
let gl = Arc::new(glow::Context::from_loader_function(|s| {
|
|
gl_context.get_proc_address(s)
|
|
}));
|
|
|
|
let vertex_array = gl
|
|
.create_vertex_array()
|
|
.expect("Cannot create vertex array");
|
|
gl.bind_vertex_array(Some(vertex_array));
|
|
|
|
let program = gl.create_program().expect("Cannot create program");
|
|
|
|
let (vertex_shader_source, fragment_shader_source) = (
|
|
r#"const vec2 verts[3] = vec2[3](
|
|
vec2(0.5f, 1.0f),
|
|
vec2(0.0f, 0.0f),
|
|
vec2(1.0f, 0.0f)
|
|
);
|
|
out vec2 vert;
|
|
void main() {
|
|
vert = verts[gl_VertexID];
|
|
gl_Position = vec4(vert - 0.5, 0.0, 1.0);
|
|
}"#,
|
|
r#"precision mediump float;
|
|
in vec2 vert;
|
|
out vec4 color;
|
|
void main() {
|
|
color = vec4(vert, 0.5, 1.0);
|
|
}"#,
|
|
);
|
|
|
|
let shader_sources = [
|
|
(glow::VERTEX_SHADER, vertex_shader_source),
|
|
(glow::FRAGMENT_SHADER, fragment_shader_source),
|
|
];
|
|
|
|
let mut shaders = Vec::with_capacity(shader_sources.len());
|
|
|
|
for (shader_type, shader_source) in shader_sources.iter() {
|
|
let shader = gl
|
|
.create_shader(*shader_type)
|
|
.expect("Cannot create shader");
|
|
gl.shader_source(shader, &format!("{}\n{}", "#version 130", shader_source));
|
|
gl.compile_shader(shader);
|
|
if !gl.get_shader_compile_status(shader) {
|
|
panic!("{}", gl.get_shader_info_log(shader));
|
|
}
|
|
gl.attach_shader(program, shader);
|
|
shaders.push(shader);
|
|
}
|
|
|
|
gl.link_program(program);
|
|
if !gl.get_program_link_status(program) {
|
|
panic!("{}", gl.get_program_info_log(program));
|
|
}
|
|
|
|
for shader in shaders {
|
|
gl.detach_shader(program, shader);
|
|
gl.delete_shader(shader);
|
|
}
|
|
|
|
gl.use_program(Some(program));
|
|
|
|
gl_context.make_not_current();
|
|
|
|
(gl, vertex_array, program)
|
|
};
|
|
|
|
Self {
|
|
gui_context,
|
|
gl,
|
|
vertex_array,
|
|
program,
|
|
params,
|
|
peak_meter,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl baseview::WindowHandler for CustomGlWindow {
|
|
fn on_frame(&mut self, window: &mut baseview::Window) {
|
|
use glow::HasContext as _;
|
|
// Do rendering here.
|
|
|
|
let (_width, _height) = self.params.editor_state.size();
|
|
|
|
let gl_context = window
|
|
.gl_context()
|
|
.expect("failed to get baseview gl context");
|
|
|
|
unsafe {
|
|
gl_context.make_current();
|
|
|
|
self.gl.clear_color(0.05, 0.05, 0.05, 1.0);
|
|
self.gl.clear(glow::COLOR_BUFFER_BIT);
|
|
|
|
self.gl.draw_arrays(glow::TRIANGLES, 0, 3);
|
|
|
|
gl_context.swap_buffers();
|
|
gl_context.make_not_current();
|
|
}
|
|
}
|
|
|
|
fn on_event(
|
|
&mut self,
|
|
_window: &mut baseview::Window,
|
|
event: baseview::Event,
|
|
) -> baseview::EventStatus {
|
|
// Use this to set parameter values.
|
|
let _param_setter = ParamSetter::new(self.gui_context.as_ref());
|
|
|
|
match &event {
|
|
// Do event processing here.
|
|
baseview::Event::Window(event) => match event {
|
|
baseview::WindowEvent::Resized(window_info) => {
|
|
self.params.editor_state.size.store((
|
|
window_info.logical_size().width.round() as u32,
|
|
window_info.logical_size().height.round() as u32,
|
|
));
|
|
}
|
|
_ => {}
|
|
},
|
|
_ => {}
|
|
}
|
|
|
|
baseview::EventStatus::Captured
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct CustomGlEditorState {
|
|
/// The window's size in logical pixels before applying `scale_factor`.
|
|
#[serde(with = "nih_plug::params::persist::serialize_atomic_cell")]
|
|
size: AtomicCell<(u32, u32)>,
|
|
/// Whether the editor's window is currently open.
|
|
#[serde(skip)]
|
|
open: AtomicBool,
|
|
}
|
|
|
|
impl CustomGlEditorState {
|
|
pub fn from_size(size: (u32, u32)) -> Arc<Self> {
|
|
Arc::new(Self {
|
|
size: AtomicCell::new(size),
|
|
open: AtomicBool::new(false),
|
|
})
|
|
}
|
|
|
|
/// Returns a `(width, height)` pair for the current size of the GUI in logical pixels.
|
|
pub fn size(&self) -> (u32, u32) {
|
|
self.size.load()
|
|
}
|
|
|
|
/// Whether the GUI is currently visible.
|
|
// Called `is_open()` instead of `open()` to avoid the ambiguity.
|
|
pub fn is_open(&self) -> bool {
|
|
self.open.load(Ordering::Acquire)
|
|
}
|
|
}
|
|
|
|
impl<'a> PersistentField<'a, CustomGlEditorState> for Arc<CustomGlEditorState> {
|
|
fn set(&self, new_value: CustomGlEditorState) {
|
|
self.size.store(new_value.size.load());
|
|
}
|
|
|
|
fn map<F, R>(&self, f: F) -> R
|
|
where
|
|
F: Fn(&CustomGlEditorState) -> R,
|
|
{
|
|
f(self)
|
|
}
|
|
}
|
|
|
|
pub struct CustomGlEditor {
|
|
params: Arc<MyPluginParams>,
|
|
peak_meter: Arc<AtomicF32>,
|
|
|
|
/// The scaling factor reported by the host, if any. On macOS this will never be set and we
|
|
/// should use the system scaling factor instead.
|
|
scaling_factor: AtomicCell<Option<f32>>,
|
|
}
|
|
|
|
impl Editor for CustomGlEditor {
|
|
fn spawn(
|
|
&self,
|
|
parent: ParentWindowHandle,
|
|
context: Arc<dyn GuiContext>,
|
|
) -> Box<dyn std::any::Any + Send> {
|
|
let (unscaled_width, unscaled_height) = self.params.editor_state.size();
|
|
let scaling_factor = self.scaling_factor.load();
|
|
|
|
let gui_context = Arc::clone(&context);
|
|
|
|
let params = Arc::clone(&self.params);
|
|
let peak_meter = Arc::clone(&self.peak_meter);
|
|
|
|
let window = baseview::Window::open_parented(
|
|
&ParentWindowHandleAdapter(parent),
|
|
WindowOpenOptions {
|
|
title: String::from("OpenGL Window"),
|
|
// Baseview should be doing the DPI scaling for us
|
|
size: baseview::Size::new(unscaled_width as f64, unscaled_height as f64),
|
|
// NOTE: For some reason passing 1.0 here causes the UI to be scaled on macOS but
|
|
// not the mouse events.
|
|
scale: scaling_factor
|
|
.map(|factor| WindowScalePolicy::ScaleFactor(factor as f64))
|
|
.unwrap_or(WindowScalePolicy::SystemScaleFactor),
|
|
|
|
gl_config: Some(GlConfig {
|
|
version: (3, 2),
|
|
red_bits: 8,
|
|
blue_bits: 8,
|
|
green_bits: 8,
|
|
alpha_bits: 8,
|
|
depth_bits: 24,
|
|
stencil_bits: 8,
|
|
samples: None,
|
|
srgb: true,
|
|
double_buffer: true,
|
|
vsync: false,
|
|
..Default::default()
|
|
}),
|
|
},
|
|
move |window: &mut baseview::Window<'_>| -> CustomGlWindow {
|
|
CustomGlWindow::new(
|
|
window,
|
|
gui_context,
|
|
params,
|
|
peak_meter,
|
|
scaling_factor.unwrap_or(1.0),
|
|
)
|
|
},
|
|
);
|
|
|
|
self.params.editor_state.open.store(true, Ordering::Release);
|
|
Box::new(CustomGlEditorHandle {
|
|
state: self.params.editor_state.clone(),
|
|
window,
|
|
})
|
|
}
|
|
|
|
fn size(&self) -> (u32, u32) {
|
|
self.params.editor_state.size()
|
|
}
|
|
|
|
fn set_scale_factor(&self, factor: f32) -> bool {
|
|
// If the editor is currently open then the host must not change the current HiDPI scale as
|
|
// we don't have a way to handle that. Ableton Live does this.
|
|
if self.params.editor_state.is_open() {
|
|
return false;
|
|
}
|
|
|
|
self.scaling_factor.store(Some(factor));
|
|
true
|
|
}
|
|
|
|
fn param_value_changed(&self, _id: &str, _normalized_value: f32) {
|
|
// As mentioned above, for now we'll always force a redraw to allow meter widgets to work
|
|
// correctly. In the future we can use an `Arc<AtomicBool>` and only force a redraw when
|
|
// that boolean is set.
|
|
}
|
|
|
|
fn param_modulation_changed(&self, _id: &str, _modulation_offset: f32) {}
|
|
|
|
fn param_values_changed(&self) {
|
|
// Same
|
|
}
|
|
}
|
|
|
|
/// The window handle used for [`CustomGlEditor`].
|
|
struct CustomGlEditorHandle {
|
|
state: Arc<CustomGlEditorState>,
|
|
window: WindowHandle,
|
|
}
|
|
|
|
/// The window handle enum stored within 'WindowHandle' contains raw pointers. Is there a way around
|
|
/// having this requirement?
|
|
unsafe impl Send for CustomGlEditorHandle {}
|
|
|
|
impl Drop for CustomGlEditorHandle {
|
|
fn drop(&mut self) {
|
|
self.state.open.store(false, Ordering::Release);
|
|
// XXX: This should automatically happen when the handle gets dropped, but apparently not
|
|
self.window.close();
|
|
}
|
|
}
|
|
|
|
/// This version of `baseview` uses a different version of `raw_window_handle than NIH-plug, so we
|
|
/// need to adapt it ourselves.
|
|
struct ParentWindowHandleAdapter(nih_plug::editor::ParentWindowHandle);
|
|
|
|
unsafe impl HasRawWindowHandle for ParentWindowHandleAdapter {
|
|
fn raw_window_handle(&self) -> RawWindowHandle {
|
|
match self.0 {
|
|
ParentWindowHandle::X11Window(window) => {
|
|
let mut handle = raw_window_handle::XcbWindowHandle::empty();
|
|
handle.window = window;
|
|
RawWindowHandle::Xcb(handle)
|
|
}
|
|
ParentWindowHandle::AppKitNsView(ns_view) => {
|
|
let mut handle = raw_window_handle::AppKitWindowHandle::empty();
|
|
handle.ns_view = ns_view;
|
|
RawWindowHandle::AppKit(handle)
|
|
}
|
|
ParentWindowHandle::Win32Hwnd(hwnd) => {
|
|
let mut handle = raw_window_handle::Win32WindowHandle::empty();
|
|
handle.hwnd = hwnd;
|
|
RawWindowHandle::Win32(handle)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// This is mostly identical to the gain example, minus some fluff, and with a GUI.
|
|
pub struct MyPlugin {
|
|
params: Arc<MyPluginParams>,
|
|
|
|
/// Needed to normalize the peak meter's response based on the sample rate.
|
|
peak_meter_decay_weight: f32,
|
|
/// The current data for the peak meter. This is stored as an [`Arc`] so we can share it between
|
|
/// the GUI and the audio processing parts. If you have more state to share, then it's a good
|
|
/// idea to put all of that in a struct behind a single `Arc`.
|
|
///
|
|
/// This is stored as voltage gain.
|
|
peak_meter: Arc<AtomicF32>,
|
|
}
|
|
|
|
#[derive(Params)]
|
|
pub struct MyPluginParams {
|
|
/// The editor state, saved together with the parameter state so the custom scaling can be
|
|
/// restored.
|
|
#[persist = "editor-state"]
|
|
editor_state: Arc<CustomGlEditorState>,
|
|
|
|
#[id = "gain"]
|
|
pub gain: FloatParam,
|
|
|
|
#[id = "foobar"]
|
|
pub some_int: IntParam,
|
|
}
|
|
|
|
impl Default for MyPlugin {
|
|
fn default() -> Self {
|
|
Self {
|
|
params: Arc::new(MyPluginParams::default()),
|
|
|
|
peak_meter_decay_weight: 1.0,
|
|
peak_meter: Arc::new(AtomicF32::new(util::MINUS_INFINITY_DB)),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for MyPluginParams {
|
|
fn default() -> Self {
|
|
Self {
|
|
editor_state: CustomGlEditorState::from_size((400, 300)),
|
|
|
|
// See the main gain example for more details
|
|
gain: FloatParam::new(
|
|
"Gain",
|
|
util::db_to_gain(0.0),
|
|
FloatRange::Skewed {
|
|
min: util::db_to_gain(-30.0),
|
|
max: util::db_to_gain(30.0),
|
|
factor: FloatRange::gain_skew_factor(-30.0, 30.0),
|
|
},
|
|
)
|
|
.with_smoother(SmoothingStyle::Logarithmic(50.0))
|
|
.with_unit(" dB")
|
|
.with_value_to_string(formatters::v2s_f32_gain_to_db(2))
|
|
.with_string_to_value(formatters::s2v_f32_gain_to_db()),
|
|
some_int: IntParam::new("Something", 3, IntRange::Linear { min: 0, max: 3 }),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Plugin for MyPlugin {
|
|
const NAME: &'static str = "BYO GUI Example (OpenGL)";
|
|
const VENDOR: &'static str = "Moist Plugins GmbH";
|
|
const URL: &'static str = "https://youtu.be/dQw4w9WgXcQ";
|
|
const EMAIL: &'static str = "info@example.com";
|
|
|
|
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
|
|
|
const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[
|
|
AudioIOLayout {
|
|
main_input_channels: NonZeroU32::new(2),
|
|
main_output_channels: NonZeroU32::new(2),
|
|
..AudioIOLayout::const_default()
|
|
},
|
|
AudioIOLayout {
|
|
main_input_channels: NonZeroU32::new(1),
|
|
main_output_channels: NonZeroU32::new(1),
|
|
..AudioIOLayout::const_default()
|
|
},
|
|
];
|
|
|
|
const SAMPLE_ACCURATE_AUTOMATION: bool = true;
|
|
|
|
type SysExMessage = ();
|
|
type BackgroundTask = ();
|
|
|
|
fn params(&self) -> Arc<dyn Params> {
|
|
self.params.clone()
|
|
}
|
|
|
|
fn editor(&mut self, _async_executor: AsyncExecutor<Self>) -> Option<Box<dyn Editor>> {
|
|
Some(Box::new(CustomGlEditor {
|
|
params: Arc::clone(&self.params),
|
|
peak_meter: Arc::clone(&self.peak_meter),
|
|
|
|
// TODO: We can't get the size of the window when baseview does its own scaling, so if the
|
|
// host does not set a scale factor on Windows or Linux we should just use a factor of
|
|
// 1. That may make the GUI tiny but it also prevents it from getting cut off.
|
|
#[cfg(target_os = "macos")]
|
|
scaling_factor: AtomicCell::new(None),
|
|
#[cfg(not(target_os = "macos"))]
|
|
scaling_factor: AtomicCell::new(Some(1.0)),
|
|
}))
|
|
}
|
|
|
|
fn initialize(
|
|
&mut self,
|
|
_audio_io_layout: &AudioIOLayout,
|
|
buffer_config: &BufferConfig,
|
|
_context: &mut impl InitContext<Self>,
|
|
) -> bool {
|
|
// After `PEAK_METER_DECAY_MS` milliseconds of pure silence, the peak meter's value should
|
|
// have dropped by 12 dB
|
|
self.peak_meter_decay_weight = 0.25f64
|
|
.powf((buffer_config.sample_rate as f64 * PEAK_METER_DECAY_MS / 1000.0).recip())
|
|
as f32;
|
|
|
|
true
|
|
}
|
|
|
|
fn process(
|
|
&mut self,
|
|
buffer: &mut Buffer,
|
|
_aux: &mut AuxiliaryBuffers,
|
|
_context: &mut impl ProcessContext<Self>,
|
|
) -> ProcessStatus {
|
|
for channel_samples in buffer.iter_samples() {
|
|
let mut amplitude = 0.0;
|
|
let num_samples = channel_samples.len();
|
|
|
|
let gain = self.params.gain.smoothed.next();
|
|
for sample in channel_samples {
|
|
*sample *= gain;
|
|
amplitude += *sample;
|
|
}
|
|
|
|
// To save resources, a plugin can (and probably should!) only perform expensive
|
|
// calculations that are only displayed on the GUI while the GUI is open
|
|
if self.params.editor_state.is_open() {
|
|
amplitude = (amplitude / num_samples as f32).abs();
|
|
let current_peak_meter = self.peak_meter.load(std::sync::atomic::Ordering::Relaxed);
|
|
let new_peak_meter = if amplitude > current_peak_meter {
|
|
amplitude
|
|
} else {
|
|
current_peak_meter * self.peak_meter_decay_weight
|
|
+ amplitude * (1.0 - self.peak_meter_decay_weight)
|
|
};
|
|
|
|
self.peak_meter
|
|
.store(new_peak_meter, std::sync::atomic::Ordering::Relaxed)
|
|
}
|
|
}
|
|
|
|
ProcessStatus::Normal
|
|
}
|
|
}
|
|
|
|
impl ClapPlugin for MyPlugin {
|
|
const CLAP_ID: &'static str = "com.moist-plugins-gmbh.byo-gui-gl";
|
|
const CLAP_DESCRIPTION: Option<&'static str> =
|
|
Some("A simple example plugin with a raw OpenGL context for rendering");
|
|
const CLAP_MANUAL_URL: Option<&'static str> = Some(Self::URL);
|
|
const CLAP_SUPPORT_URL: Option<&'static str> = None;
|
|
const CLAP_FEATURES: &'static [ClapFeature] = &[
|
|
ClapFeature::AudioEffect,
|
|
ClapFeature::Stereo,
|
|
ClapFeature::Mono,
|
|
ClapFeature::Utility,
|
|
];
|
|
}
|
|
|
|
impl Vst3Plugin for MyPlugin {
|
|
const VST3_CLASS_ID: [u8; 16] = *b"ByoGuiOpenGLWooo";
|
|
const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] =
|
|
&[Vst3SubCategory::Fx, Vst3SubCategory::Tools];
|
|
}
|
|
|
|
nih_export_clap!(MyPlugin);
|
|
nih_export_vst3!(MyPlugin);
|