From 029fc9d3cf4768d94a6e37380e1dcc1e758ae876 Mon Sep 17 00:00:00 2001 From: ozpv <39195175+ozpv@users.noreply.github.com> Date: Sat, 20 Jun 2026 03:11:03 -0500 Subject: add vst3 support --- src/lib.rs | 420 ++++++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 335 insertions(+), 85 deletions(-) (limited to 'src/lib.rs') diff --git a/src/lib.rs b/src/lib.rs index d753321..0908ee7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,114 +1,364 @@ -use crate::{ - audio::RetainPluginAudioProcessor, - gui::{AtomicVec2, RetainPluginGui}, - params::RetainParams, -}; -use clack_extensions::{ - audio_ports::PluginAudioPorts, - gui::PluginGui, - latency::{PluginLatency, PluginLatencyImpl}, - params::PluginParams, - state::PluginState, -}; -use clack_plugin::{ - plugin::features::{AUDIO_EFFECT, STEREO}, - prelude::*, -}; -use std::sync::Arc; +#![allow(clippy::cast_precision_loss)] -mod audio; -mod gui; -mod params; mod retain; mod window_function; mod window_size; mod window_type; mod windowed_fft; -/// The type that represents our plugin in Clack. -/// -/// This is what implements the [`Plugin`] trait, where all the other subtypes are attached. -pub struct RetainPlugin; - -impl Plugin for RetainPlugin { - type AudioProcessor<'a> = RetainPluginAudioProcessor<'a>; - type Shared<'a> = RetainPluginShared<'a>; - type MainThread<'a> = RetainPluginMainThread<'a>; - - fn declare_extensions( - builder: &mut PluginExtensions, - _shared: Option<&RetainPluginShared>, - ) { - builder - .register::() - .register::() - .register::() - .register::() - .register::(); - } +use crate::{ + retain::retain_top_n_magnitudes, window_size::WindowSize, window_type::WindowType, + windowed_fft::WindowedRealFft, +}; +use egui::{Align, CentralPanel, ComboBox, Layout, Slider, Vec2}; +use nice_plug::prelude::*; +use nice_plug_egui::{ + EguiSettings, EguiState, create_egui_editor, resizable_window::ResizableWindow, +}; +use rtrb::RingBuffer; +use std::sync::Arc; + +pub const DEFAULT_WINDOW_TYPE: WindowType = WindowType::Hann; + +const MIN_WINDOW_WIDTH: u32 = 600; +const MIN_WINDOW_HEIGHT: u32 = 350; + +const GUI_TO_AUDIO_CHANNEL_CAPACITY: usize = 128; + +pub struct Retain { + params: Arc, + msg_channel: AudioMsgChannel, + initial_gui_state: Option, + fft_left: WindowedRealFft, + fft_right: WindowedRealFft, } -impl DefaultPluginFactory for RetainPlugin { - fn get_descriptor() -> PluginDescriptor { - PluginDescriptor::new("com.haemolacriaa.retain", "Retain") - .with_description("Retains only the nth largest magnitude frequencies in a signal") - .with_version("0.1.0-pre") - .with_vendor("haemolacriaa") - .with_features([AUDIO_EFFECT, STEREO]) - } +impl Default for Retain { + fn default() -> Self { + let (to_audio_tx, from_gui_rx) = RingBuffer::new(GUI_TO_AUDIO_CHANNEL_CAPACITY); - fn new_shared(host: HostSharedHandle<'_>) -> Result, PluginError> { - Ok(Self::Shared::new(host)) - } + let params = RetainParams::default(); + + let fft_left = WindowedRealFft::new((params.window_size.value() as usize).into()); + let fft_right = WindowedRealFft::new((params.window_size.value() as usize).into()); - fn new_main_thread<'a>( - _host: HostMainThreadHandle<'a>, - shared: &'a Self::Shared<'a>, - ) -> Result, PluginError> { - Ok(Self::MainThread { shared, gui: None }) + Self { + params: Arc::new(params), + msg_channel: AudioMsgChannel { from_gui_rx }, + initial_gui_state: Some(GuiState { + msg_channel: GuiMsgChannel { to_audio_tx }, + }), + fft_left, + fft_right, + } } } -/// The plugin data that gets shared between the Main Thread and the Audio Thread. -pub struct RetainPluginShared<'a> { - /// The plugin's parameter values. - params: Arc, - gui_size: Arc, - host: HostSharedHandle<'a>, +#[derive(Params)] +pub struct RetainParams { + /// This allows the resizing of the plugin to be restored + #[persist = "editor-state"] + editor_state: Arc, + + /// the number of top frequencies to keep + #[id = "order"] + pub order: IntParam, + + /// the window size of the fft + /// this effects the frequency resolution and can make for cool effects + #[id = "window_size"] + pub window_size: IntParam, + + /// the type of window function used to reduce spectral leakage or clicking sounds + /// changing this makes a big difference + #[id = "window_type"] + pub window_type: EnumParam, + + /// whether the plugin retains or removes the highest magnitudes + #[id = "complement"] + pub complement: BoolParam, } -impl<'a> RetainPluginShared<'a> { - fn new(host: HostSharedHandle<'a>) -> Self { - RetainPluginShared { - params: Arc::new(RetainParams::new()), - gui_size: Arc::new(AtomicVec2::new()), - host, +impl Default for RetainParams { + fn default() -> Self { + Self { + editor_state: EguiState::from_size(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT), + order: IntParam::new("Order", 1, IntRange::Linear { min: 0, max: 32768 }), + window_size: IntParam::new( + "Window Size", + 1024, + IntRange::Linear { + min: 256, + max: 32768, + }, + ) + .non_automatable() + .hide(), + window_type: EnumParam::new("Window Type", WindowType::Hann) + .non_automatable() + .hide(), + complement: BoolParam::new("Complement", false).non_automatable().hide(), } } } -impl<'a> PluginShared<'a> for RetainPluginShared<'a> {} +pub struct GuiState { + msg_channel: GuiMsgChannel, +} + +#[derive(Debug)] +pub enum GuiToAudioMsg { + WindowSizeUpdate, + WindowTypeUpdate, +} + +pub struct GuiMsgChannel { + to_audio_tx: rtrb::Producer, +} -/// The data that belongs to the main thread of our plugin. -pub struct RetainPluginMainThread<'a> { - /// A reference to the plugin's shared data. - shared: &'a RetainPluginShared<'a>, - /// The plugin's GUI state and context - gui: Option, +pub struct AudioMsgChannel { + from_gui_rx: rtrb::Consumer, } -impl<'a> PluginMainThread<'a, RetainPluginShared<'a>> for RetainPluginMainThread<'a> { - fn on_main_thread(&mut self) { - if let Some(gui) = &self.gui { - gui.request_repaint(); +impl Plugin for Retain { + const NAME: &'static str = "Retain"; + const VENDOR: &'static str = "haemolacriaa"; + const URL: &'static str = "https://git.haemolacriaa.com/retain/"; + const EMAIL: &'static str = ""; + 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() + }]; + const SAMPLE_ACCURATE_AUTOMATION: bool = false; + + type SysExMessage = (); + type BackgroundTask = (); + + fn initialize( + &mut self, + _audio_io_layout: &AudioIOLayout, + _buffer_config: &BufferConfig, + context: &mut impl InitContext, + ) -> bool { + context.set_latency_samples(self.params.window_size.value().cast_unsigned()); + + true + } + + fn editor(&mut self, _async_executor: AsyncExecutor) -> Option> { + let params = self.params.clone(); + let egui_state = params.editor_state.clone(); + + create_egui_editor( + self.params.editor_state.clone(), + self.initial_gui_state.take().unwrap(), + EguiSettings::default(), + |_egui_ctx, _queue, _gui_state| {}, + move |ui, setter, _queue, gui_state| { + ResizableWindow::new("retainwindow") + .min_size(Vec2::new(MIN_WINDOW_WIDTH as f32, MIN_WINDOW_HEIGHT as f32)) + .show(ui, egui_state.as_ref(), |ui| { + CentralPanel::default().show_inside(ui, |ui| { + ui.heading("Retain"); + + ui.horizontal(|ui| { + ui.with_layout(Layout::top_down(Align::LEFT), |ui| { + let mut order = params.order.value(); + let window_size = params.window_size.value(); + + let slider = ui.add( + Slider::new(&mut order, 0..=window_size) + .text("Order") + .logarithmic(true), + ); + + if slider.changed() { + setter.set_parameter(¶ms.order, order); + } + + let mut window_size = Into::::into( + params.window_size.value() as usize, + ); + + ComboBox::from_label("Window Size") + .selected_text(window_size.as_str()) + .show_ui(ui, |ui| { + for size in WindowSize::iter() { + let display = size.as_str(); + let inner = size.inner(); + + if ui + .selectable_value( + &mut window_size, + size, + display, + ) + .changed() + { + setter.set_parameter( + ¶ms.window_size, + inner as i32, + ); + + if let Err(e) = gui_state.msg_channel.to_audio_tx.push(GuiToAudioMsg::WindowSizeUpdate) { + nice_error!("Failed to send message to audio thread: {}", e); + } + } + } + }); + }); + + ui.with_layout(Layout::top_down(Align::RIGHT), |ui| { + let mut complement = params.complement.value(); + + if ui.checkbox(&mut complement, "Complement").changed() { + setter.set_parameter(¶ms.complement, complement); + } + + let window_type = params.window_type.value(); + let selected = window_type.as_str(); + let mut window_type = window_type.as_byte(); + + ComboBox::from_label("Window Function") + .selected_text(selected) + .show_ui(ui, |ui| { + for function in WindowType::iter() { + let display = function.as_str(); + let byte = function.as_byte(); + + if ui + .selectable_value( + &mut window_type, + function.as_byte(), + display, + ) + .changed() + { + setter.set_parameter( + ¶ms.window_type, + byte.into(), + ); + + if let Err(e) = gui_state.msg_channel.to_audio_tx.push(GuiToAudioMsg::WindowTypeUpdate) { + nice_error!("Failed to send message to audio thread: {}", e); + } + } + } + }); + }); + }); + }); + }); + }, + ) + } + + fn process( + &mut self, + buffer: &mut Buffer, + _aux: &mut AuxiliaryBuffers, + context: &mut impl ProcessContext, + ) -> ProcessStatus { + // this is to prevent multiple updates from slowing this thread down if it somehow happens + let mut previously_updated_window_size = false; + let mut previously_updated_window_type = false; + + while let Ok(msg) = self.msg_channel.from_gui_rx.pop() { + match msg { + GuiToAudioMsg::WindowSizeUpdate => { + if previously_updated_window_size { + continue; + } + + let window_size = self.params.window_size.value() as usize; + + self.fft_left.window_size(window_size.into()); + self.fft_right.window_size(window_size.into()); + + context.set_latency_samples(window_size as u32); + + previously_updated_window_size = true; + } + GuiToAudioMsg::WindowTypeUpdate => { + if previously_updated_window_type { + continue; + } + + let window_type = self.params.window_type.value(); + + self.fft_left.window_function(&window_type); + self.fft_right.window_function(&window_type); + + previously_updated_window_type = true; + } + } + } + + if let [left, right] = buffer.as_slice() { + for sample in left.iter_mut() { + if self.fft_left.push_back_input(*sample) { + self.fft_left.forward(); + + let order = self.params.order.value() as usize; + let complement = self.params.complement.value(); + + retain_top_n_magnitudes(self.fft_left.get_spectrum(), order, complement); + + self.fft_left.inverse(); + self.fft_left.clear_input(); + } + + *sample = self.fft_left.pop_front_output(); + } + + for sample in right.iter_mut() { + if self.fft_right.push_back_input(*sample) { + self.fft_right.forward(); + + let order = self.params.order.value() as usize; + let complement = self.params.complement.value(); + + retain_top_n_magnitudes(self.fft_right.get_spectrum(), order, complement); + + self.fft_right.inverse(); + self.fft_right.clear_input(); + } + + *sample = self.fft_right.pop_front_output(); + } } + + ProcessStatus::Normal } -} -impl PluginLatencyImpl for RetainPluginMainThread<'_> { - fn get(&mut self) -> u32 { - self.shared.params.get_window_size().inner() as u32 + fn params(&self) -> Arc { + self.params.clone() } } -clack_export_entry!(SinglePluginEntry); +impl ClapPlugin for Retain { + const CLAP_ID: &'static str = "com.haemolacriaa.retain"; + const CLAP_DESCRIPTION: Option<&'static str> = + Some("Retain is a real-time audio plugin that retains the nth largest magnitude frequencies in a signal"); + 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::Filter, + ClapFeature::Stereo, + ]; +} + +impl Vst3Plugin for Retain { + const VST3_CLASS_ID: [u8; 16] = *b"Retainhaemolacir"; + const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] = &[ + Vst3SubCategory::Fx, + Vst3SubCategory::Filter, + Vst3SubCategory::Stereo, + ]; +} + +//nice_export_clap!(Retain); +nice_export_vst3!(Retain); -- cgit v1.2.3