summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorozpv <39195175+ozpv@users.noreply.github.com>2026-05-16 03:06:33 -0500
committerGitHub <noreply@github.com>2026-05-16 03:06:33 -0500
commit31b83a1daf401e41a1c73484c0aec3016b795e3f (patch)
tree85c06870b172d11a0602f21ddb2bf3c25c686997
parent7901b970d14c86d00a80347471bf8e071d39b83e (diff)
latency
-rw-r--r--Cargo.toml6
-rw-r--r--src/audio.rs29
-rw-r--r--src/gui.rs4
-rw-r--r--src/lib.rs49
-rw-r--r--src/params.rs7
-rw-r--r--src/windowed_fft.rs111
6 files changed, 175 insertions, 31 deletions
diff --git a/Cargo.toml b/Cargo.toml
index ccd8968..ed301af 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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() {}
}
}
diff --git a/src/gui.rs b/src/gui.rs
index 8ab11b4..310fbef 100644
--- a/src/gui.rs
+++ b/src/gui.rs
@@ -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();
},
diff --git a/src/lib.rs b/src/lib.rs
index 82c1957..9f73aa9 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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>>()
+ }
+}