diff options
| author | ozpv <39195175+ozpv@users.noreply.github.com> | 2026-06-20 03:11:03 -0500 |
|---|---|---|
| committer | ozpv <39195175+ozpv@users.noreply.github.com> | 2026-06-20 03:11:03 -0500 |
| commit | 029fc9d3cf4768d94a6e37380e1dcc1e758ae876 (patch) | |
| tree | e38d6533ac8a08fca1109e6e4ee92fe2e227e274 | |
| parent | 15452d1352dc01e4618a54c8159d8f049225d4fd (diff) | |
add vst3 support
| -rw-r--r-- | Cargo.toml | 33 | ||||
| -rw-r--r-- | src/audio.rs | 204 | ||||
| -rw-r--r-- | src/gui.rs | 295 | ||||
| -rw-r--r-- | src/lib.rs | 420 | ||||
| -rw-r--r-- | src/params.rs | 315 | ||||
| -rw-r--r-- | src/window_function.rs | 25 | ||||
| -rw-r--r-- | src/window_size.rs | 5 | ||||
| -rw-r--r-- | src/window_type.rs | 3 | ||||
| -rw-r--r-- | src/windowed_fft.rs | 9 |
9 files changed, 365 insertions, 944 deletions
@@ -1,35 +1,26 @@ [package] name = "retain" -version = "0.1.0-pre" +version = "0.1.0-pre2" +authors = [ "haemolacriaa" ] +description = "Retain is a real-time audio plugin that retains the nth largest magnitude frequencies in a signal" edition = "2024" +repository = "https://git.haemolacriaa.com/retain" +publish = false [lib] crate-type = ["rlib", "cdylib"] - [dependencies] -clack-plugin = { path = "../clack/plugin" } -clack-extensions = { path = "../clack/extensions", features = [ - "audio-ports", - "clack-plugin", - "gui", - "note-ports", - "params", - "raw-window-handle_05", - "state", - "latency", -] } -egui-baseview = { git = "https://codeberg.org/RustAudio/egui-baseview.git", version = "0.2.7" } - -# for calculation +nice-plug = { + path = "../nice-plug/crates/nice-plug", + version = "0.1.0", + features = ["assert_process_allocs"] +} +nice-plug-egui = { path = "../nice-plug/crates/nice-plug-egui", version = "0.1.5" } realfft = { version = "3.5.0", features = ["avx", "neon", "sse"] } num-complex = "0.4.6" rustc-hash = "2.1.2" -atomic_float = "1.1.0" - -# for saving the state -serde = { version = "1.0.228", features = ["derive"] } -serde_json = "1.0.150" +rtrb = "0.3.4" egui = "0.34.3" [profile.release] diff --git a/src/audio.rs b/src/audio.rs deleted file mode 100644 index 4aaac9f..0000000 --- a/src/audio.rs +++ /dev/null @@ -1,204 +0,0 @@ -use crate::{ - RetainPluginMainThread, RetainPluginShared, params::RetainParams, - retain::retain_top_n_magnitudes, window_size::WindowSize, window_type::WindowType, - windowed_fft::WindowedRealFft, -}; -use clack_extensions::{ - audio_ports::{ - AudioPortFlags, AudioPortInfo, AudioPortInfoWriter, AudioPortType, PluginAudioPortsImpl, - }, - latency::HostLatency, - params::PluginAudioProcessorParams, -}; -use clack_plugin::prelude::*; -use std::sync::Arc; - -/// Our plugin's audio processor. It lives in the audio thread. -/// -/// It receives parameter events, and process a stereo audio signal by operating on the given audio -/// buffer. -pub struct RetainPluginAudioProcessor<'a> { - /// The local state of the parameters - params: Arc<RetainParams>, - /// A reference to the plugin's shared data. - shared: &'a RetainPluginShared<'a>, - /// Our handle to the host - host: HostAudioProcessorHandle<'a>, - /// Fft for the left channel - fft_left: WindowedRealFft, - /// Fft for the right channel - fft_right: WindowedRealFft, - /// Previous window type - prev_window_type: Option<WindowType>, - /// Previous window size - prev_window_size: Option<WindowSize>, -} - -impl<'a> PluginAudioProcessor<'a, RetainPluginShared<'a>, RetainPluginMainThread<'a>> - for RetainPluginAudioProcessor<'a> -{ - fn activate( - host: HostAudioProcessorHandle<'a>, - _main_thread: &mut RetainPluginMainThread, - shared: &'a RetainPluginShared<'a>, - _audio_config: PluginAudioConfiguration, - ) -> Result<Self, PluginError> { - let params = Arc::clone(&shared.params); - - let fft_left = WindowedRealFft::new(params.get_window_size()); - let fft_right = WindowedRealFft::new(params.get_window_size()); - - // This is where we would allocate intermediate buffers and such if we needed them. - Ok(Self { - params, - shared, - host, - fft_left, - fft_right, - prev_window_size: None, - prev_window_type: None, - }) - } - - fn process( - &mut self, - _process: Process, - mut audio: Audio, - events: Events, - ) -> Result<ProcessStatus, PluginError> { - // Ensure at least a single input/output port pair, which contains channels of `f32` - // audio sample data. - let mut port_pair = audio - .port_pair(0) - .ok_or(PluginError::Message("No input/output ports found"))?; - - let mut output_channels = port_pair - .channels()? - .into_f32() - .ok_or(PluginError::Message("Expected f32 input/output"))?; - - // Extract the buffer slices that we need, while making sure they are paired correctly and - // check for either in-place or separate buffers. - let mut output_buffers = [None, None]; - - for (pair, buf) in output_channels.iter_mut().zip(&mut output_buffers) { - *buf = match pair { - ChannelPair::InputOnly(_) | ChannelPair::OutputOnly(_) => None, - ChannelPair::InPlace(b) => Some(b), - ChannelPair::InputOutput(i, o) => { - o.copy_from_slice(i); - Some(o) - } - } - } - - // update window size and latency if it has changed - // updates fft window sizes only if necessary - let window_size = self.params.get_window_size(); - if self.prev_window_size != Some(window_size.clone()) { - self.fft_left.window_size(window_size.clone()); - self.fft_right.window_size(window_size.clone()); - - if let Some(latency) = self.shared.host.get_extension::<HostLatency>() { - // SAFETY: should be safe as self.shared.host is a shared - // version of the main thread handle - // no reason to propagate it through all these layers - let mut main = unsafe { self.shared.host.as_main_thread_unchecked() }; - - latency.changed(&mut main); - // self.shared.host.request_restart(); - } - - self.prev_window_size = Some(window_size); - } - - // update change in window type if needed - let window_type = self.params.get_window_type(); - if self.prev_window_type != Some(window_type.clone()) { - self.fft_left.window_function(&window_type); - self.fft_right.window_function(&window_type); - self.prev_window_type = Some(window_type); - } - - // process the audio - for event_batch in events.input.batch() { - for event in event_batch.events() { - self.params.handle_event(event); - } - - // Get the parameters after all changes have been handled. - let order = self.params.get_order(); - - // this parameter is read-only and can only be changed in the UI - let complement = self.params.get_complement(); - - // process samples in place here - if let [Some(left), Some(right)] = &mut output_buffers { - let range = event_batch.sample_bounds(); - - for sample in &mut left[range] { - if self.fft_left.push_back_input(*sample) { - self.fft_left.forward(); - - 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 &mut right[range] { - if self.fft_right.push_back_input(*sample) { - self.fft_right.forward(); - - 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(); - } - } - } - - // Publish any parameter changes we may have received back to the GUI. - // Request the on-main-thread callback, which we use to refresh the UI if it is open - self.host.request_callback(); - - Ok(ProcessStatus::ContinueIfNotQuiet) - } -} - -impl PluginAudioPortsImpl for RetainPluginMainThread<'_> { - fn count(&mut self, _is_input: bool) -> u32 { - 1 - } - - fn get(&mut self, index: u32, _is_input: bool, writer: &mut AudioPortInfoWriter) { - if index == 0 { - writer.set(&AudioPortInfo { - id: ClapId::new(0), - name: b"main", - channel_count: 2, - flags: AudioPortFlags::IS_MAIN, - port_type: Some(AudioPortType::STEREO), - in_place_pair: None, - }); - } - } -} - -impl PluginAudioProcessorParams for RetainPluginAudioProcessor<'_> { - fn flush( - &mut self, - input_parameter_changes: &InputEvents, - _output_parameter_changes: &mut OutputEvents, - ) { - for event in input_parameter_changes { - self.params.handle_event(event); - } - } -} diff --git a/src/gui.rs b/src/gui.rs deleted file mode 100644 index 7032a79..0000000 --- a/src/gui.rs +++ /dev/null @@ -1,295 +0,0 @@ -//! Contains all types and implementations related to the plugin's GUI - -use crate::{ - RetainPluginMainThread, RetainPluginShared, params::RetainParams, window_size::WindowSize, - window_type::WindowType, -}; -use atomic_float::AtomicF32; -use clack_extensions::gui::{GuiApiType, GuiConfiguration, GuiSize, PluginGuiImpl, Window}; -use clack_plugin::prelude::*; -use egui::{Align, CentralPanel, ComboBox, Context, Layout, Slider, Ui, Vec2, ViewportCommand}; -use egui_baseview::{ - EguiWindow, GraphicsConfig, Queue, - baseview::{Size, WindowHandle, WindowOpenOptions, WindowScalePolicy, gl::GlConfig}, -}; -use std::sync::{Arc, atomic::Ordering}; - -const DEFAULT_SIZE_X: f32 = 600.0; -const DEFAULT_SIZE_Y: f32 = 350.0; - -pub struct AtomicVec2 { - x: AtomicF32, - y: AtomicF32, -} - -impl Default for AtomicVec2 { - fn default() -> Self { - Self { - x: AtomicF32::new(DEFAULT_SIZE_X), - y: AtomicF32::new(DEFAULT_SIZE_Y), - } - } -} - -impl AtomicVec2 { - pub fn new() -> Self { - Self::default() - } - - pub fn get_x(&self) -> f32 { - self.x.load(Ordering::SeqCst) - } - - pub fn set_x(&self, x: f32) { - self.x.store(x, Ordering::SeqCst); - } - - pub fn get_y(&self) -> f32 { - self.y.load(Ordering::SeqCst) - } - - pub fn set_y(&self, x: f32) { - self.y.store(x, Ordering::SeqCst); - } - - pub fn as_vec2(&self) -> Vec2 { - Vec2 { - x: self.get_x(), - y: self.get_y(), - } - } -} - -/// The EGUI application state -struct AppState { - /// A handle to the shared params state - params: Arc<RetainParams>, - gui_size: Arc<AtomicVec2>, -} - -impl AppState { - /// Initializes a new [`AppState`] from the given shared params state handle - pub fn new(params: &Arc<RetainParams>, gui_size: &Arc<AtomicVec2>) -> Self { - Self { - params: Arc::clone(params), - gui_size: Arc::clone(gui_size), - } - } -} - -/// GUI state that can be accessed directly by the main thread -pub struct RetainPluginGui { - /// The handle to the baseview plugin window. - handle: WindowHandle, - /// The handle to the EGUI context - egui_context: Context, -} - -impl RetainPluginGui { - /// Creates a new GUI window, and embeds it into the given `parent`. - pub fn new(parent: Window<'_>, plugin_state: &RetainPluginShared) -> Self { - let settings = WindowOpenOptions { - title: "Retain".to_string(), - size: Size::new( - plugin_state.gui_size.get_x().into(), - plugin_state.gui_size.get_y().into(), - ), - scale: WindowScalePolicy::SystemScaleFactor, - gl_config: Some(GlConfig::default()), - }; - - let (tx, rx) = std::sync::mpsc::channel(); - - let handle = EguiWindow::open_parented( - &parent, - settings, - GraphicsConfig::default(), - AppState::new(&plugin_state.params, &plugin_state.gui_size), - move |ctx: &Context, _queue: &mut Queue, _state: &mut AppState| { - tx.send(ctx.clone()).unwrap(); - }, - |ui: &mut Ui, _queue: &mut Queue, state: &mut AppState| { - ui.send_viewport_cmd(ViewportCommand::InnerSize(state.gui_size.as_vec2())); - - CentralPanel::default().show_inside(ui, |ui| { - ui.heading("Retain"); - - ui.horizontal(|ui| { - ui.with_layout(Layout::top_down(Align::LEFT), |ui| { - let mut order = state.params.get_order(); - let window_size = state.params.get_window_size().inner(); - - let slider = ui.add( - Slider::new(&mut order, 0..=window_size) - .text("Order") - .logarithmic(true), - ); - - if slider.changed() { - state.params.set_order(order); - } - - let mut window_size = state.params.get_window_size(); - - 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() - { - state.params.set_window_size(inner.into()); - } - } - }); - }); - - ui.with_layout(Layout::top_down(Align::RIGHT), |ui| { - let mut complement = state.params.get_complement(); - - if ui.checkbox(&mut complement, "Complement").changed() { - state.params.set_complement(complement); - } - - let window_type = state.params.get_window_type(); - 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() - { - state.params.set_window_type(byte.into()); - } - } - }); - }); - }); - }); - }, - ); - - let egui_context = rx.recv().unwrap(); - - Self { - handle, - egui_context, - } - } - - /// Requests the UI to repaint itself, e.g. in response to events or parameter changes - pub fn request_repaint(&self) { - self.egui_context.request_repaint(); - } -} - -impl Drop for RetainPluginGui { - fn drop(&mut self) { - self.handle.close(); - } -} - -impl PluginGuiImpl for RetainPluginMainThread<'_> { - fn is_api_supported(&mut self, configuration: GuiConfiguration) -> bool { - configuration.api_type - == GuiApiType::default_for_current_platform().expect("Unsupported platform") - && !configuration.is_floating - } - - fn get_preferred_api(&mut self) -> Option<GuiConfiguration<'_>> { - Some(GuiConfiguration { - api_type: GuiApiType::default_for_current_platform().expect("Unsupported platform"), - is_floating: false, - }) - } - - fn create(&mut self, configuration: GuiConfiguration) -> Result<(), PluginError> { - if configuration.is_floating { - return Err(PluginError::Message( - "Invalid GUI configuration: this plugin does not support floating mode", - )); - } - - let supported_type = - GuiApiType::default_for_current_platform().expect("Unsupported platform"); - - if configuration.api_type != supported_type { - return Err(PluginError::Message( - "Invalid GUI configuration: unsupported API type", - )); - } - - Ok(()) - } - - fn destroy(&mut self) { - let _ = self.gui.take(); - } - - fn set_scale(&mut self, _scale: f64) -> Result<(), PluginError> { - Ok(()) - } - - fn get_size(&mut self) -> Option<GuiSize> { - Some(GuiSize { - width: self.shared.gui_size.get_x() as u32, - height: self.shared.gui_size.get_y() as u32, - }) - } - - fn can_resize(&mut self) -> bool { - true - } - - fn set_size(&mut self, size: GuiSize) -> Result<(), PluginError> { - self.shared.gui_size.set_x(size.width as f32); - self.shared.gui_size.set_y(size.height as f32); - - if let Some(gui) = &self.gui { - gui.request_repaint(); - } - - Ok(()) - } - - fn set_parent(&mut self, window: Window) -> Result<(), PluginError> { - self.gui = Some(RetainPluginGui::new(window, self.shared)); - - Ok(()) - } - - fn set_transient(&mut self, _window: Window) -> Result<(), PluginError> { - Ok(()) - } - - fn show(&mut self) -> Result<(), PluginError> { - if let Some(gui) = &self.gui { - gui.request_repaint(); - } - - Ok(()) - } - - fn hide(&mut self) -> Result<(), PluginError> { - if let Some(gui) = &self.gui { - gui.request_repaint(); - } - - Ok(()) - } -} @@ -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<Self>, - _shared: Option<&RetainPluginShared>, - ) { - builder - .register::<PluginAudioPorts>() - .register::<PluginParams>() - .register::<PluginState>() - .register::<PluginLatency>() - .register::<PluginGui>(); - } +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<RetainParams>, + msg_channel: AudioMsgChannel, + initial_gui_state: Option<GuiState>, + 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<Self::Shared<'_>, 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<Self::MainThread<'a>, 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<RetainParams>, - gui_size: Arc<AtomicVec2>, - host: HostSharedHandle<'a>, +#[derive(Params)] +pub struct RetainParams { + /// This allows the resizing of the plugin to be restored + #[persist = "editor-state"] + editor_state: Arc<EguiState>, + + /// 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<WindowType>, + + /// 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<GuiToAudioMsg>, +} -/// 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<RetainPluginGui>, +pub struct AudioMsgChannel { + from_gui_rx: rtrb::Consumer<GuiToAudioMsg>, } -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<Self>, + ) -> bool { + context.set_latency_samples(self.params.window_size.value().cast_unsigned()); + + true + } + + fn editor(&mut self, _async_executor: AsyncExecutor<Self>) -> Option<Box<dyn Editor>> { + 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::<WindowSize>::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<Self>, + ) -> 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<dyn Params> { + self.params.clone() } } -clack_export_entry!(SinglePluginEntry<RetainPlugin>); +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); diff --git a/src/params.rs b/src/params.rs deleted file mode 100644 index dc0b162..0000000 --- a/src/params.rs +++ /dev/null @@ -1,315 +0,0 @@ -use crate::{RetainPluginMainThread, window_size::WindowSize, window_type::WindowType}; -use clack_extensions::{ - params::{ - ParamDisplayWriter, ParamInfo, ParamInfoFlags, ParamInfoWriter, PluginMainThreadParams, - }, - state::PluginStateImpl, -}; -use clack_plugin::{ - events::spaces::CoreEventSpace, - prelude::*, - stream::{InputStream, OutputStream}, - utils::Cookie, -}; -use serde::{Deserialize, Serialize}; -use std::{ - ffi::CStr, - fmt::Write as _, - io::{Read, Write as _}, - sync::atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering}, -}; - -/// The default values of the parameters. -const DEFAULT_ORDER: usize = 1; -pub const DEFAULT_WINDOW_SIZE: WindowSize = WindowSize::Size4096; -pub const DEFAULT_WINDOW_TYPE: WindowType = WindowType::Hann; -const DEFAULT_COMPLEMENT: bool = false; - -/// A struct that manages the parameters for the plugin. -pub struct RetainParams { - /// The current value of the order parameter. - order: AtomicUsize, - /// Window size of FFT - window_size: AtomicUsize, - /// Type of window function of FFT - window_type: AtomicU8, - /// Whether it retains or removes the highest magnitudes - complement: AtomicBool, -} - -impl RetainParams { - /// The unique identifier for the order parameter. - pub const PARAM_ORDER_ID: ClapId = ClapId::new(1); - pub const PARAM_WINDOW_SIZE_ID: ClapId = ClapId::new(2); - pub const PARAM_WINDOW_TYPE_ID: ClapId = ClapId::new(3); - pub const PARAM_COMPLEMENT_ID: ClapId = ClapId::new(4); - - /// Initializes the shared parameter value. - pub fn new() -> Self { - Self { - order: AtomicUsize::new(DEFAULT_ORDER), - window_size: AtomicUsize::new(DEFAULT_WINDOW_SIZE.inner()), - window_type: AtomicU8::new(DEFAULT_WINDOW_TYPE.as_byte()), - complement: AtomicBool::new(DEFAULT_COMPLEMENT), - } - } - - /// Returns the current order value. - #[inline] - pub fn get_order(&self) -> usize { - self.order.load(Ordering::SeqCst) - } - - /// Sets the current order to `value`. - /// - /// It is clamped to the range `0..=32_768`. - #[inline] - pub fn set_order(&self, value: usize) { - self.order.store(value.clamp(0, 32768), Ordering::SeqCst); - } - - /// Returns the current window size value. - #[inline] - pub fn get_window_size(&self) -> WindowSize { - self.window_size.load(Ordering::SeqCst).into() - } - - /// Sets the current local window size to `value`. - #[inline] - pub fn set_window_size(&self, value: WindowSize) { - self.window_size.store(value.inner(), Ordering::SeqCst); - } - - /// Returns the current local window type. - #[inline] - pub fn get_window_type(&self) -> WindowType { - self.window_type.load(Ordering::SeqCst).into() - } - - /// Sets the current local window type from `bits`. - #[inline] - pub fn set_window_type(&self, window_type: WindowType) { - self.window_type - .store(window_type.as_byte(), Ordering::SeqCst); - } - - /// Returns the current local complement. - #[inline] - pub fn get_complement(&self) -> bool { - self.complement.load(Ordering::SeqCst) - } - - /// Sets the current local complement to `value`. - #[inline] - pub fn set_complement(&self, value: bool) { - self.complement.store(value, Ordering::SeqCst); - } - - /// Handles incoming events. - /// - /// If the given event is a matching parameter change event, the order parameter will be - /// updated accordingly. - pub fn handle_event(&self, event: &UnknownEvent) { - if let Some(CoreEventSpace::ParamValue(event)) = event.as_core_event() - && event.param_id() == RetainParams::PARAM_ORDER_ID - { - self.set_order(event.value() as usize); - } - } -} - -/// Wrapper that helps save plugin parameters using protocol buffers -/// Updating this will change the schema and break old -#[derive(Serialize, Deserialize)] -#[serde(default)] -struct PluginState { - order: usize, - window_size: usize, - window_type: u8, - complement: bool, -} - -impl Default for PluginState { - fn default() -> Self { - Self { - order: DEFAULT_ORDER, - window_size: DEFAULT_WINDOW_SIZE.inner(), - window_type: DEFAULT_WINDOW_TYPE.as_byte(), - complement: DEFAULT_COMPLEMENT, - } - } -} - -impl PluginState { - fn copied(params: &RetainParams) -> Self { - Self { - order: params.get_order(), - window_size: params.get_window_size().inner(), - window_type: params.get_window_type().as_byte(), - complement: params.get_complement(), - } - } - - fn get_order(&self) -> usize { - self.order - } - - fn get_window_size(&self) -> WindowSize { - self.window_size.into() - } - - fn get_window_type(&self) -> WindowType { - self.window_type.into() - } - - fn get_complement(&self) -> bool { - self.complement - } -} - -/// To save and load the plugin parameters -impl PluginStateImpl for RetainPluginMainThread<'_> { - fn save(&mut self, output: &mut OutputStream) -> Result<(), PluginError> { - let state = PluginState::copied(&self.shared.params); - - let data = serde_json::to_vec(&state)?; - - output.write_all(&data)?; - - Ok(()) - } - - fn load(&mut self, input: &mut InputStream) -> Result<(), PluginError> { - let mut data = vec![]; - input.read_to_end(&mut data)?; - - let data = serde_json::from_slice::<PluginState>(&data)?; - - self.shared.params.set_order(data.get_order()); - self.shared.params.set_window_size(data.get_window_size()); - self.shared.params.set_window_type(data.get_window_type()); - self.shared.params.set_complement(data.get_complement()); - - Ok(()) - } -} - -impl PluginMainThreadParams for RetainPluginMainThread<'_> { - fn count(&mut self) -> u32 { - 4 - } - - /// `param_index`: The index of the parameter to query. - /// Must be less than the value returned by `count()`. - fn get_info(&mut self, param_index: u32, info: &mut ParamInfoWriter) { - if param_index >= 4 { - return; - } - - // cleaner than match is - if param_index == 0 { - info.set(&ParamInfo { - id: RetainParams::PARAM_ORDER_ID, - flags: ParamInfoFlags::IS_AUTOMATABLE | ParamInfoFlags::IS_STEPPED, - cookie: Cookie::default(), - name: b"Order", - module: b"", - min_value: 0.0, - max_value: 32768.0, - default_value: DEFAULT_ORDER as f64, - }); - } - - if param_index == 1 { - info.set(&ParamInfo { - id: RetainParams::PARAM_WINDOW_SIZE_ID, - flags: ParamInfoFlags::IS_READONLY, - cookie: Cookie::default(), - name: b"Window Size", - module: b"", - min_value: 256.0, - max_value: 32768.0, - default_value: DEFAULT_WINDOW_SIZE.inner() as f64, - }); - } - - if param_index == 2 { - info.set(&ParamInfo { - id: RetainParams::PARAM_WINDOW_TYPE_ID, - flags: ParamInfoFlags::IS_READONLY, - cookie: Cookie::default(), - name: b"Window Function", - module: b"", - min_value: 0.0, - max_value: 4.0, - default_value: DEFAULT_WINDOW_TYPE.as_byte() as f64, - }); - } - - if param_index == 3 { - info.set(&ParamInfo { - id: RetainParams::PARAM_COMPLEMENT_ID, - flags: ParamInfoFlags::IS_READONLY, - cookie: Cookie::default(), - name: b"Complement", - module: b"", - min_value: 0.0, - max_value: 1.0, - default_value: DEFAULT_COMPLEMENT as u8 as f64, - }); - } - } - - fn get_value(&mut self, param_id: ClapId) -> Option<f64> { - match param_id { - RetainParams::PARAM_ORDER_ID => Some(self.shared.params.get_order() as f64), - RetainParams::PARAM_WINDOW_SIZE_ID => { - Some(self.shared.params.get_window_size().inner() as f64) - } - RetainParams::PARAM_WINDOW_TYPE_ID => { - Some(self.shared.params.get_window_type().as_byte() as f64) - } - RetainParams::PARAM_COMPLEMENT_ID => { - Some(self.shared.params.get_complement() as u8 as f64) - } - _ => None, - } - } - - fn value_to_text( - &mut self, - param_id: ClapId, - value: f64, - writer: &mut ParamDisplayWriter, - ) -> std::fmt::Result { - if param_id == RetainParams::PARAM_ORDER_ID { - write!(writer, "{}", value as u64) - } else { - Err(std::fmt::Error) - } - } - - fn text_to_value(&mut self, param_id: ClapId, text: &CStr) -> Option<f64> { - let text = text.to_str().ok()?; - - if param_id == RetainParams::PARAM_ORDER_ID { - let max = self.shared.params.get_window_size().inner() as f64; - - let order_value = text.parse::<f64>().ok()?; - - Some(order_value.clamp(0.0, max)) - } else { - None - } - } - - fn flush( - &mut self, - input_parameter_changes: &InputEvents, - _output_parameter_changes: &mut OutputEvents, - ) { - for event in input_parameter_changes { - self.shared.params.handle_event(event); - } - } -} diff --git a/src/window_function.rs b/src/window_function.rs index 3d59953..c6aef77 100644 --- a/src/window_function.rs +++ b/src/window_function.rs @@ -1,18 +1,13 @@ use crate::window_size::WindowSize; use std::{collections::VecDeque, f32::consts::PI}; -pub trait WindowFunction: Send { - /// Apply the window in-place +pub trait WindowFunction: Send + Sync { fn apply(&mut self, data: &mut VecDeque<f32>); - /// Reverse the window in-place fn reverse(&mut self, data: &mut [f32]); - /// Yields the amount of samples still required to apply the window fn needed(&self) -> usize; - /// Take the steps necessary to handle a resize fn resize(&mut self, window_size: &WindowSize); } -/// Effectively a chunking function pub struct RectangularWindow { window_size: usize, } @@ -43,8 +38,6 @@ impl WindowFunction for RectangularWindow { } } -/// Hann function -/// Implemented using sqrt Hann pub struct HannWindow { window_size: usize, function: Vec<f32>, @@ -104,15 +97,13 @@ impl WindowFunction for HannWindow { let half_window_size = self.window_size / 2; // overlap add data and save the next overlap (latter half of this buffer) - for i in 0..self.window_size { - self.overlap_add[i] += data[i] * self.function[i]; + for (i, sample) in data.iter().enumerate().take(self.window_size) { + self.overlap_add[i] += sample * self.function[i]; } // set the output samples - // in other functions this will also include normalizing - for i in 0..half_window_size { - data[i] = self.overlap_add[i]; - } + // in other functions this will also include normalizing in a loop + data[..half_window_size].copy_from_slice(&self.overlap_add[..half_window_size]); // shift the saved overlap to become the next overlap self.overlap_add.rotate_left(half_window_size); @@ -132,7 +123,6 @@ impl WindowFunction for HannWindow { } } -/// Hamming function pub struct HammingWindow { window_size: usize, function: Vec<f32>, @@ -157,7 +147,8 @@ impl HammingWindow { }) .collect::<Vec<f32>>(); - // not exact but good enough + // not exact reconstruction but good enough + // I'm thinking of removing the hamming window let normalize = (0..half_window_size) .map(|i| { (function[i] * function[i]) @@ -228,7 +219,6 @@ impl WindowFunction for HammingWindow { } } -/// 4-term Blackman-Harris window function pub struct BlackmanHarrisWindow { window_size: usize, function: Vec<f32>, @@ -337,7 +327,6 @@ impl WindowFunction for BlackmanHarrisWindow { } } -/// 4th power of sine window function pub struct Sine4Window { window_size: usize, function: Vec<f32>, diff --git a/src/window_size.rs b/src/window_size.rs index c84a137..a687b27 100644 --- a/src/window_size.rs +++ b/src/window_size.rs @@ -1,6 +1,3 @@ -/// The max value of the custom window size is `u32::MAX`. -/// The min value of the custom window size is `WindowSize::Size256`. -/// This is because it's the maximum latency one can report to a CLAP host. #[derive(Debug, Clone, PartialEq)] pub enum WindowSize { Size256, @@ -11,6 +8,8 @@ pub enum WindowSize { Size8192, Size16384, Size32768, + // Forced to use IntParam for this because of this "custom" field + // Midi support for time synced window sizes will be added in the future Custom(usize), } diff --git a/src/window_type.rs b/src/window_type.rs index 7fb7d58..0c6a19f 100644 --- a/src/window_type.rs +++ b/src/window_type.rs @@ -5,8 +5,9 @@ use crate::{ }, window_size::WindowSize, }; +use nice_plug::prelude::*; -#[derive(Clone, PartialEq)] +#[derive(Clone, PartialEq, Enum)] pub enum WindowType { Rectangular, Hann, diff --git a/src/windowed_fft.rs b/src/windowed_fft.rs index e99f1c1..b3a6bfa 100644 --- a/src/windowed_fft.rs +++ b/src/windowed_fft.rs @@ -2,7 +2,7 @@ #![allow(clippy::return_self_not_must_use)] use crate::{ - params::DEFAULT_WINDOW_TYPE, window_function::WindowFunction, window_size::WindowSize, + DEFAULT_WINDOW_TYPE, window_function::WindowFunction, window_size::WindowSize, window_type::WindowType, }; use num_complex::Complex; @@ -22,6 +22,8 @@ pub struct WindowedRealFft { } impl WindowedRealFft { + // clippy is very wrong here and passing window_size by ref will result in errors + #[allow(clippy::needless_pass_by_value)] pub fn new(window_size: WindowSize) -> Self { let window_function = DEFAULT_WINDOW_TYPE.new_function(&window_size); let window_size = window_size.inner(); @@ -35,7 +37,8 @@ impl WindowedRealFft { let spectrum = vec![Complex::ZERO; (window_size / 2) + 1]; - // scratches should be the same length for both left and right channels + // scratches should theoretically be the same length for forward and inverse operations + // So I reuse it on to save on allocations let scratch = forward.make_scratch_vec(); Self { @@ -60,6 +63,8 @@ impl WindowedRealFft { self.window_function = window_function; } + // clippy is very wrong here and passing window_size by ref will result in errors + #[allow(clippy::needless_pass_by_value)] pub fn window_size(&mut self, window_size: WindowSize) { if window_size == self.window_size.into() { return; |
