summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorozpv <39195175+ozpv@users.noreply.github.com>2026-06-20 03:11:03 -0500
committerozpv <39195175+ozpv@users.noreply.github.com>2026-06-20 03:11:03 -0500
commit029fc9d3cf4768d94a6e37380e1dcc1e758ae876 (patch)
treee38d6533ac8a08fca1109e6e4ee92fe2e227e274
parent15452d1352dc01e4618a54c8159d8f049225d4fd (diff)
add vst3 support
-rw-r--r--Cargo.toml33
-rw-r--r--src/audio.rs204
-rw-r--r--src/gui.rs295
-rw-r--r--src/lib.rs420
-rw-r--r--src/params.rs315
-rw-r--r--src/window_function.rs25
-rw-r--r--src/window_size.rs5
-rw-r--r--src/window_type.rs3
-rw-r--r--src/windowed_fft.rs9
9 files changed, 365 insertions, 944 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 942d075..aff957a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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(())
- }
-}
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<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(&params.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(
+ &params.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(&params.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(
+ &params.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;