diff options
| author | ozpv <39195175+ozpv@users.noreply.github.com> | 2026-05-16 03:06:33 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-05-16 03:06:33 -0500 |
| commit | 31b83a1daf401e41a1c73484c0aec3016b795e3f (patch) | |
| tree | 85c06870b172d11a0602f21ddb2bf3c25c686997 | |
| parent | 7901b970d14c86d00a80347471bf8e071d39b83e (diff) | |
latency
| -rw-r--r-- | Cargo.toml | 6 | ||||
| -rw-r--r-- | src/audio.rs | 29 | ||||
| -rw-r--r-- | src/gui.rs | 4 | ||||
| -rw-r--r-- | src/lib.rs | 49 | ||||
| -rw-r--r-- | src/params.rs | 7 | ||||
| -rw-r--r-- | src/windowed_fft.rs | 111 |
6 files changed, 175 insertions, 31 deletions
@@ -17,6 +17,7 @@ clack-extensions = { path = "../clack/extensions", features = [ "params", "raw-window-handle_05", "state", + "latency", ] } baseview = { git = "https://github.com/RustAudio/baseview.git", rev = "237d323c729f3aa99476ba3efa50129c5e86cad3", version = "0.1.0" } egui-baseview = { git = "https://codeberg.org/BillyDM/egui-baseview.git", version = "0.7.0" } @@ -24,13 +25,14 @@ egui-baseview = { git = "https://codeberg.org/BillyDM/egui-baseview.git", versio # to serialize parameters and save the plugin state prost = "0.14.3" prost-build = "0.14.3" +realfft = { version = "3.5.0", features = ["avx", "neon", "sse"] } [build-dependencies] prost-build = "0.14.3" [profile.release] opt-level = 3 -lto = "fat" +lto = true codegen-units = 1 panic = "abort" -strip = "debuginfo" +strip = true diff --git a/src/audio.rs b/src/audio.rs index 1552292..495cc29 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -4,7 +4,7 @@ use crate::{ RetainPluginMainThread, RetainPluginShared, params::{GestureChange, RetainParamsLocal, RetainParamsShared}, }; -use clack_extensions::{audio_ports::*, params::PluginAudioProcessorParams}; +use clack_extensions::{audio_ports::*, latency::HostLatency, params::PluginAudioProcessorParams}; use clack_plugin::{ events::event_types::{ParamGestureBeginEvent, ParamGestureEndEvent}, prelude::*, @@ -18,20 +18,24 @@ pub struct RetainPluginAudioProcessor<'a> { /// The local state of the parameters params: RetainParamsLocal, /// A reference to the plugin's shared data. - shared: &'a RetainPluginShared, + shared: &'a RetainPluginShared<'a>, /// Our handle to the host host: HostAudioProcessorHandle<'a>, } -impl<'a> PluginAudioProcessor<'a, RetainPluginShared, RetainPluginMainThread<'a>> +impl<'a> PluginAudioProcessor<'a, RetainPluginShared<'a>, RetainPluginMainThread<'a>> for RetainPluginAudioProcessor<'a> { fn activate( host: HostAudioProcessorHandle<'a>, - _main_thread: &mut RetainPluginMainThread, - shared: &'a RetainPluginShared, + main_thread: &mut RetainPluginMainThread, + shared: &'a RetainPluginShared<'a>, _audio_config: PluginAudioConfiguration, ) -> Result<Self, PluginError> { + if let Some(latency) = shared.host.get_extension::<HostLatency>() { + latency.changed(&mut main_thread.host_main_thread); + } + // This is where we would allocate intermediate buffers and such if we needed them. Ok(Self { host, @@ -78,20 +82,21 @@ impl<'a> PluginAudioProcessor<'a, RetainPluginShared, RetainPluginMainThread<'a> // Now let's process the audio, while splitting the processing in batches between each // sample-accurate event. - for event_batch in events.input.batch() { // Process all param events in this batch for event in event_batch.events() { self.params.handle_event(event); } - // Get the order value after all parameter changes have been handled. - let order = self.params.get_order(); + // Get the parameters after all changes have been handled. + let _order = self.params.get_order(); + let _window_size = self.params.get_window_size(); - for buf in channel_buffers.iter_mut().flatten() { - for sample in buf.iter_mut() { - *sample = order as f32; - } + // process in place here + if let [Some(left), Some(right)] = &mut channel_buffers { + for _ in left.iter_mut() {} + + for _ in right.iter_mut() {} } } @@ -42,7 +42,7 @@ pub struct RetainPluginGui { impl RetainPluginGui { /// Creates a new GUI window, and embeds it into the given `parent`. - pub fn new(parent: Window<'_>, state: &RetainPluginShared) -> Self { + pub fn new(parent: Window<'_>, plugin_state: &RetainPluginShared) -> Self { let settings = WindowOpenOptions { title: "Retain".to_string(), size: Size::new(1000.0, 500.0), @@ -56,7 +56,7 @@ impl RetainPluginGui { &parent, settings, GraphicsConfig::default(), - AppState::new(&state.params), + AppState::new(&plugin_state.params), move |egui_ctx: &Context, _queue: &mut Queue, _state: &mut AppState| { tx.send(egui_ctx.clone()).unwrap(); }, @@ -3,7 +3,13 @@ use crate::{ gui::RetainPluginGui, params::{RetainParamsLocal, RetainParamsShared}, }; -use clack_extensions::{audio_ports::*, gui::PluginGui, params::*, state::PluginState}; +use clack_extensions::{ + audio_ports::*, + gui::PluginGui, + latency::{PluginLatency, PluginLatencyImpl}, + params::*, + state::PluginState, +}; use clack_plugin::prelude::*; use std::sync::Arc; @@ -19,7 +25,7 @@ pub struct RetainPlugin; impl Plugin for RetainPlugin { type AudioProcessor<'a> = RetainPluginAudioProcessor<'a>; - type Shared<'a> = RetainPluginShared; + type Shared<'a> = RetainPluginShared<'a>; type MainThread<'a> = RetainPluginMainThread<'a>; fn declare_extensions( @@ -30,6 +36,7 @@ impl Plugin for RetainPlugin { .register::<PluginAudioPorts>() .register::<PluginParams>() .register::<PluginState>() + .register::<PluginLatency>() .register::<PluginGui>(); } } @@ -38,47 +45,59 @@ impl DefaultPluginFactory for RetainPlugin { fn get_descriptor() -> PluginDescriptor { use clack_plugin::plugin::features::*; - PluginDescriptor::new("org.haemolacriaa.retain", "Retain") + PluginDescriptor::new("com.haemolacriaa.retain", "Retain") + .with_description("Retains only the nth largest magnitude frequencies in the signal") .with_features([AUDIO_EFFECT, STEREO]) } - fn new_shared(_host: HostSharedHandle<'_>) -> Result<Self::Shared<'_>, PluginError> { - Ok(RetainPluginShared { - params: Arc::new(RetainParamsShared::new()), - }) + fn new_shared(host: HostSharedHandle<'_>) -> Result<Self::Shared<'_>, PluginError> { + Ok(RetainPluginShared::new(host)) } fn new_main_thread<'a>( - _host: HostMainThreadHandle<'a>, + host: HostMainThreadHandle<'a>, shared: &'a Self::Shared<'a>, ) -> Result<Self::MainThread<'a>, PluginError> { Ok(Self::MainThread { shared, params: RetainParamsLocal::new(&shared.params), + host_main_thread: host, gui: None, }) } } /// The plugin data that gets shared between the Main Thread and the Audio Thread. -pub struct RetainPluginShared { +pub struct RetainPluginShared<'a> { /// The plugin's parameter values. params: Arc<RetainParamsShared>, + /// A handle to the host + host: HostSharedHandle<'a>, } -impl PluginShared<'_> for RetainPluginShared {} +impl<'a> RetainPluginShared<'a> { + fn new(host: HostSharedHandle<'a>) -> Self { + RetainPluginShared { + params: Arc::new(RetainParamsShared::new()), + host: host, + } + } +} + +impl<'a> PluginShared<'a> for RetainPluginShared<'a> {} /// The data that belongs to the main thread of our plugin. pub struct RetainPluginMainThread<'a> { /// The local state of the parameters params: RetainParamsLocal, /// A reference to the plugin's shared data. - shared: &'a RetainPluginShared, + shared: &'a RetainPluginShared<'a>, + host_main_thread: HostMainThreadHandle<'a>, /// The plugin's GUI state and context gui: Option<RetainPluginGui>, } -impl<'a> PluginMainThread<'a, RetainPluginShared> for RetainPluginMainThread<'a> { +impl<'a> PluginMainThread<'a, RetainPluginShared<'a>> for RetainPluginMainThread<'a> { fn on_main_thread(&mut self) { if let Some(gui) = &self.gui { gui.request_repaint(); @@ -86,4 +105,10 @@ impl<'a> PluginMainThread<'a, RetainPluginShared> for RetainPluginMainThread<'a> } } +impl PluginLatencyImpl for RetainPluginMainThread<'_> { + fn get(&mut self) -> u32 { + self.params.get_window_size() as u32 + } +} + clack_export_entry!(SinglePluginEntry<RetainPlugin>); diff --git a/src/params.rs b/src/params.rs index b7e6676..00782ba 100644 --- a/src/params.rs +++ b/src/params.rs @@ -218,11 +218,12 @@ impl RetainParamsLocal { } } +/// To save the plugin state (parameter values) using protocol buffers #[derive(Message)] struct PluginState { - #[prost(uint64, tag = "0")] - order: u64, #[prost(uint64, tag = "1")] + order: u64, + #[prost(uint64, tag = "2")] window_size: u64, } @@ -261,7 +262,7 @@ impl PluginStateImpl for RetainPluginMainThread<'_> { fn load(&mut self, input: &mut InputStream) -> Result<(), PluginError> { let mut data = vec![]; - input.read_exact(&mut data)?; + input.read_to_end(&mut data)?; let data = PluginState::decode(&data[..])?; diff --git a/src/windowed_fft.rs b/src/windowed_fft.rs new file mode 100644 index 0000000..161d3f0 --- /dev/null +++ b/src/windowed_fft.rs @@ -0,0 +1,111 @@ +#![allow(clippy::must_use_candidate)] +#![allow(clippy::return_self_not_must_use)] + +use crate::window::{ + BlackmanHarrisWindow, HammingWindow, HannWindow, RectangularWindow, WindowFunction, +}; +use num_complex::Complex; +use realfft::{ComplexToReal, RealFftPlanner, RealToComplex}; +use std::sync::Arc; + +pub struct WindowedRealFft { + fft_size: usize, + planner: RealFftPlanner<f32>, + forward: Arc<dyn RealToComplex<f32>>, + inverse: Arc<dyn ComplexToReal<f32>>, + window_function: Box<dyn WindowFunction>, + original_length: usize, +} + +impl WindowedRealFft { + pub fn new(fft_size: usize) -> Self { + let mut planner = RealFftPlanner::new(); + let forward = planner.plan_fft_forward(fft_size); + let inverse = planner.plan_fft_inverse(fft_size); + + let window_function = Box::new(HannWindow::new(fft_size)); + + Self { + fft_size, + planner, + forward, + inverse, + window_function, + original_length: 0, + } + } + + pub fn fft_size(mut self, value: usize) -> Self { + self.fft_size = value; + + let forward = self.planner.plan_fft_forward(value); + let inverse = self.planner.plan_fft_inverse(value); + + self.forward = forward; + self.inverse = inverse; + + self + } + + pub fn original_length(&mut self, value: usize) { + self.original_length = value; + } + + pub fn forward(&mut self, data: impl Into<Vec<f32>>) -> Vec<Vec<Complex<f32>>> { + let mut data = data.into(); + + let Some(new_length) = data.len().checked_next_multiple_of(self.fft_size) else { + return vec![]; + }; + + self.original_length = data.len(); + + data.resize(new_length, 0.0); + + self.window_function + .apply(data) + .into_par_iter() + .map(|mut chunk| { + let mut output = self.forward.make_output_vec(); + + self.forward.process(&mut chunk, &mut output).unwrap(); + + output + }) + .collect::<Vec<Vec<Complex<f32>>>>() + } + + pub fn inverse(&mut self, data: Vec<Vec<Complex<f32>>>) -> Vec<f32> { + let data = data + .into_par_iter() + .map(|mut chunk| { + let mut real_output = self.inverse.make_output_vec(); + + self.inverse.process(&mut chunk, &mut real_output).unwrap(); + + let chunk_size_f32 = self.fft_size as f32; + + real_output + .into_iter() + .map(|sample| sample / chunk_size_f32) + .collect::<Vec<f32>>() + }) + .collect::<Vec<Vec<f32>>>(); + + let mut output = self.window_function.reverse(data); + + output.truncate(self.original_length); + + output + } + + pub fn i32_to_f32(item: Vec<i32>) -> Vec<f32> { + item.into_iter().map(f32::from).collect::<Vec<f32>>() + } + + pub fn f32_to_i32(item: Vec<f32>) -> Vec<i32> { + item.into_iter() + .map(|value| value as i32) + .collect::<Vec<i32>>() + } +} |
