summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.toml36
-rw-r--r--src/audio.rs163
-rw-r--r--src/gui.rs203
-rw-r--r--src/lib.rs89
-rw-r--r--src/params.rs352
-rw-r--r--src/window_size.rs77
6 files changed, 920 insertions, 0 deletions
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..ccd8968
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,36 @@
+[package]
+name = "retain"
+version = "0.1.0"
+edition = "2024"
+
+[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",
+] }
+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" }
+
+# to serialize parameters and save the plugin state
+prost = "0.14.3"
+prost-build = "0.14.3"
+
+[build-dependencies]
+prost-build = "0.14.3"
+
+[profile.release]
+opt-level = 3
+lto = "fat"
+codegen-units = 1
+panic = "abort"
+strip = "debuginfo"
diff --git a/src/audio.rs b/src/audio.rs
new file mode 100644
index 0000000..1552292
--- /dev/null
+++ b/src/audio.rs
@@ -0,0 +1,163 @@
+//! Contains all types and implementations related to audio processing and the audio thread.
+
+use crate::{
+ RetainPluginMainThread, RetainPluginShared,
+ params::{GestureChange, RetainParamsLocal, RetainParamsShared},
+};
+use clack_extensions::{audio_ports::*, params::PluginAudioProcessorParams};
+use clack_plugin::{
+ events::event_types::{ParamGestureBeginEvent, ParamGestureEndEvent},
+ prelude::*,
+};
+
+/// 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: RetainParamsLocal,
+ /// A reference to the plugin's shared data.
+ shared: &'a RetainPluginShared,
+ /// Our handle to the host
+ host: HostAudioProcessorHandle<'a>,
+}
+
+impl<'a> PluginAudioProcessor<'a, RetainPluginShared, RetainPluginMainThread<'a>>
+ for RetainPluginAudioProcessor<'a>
+{
+ fn activate(
+ host: HostAudioProcessorHandle<'a>,
+ _main_thread: &mut RetainPluginMainThread,
+ shared: &'a RetainPluginShared,
+ _audio_config: PluginAudioConfiguration,
+ ) -> Result<Self, PluginError> {
+ // This is where we would allocate intermediate buffers and such if we needed them.
+ Ok(Self {
+ host,
+ shared,
+ params: RetainParamsLocal::new(&shared.params),
+ })
+ }
+
+ fn process(
+ &mut self,
+ _process: Process,
+ mut audio: Audio,
+ events: Events,
+ ) -> Result<ProcessStatus, PluginError> {
+ // First, we have to make a few sanity checks.
+ // We want 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"))?;
+
+ let mut channel_buffers = [None, None];
+
+ // Extract the buffer slices that we need, while making sure they are paired correctly and
+ // check for either in-place or separate buffers.
+ for (pair, buf) in output_channels.iter_mut().zip(&mut channel_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)
+ }
+ }
+ }
+
+ // Receive any param updates from the main thread and/or the GUI.
+ let has_ui_param_updates = self.params.fetch_updates(&self.shared.params);
+
+ // 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();
+
+ for buf in channel_buffers.iter_mut().flatten() {
+ for sample in buf.iter_mut() {
+ *sample = order as f32;
+ }
+ }
+ }
+
+ // Publish any parameter changes we may have received back to the GUI.
+ if self.params.push_updates(&self.shared.params) {
+ // Request the on-main-thread callback, which we use to refresh the UI if it is open
+ self.host.request_callback();
+ }
+
+ // Fetch the latest gesture status
+ let current_gesture = self
+ .params
+ .fetch_gesture(&self.shared.params, has_ui_param_updates);
+
+ // Send a Gesture Begin event, if we need to do so
+ if let Some(GestureChange::Begin | GestureChange::Both) = current_gesture {
+ let _ = events.output.try_push(ParamGestureBeginEvent::new(
+ 0,
+ RetainParamsShared::PARAM_ORDER_ID,
+ ));
+ }
+
+ // If the UI sent us param updates, send them to the Host
+ if has_ui_param_updates {
+ self.params.send_param_events(events.output);
+ }
+
+ // Send a Gesture End event, if we need to do so
+ if let Some(GestureChange::End | GestureChange::Both) = current_gesture {
+ let _ = events.output.try_push(ParamGestureEndEvent::new(
+ audio.frames_count(),
+ RetainParamsShared::PARAM_ORDER_ID,
+ ));
+ }
+
+ 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
new file mode 100644
index 0000000..8ab11b4
--- /dev/null
+++ b/src/gui.rs
@@ -0,0 +1,203 @@
+//! Contains all types and implementations related to the plugin's GUI
+
+use crate::{
+ RetainPluginMainThread, RetainPluginShared,
+ params::{RetainParamsLocal, RetainParamsShared},
+ window_size::WindowSize,
+};
+use baseview::{Size, WindowHandle, WindowOpenOptions, WindowScalePolicy, gl::GlConfig};
+use clack_extensions::gui::*;
+use clack_plugin::prelude::*;
+use egui_baseview::{
+ EguiWindow, GraphicsConfig, Queue,
+ egui::{self, ComboBox, Context, Slider},
+};
+use std::sync::Arc;
+
+/// The EGUI application state
+struct AppState {
+ /// A handle to the shared params state
+ shared_params: Arc<RetainParamsShared>,
+ /// The local state of the parameters
+ local_params: RetainParamsLocal,
+}
+
+impl AppState {
+ /// Initializes a new [`AppState`] from the given shared params state handle
+ pub fn new(shared_params: &Arc<RetainParamsShared>) -> Self {
+ Self {
+ local_params: RetainParamsLocal::new(shared_params),
+ shared_params: Arc::clone(shared_params),
+ }
+ }
+}
+
+/// 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<'_>, state: &RetainPluginShared) -> Self {
+ let settings = WindowOpenOptions {
+ title: "Retain".to_string(),
+ size: Size::new(1000.0, 500.0),
+ 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(&state.params),
+ move |egui_ctx: &Context, _queue: &mut Queue, _state: &mut AppState| {
+ tx.send(egui_ctx.clone()).unwrap();
+ },
+ |egui_ctx: &Context, _queue: &mut Queue, state: &mut AppState| {
+ state.local_params.fetch_updates(&state.shared_params);
+
+ egui::CentralPanel::default().show(egui_ctx, |ui| {
+ ui.heading("Retain");
+ let mut order = state.local_params.get_order();
+
+ let slider = ui.add(
+ Slider::new(&mut order, 0..=100_000)
+ .text("Order")
+ .logarithmic(true),
+ );
+
+ if slider.changed() {
+ state.local_params.set_order(order);
+ state.local_params.push_updates(&state.shared_params);
+ }
+
+ state.local_params.has_gesture = slider.is_pointer_button_down_on();
+ state.local_params.push_gesture(&state.shared_params);
+
+ let mut window_size = WindowSize::from(state.local_params.get_window_size());
+
+ ComboBox::from_label("Window Size")
+ .selected_text(window_size.as_str())
+ .show_ui(ui, |ui| {
+ for size in WindowSize::into_iter() {
+ let display = size.as_str();
+
+ if ui
+ .selectable_value(&mut window_size, size, display)
+ .changed()
+ {
+ let u = window_size.value();
+
+ state.local_params.set_window_size(u);
+ state.local_params.push_updates(&state.shared_params);
+ }
+ }
+ });
+ });
+ },
+ );
+
+ 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: 1000,
+ height: 500,
+ })
+ }
+
+ fn set_size(&mut self, _size: GuiSize) -> Result<(), PluginError> {
+ 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
new file mode 100644
index 0000000..82c1957
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,89 @@
+use crate::{
+ audio::RetainPluginAudioProcessor,
+ gui::RetainPluginGui,
+ params::{RetainParamsLocal, RetainParamsShared},
+};
+use clack_extensions::{audio_ports::*, gui::PluginGui, params::*, state::PluginState};
+use clack_plugin::prelude::*;
+use std::sync::Arc;
+
+mod audio;
+mod gui;
+mod params;
+mod window_size;
+
+/// 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;
+ type MainThread<'a> = RetainPluginMainThread<'a>;
+
+ fn declare_extensions(
+ builder: &mut PluginExtensions<Self>,
+ _shared: Option<&RetainPluginShared>,
+ ) {
+ builder
+ .register::<PluginAudioPorts>()
+ .register::<PluginParams>()
+ .register::<PluginState>()
+ .register::<PluginGui>();
+ }
+}
+
+impl DefaultPluginFactory for RetainPlugin {
+ fn get_descriptor() -> PluginDescriptor {
+ use clack_plugin::plugin::features::*;
+
+ PluginDescriptor::new("org.haemolacriaa.retain", "Retain")
+ .with_features([AUDIO_EFFECT, STEREO])
+ }
+
+ fn new_shared(_host: HostSharedHandle<'_>) -> Result<Self::Shared<'_>, PluginError> {
+ Ok(RetainPluginShared {
+ params: Arc::new(RetainParamsShared::new()),
+ })
+ }
+
+ fn new_main_thread<'a>(
+ _host: HostMainThreadHandle<'a>,
+ shared: &'a Self::Shared<'a>,
+ ) -> Result<Self::MainThread<'a>, PluginError> {
+ Ok(Self::MainThread {
+ shared,
+ params: RetainParamsLocal::new(&shared.params),
+ gui: None,
+ })
+ }
+}
+
+/// The plugin data that gets shared between the Main Thread and the Audio Thread.
+pub struct RetainPluginShared {
+ /// The plugin's parameter values.
+ params: Arc<RetainParamsShared>,
+}
+
+impl PluginShared<'_> for RetainPluginShared {}
+
+/// 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,
+ /// The plugin's GUI state and context
+ gui: Option<RetainPluginGui>,
+}
+
+impl<'a> PluginMainThread<'a, RetainPluginShared> for RetainPluginMainThread<'a> {
+ fn on_main_thread(&mut self) {
+ if let Some(gui) = &self.gui {
+ gui.request_repaint();
+ }
+ }
+}
+
+clack_export_entry!(SinglePluginEntry<RetainPlugin>);
diff --git a/src/params.rs b/src/params.rs
new file mode 100644
index 0000000..b7e6676
--- /dev/null
+++ b/src/params.rs
@@ -0,0 +1,352 @@
+//! Contains all types and implementations related to parameter management.
+
+use crate::{RetainPluginMainThread, window_size::WindowSize};
+use clack_extensions::{params::*, state::PluginStateImpl};
+use clack_plugin::{
+ events::{event_types::ParamValueEvent, spaces::CoreEventSpace},
+ prelude::*,
+ stream::{InputStream, OutputStream},
+ utils::Cookie,
+};
+use prost::Message;
+use std::{
+ ffi::CStr,
+ fmt::Write as _,
+ io::{Read, Write as _},
+ sync::atomic::{AtomicBool, AtomicUsize, Ordering},
+};
+
+/// The default value of the order parameter.
+const DEFAULT_ORDER: usize = 1000;
+pub const DEFAULT_WINDOW_SIZE: WindowSize = WindowSize::Size128;
+
+/// A struct that manages the parameters for our plugin.
+///
+/// This struct will be used both by the [`RetainPluginMainThread`] (which the host will use
+/// to query the value of our parameters), and by the [`RetainPluginAudioProcessor`], which will
+/// actually modulate the audio samples.
+pub struct RetainParamsShared {
+ /// The current value of the order parameter.
+ order: AtomicUsize,
+ /// Window size of FFT
+ window_size: AtomicUsize,
+ /// Whether a gesture is currently active or not on the parameters
+ has_gesture: AtomicBool,
+}
+
+impl RetainParamsShared {
+ /// 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);
+
+ /// Initializes the shared parameter value.
+ pub fn new() -> Self {
+ Self {
+ order: AtomicUsize::new(DEFAULT_ORDER),
+ window_size: AtomicUsize::new(DEFAULT_WINDOW_SIZE.value()),
+ has_gesture: AtomicBool::new(false),
+ }
+ }
+}
+
+/// A param gesture change type
+#[derive(Copy, Clone, Eq, PartialEq)]
+pub enum GestureChange {
+ /// User started changing a parameter
+ Begin,
+ /// User finished changing a parameter
+ End,
+ /// User both started and finished changing a parameter during the current block
+ Both,
+}
+
+/// The local-side of parameter state.
+///
+/// This state is local to the current thread (whether it is the main-thread, the UI or the audio
+/// thread), it is not shared directly with the others.
+///
+/// This allows us to both check for differences, and to only update parameter state when we want
+/// to.
+pub struct RetainParamsLocal {
+ /// The local value of the order parameter.
+ order: usize,
+ /// The local value of the window size paramter.
+ window_size: usize,
+ /// Whether the user is currently changing the parameter, that we know of
+ pub has_gesture: bool,
+}
+
+impl RetainParamsLocal {
+ /// Initializes a new local state from the current shared state.
+ pub fn new(shared: &RetainParamsShared) -> Self {
+ Self {
+ order: shared.order.load(Ordering::Relaxed),
+ window_size: shared.window_size.load(Ordering::Relaxed),
+ has_gesture: shared.has_gesture.load(Ordering::Relaxed),
+ }
+ }
+
+ /// Returns the current local order value.
+ #[inline]
+ pub fn get_order(&self) -> usize {
+ self.order
+ }
+
+ /// Sets the current local order to `value`.
+ ///
+ /// It is clamped to the range `0..=100_000`.
+ #[inline]
+ pub fn set_order(&mut self, value: usize) {
+ self.order = value.clamp(0, 100_000);
+ }
+
+ /// Returns the current window size value.
+ #[inline]
+ pub fn get_window_size(&self) -> usize {
+ self.window_size
+ }
+
+ /// Sets the current local window size to `value`.
+ #[inline]
+ pub fn set_window_size(&mut self, value: usize) {
+ self.window_size = value;
+ }
+
+ /// Fetch updates from the `shared` state.
+ ///
+ /// If any of the parameters have been updated, this returns `true`.
+ #[inline]
+ pub fn fetch_updates(&mut self, shared: &RetainParamsShared) -> bool {
+ let latest_order = shared.order.load(Ordering::Relaxed);
+ let latest_window_size = shared.window_size.load(Ordering::Relaxed);
+
+ if latest_order != self.order || latest_window_size != self.order {
+ self.order = latest_order;
+ self.window_size = latest_window_size;
+
+ true
+ } else {
+ false
+ }
+ }
+
+ /// Pushes the local parameter values to the `shared` state.
+ ///
+ /// If the values were different and an actual update occurred, this returns `true`.
+ #[inline]
+ pub fn push_updates(&self, shared: &RetainParamsShared) -> bool {
+ let previous_order = shared.order.swap(self.order, Ordering::Relaxed);
+ let previous_window_size = shared.window_size.swap(self.window_size, Ordering::Relaxed);
+
+ previous_order != self.order || previous_window_size != self.window_size
+ }
+
+ /// Pushes the local gesture state to the `shared` state.
+ #[inline]
+ pub fn push_gesture(&self, shared: &RetainParamsShared) {
+ shared
+ .has_gesture
+ .store(self.has_gesture, Ordering::Relaxed);
+ }
+
+ /// Fetches updates to the gesture state.
+ ///
+ /// If a gesture state changed occurred, it is returned.
+ /// If the gesture did not change from the last update, `None` is returned.
+ #[inline]
+ pub fn fetch_gesture(
+ &mut self,
+ shared: &RetainParamsShared,
+ has_ui_param_updates: bool,
+ ) -> Option<GestureChange> {
+ let previous_gesture = self.has_gesture;
+
+ self.has_gesture = shared.has_gesture.load(Ordering::Relaxed);
+
+ if previous_gesture == self.has_gesture {
+ return if has_ui_param_updates && !self.has_gesture {
+ Some(GestureChange::Both)
+ } else {
+ None
+ };
+ }
+
+ if previous_gesture {
+ Some(GestureChange::End)
+ } else {
+ Some(GestureChange::Begin)
+ }
+ }
+
+ /// Handles incoming events.
+ ///
+ /// If the given event is a matching parameter change event, the order parameter will be
+ /// updated accordingly.
+ pub fn handle_event(&mut self, event: &UnknownEvent) {
+ if let Some(CoreEventSpace::ParamValue(event)) = event.as_core_event() {
+ if event.param_id() == RetainParamsShared::PARAM_ORDER_ID {
+ self.set_order(event.value() as usize);
+ }
+
+ if event.param_id() == RetainParamsShared::PARAM_WINDOW_SIZE_ID {
+ self.set_window_size(event.value() as usize);
+ }
+ }
+ }
+
+ /// Sends the value of the parameters to the host, via a [`ParamValueEvent`].
+ pub fn send_param_events(&self, output_events: &mut OutputEvents) {
+ let order_event = ParamValueEvent::new(
+ 0,
+ RetainParamsShared::PARAM_ORDER_ID,
+ Pckn::match_all(),
+ self.order as f64,
+ Cookie::empty(),
+ );
+
+ let _ = output_events.try_push(order_event);
+
+ let window_size_event = ParamValueEvent::new(
+ 0,
+ RetainParamsShared::PARAM_ORDER_ID,
+ Pckn::match_all(),
+ self.window_size as f64,
+ Cookie::empty(),
+ );
+
+ let _ = output_events.try_push(window_size_event);
+ }
+}
+
+#[derive(Message)]
+struct PluginState {
+ #[prost(uint64, tag = "0")]
+ order: u64,
+ #[prost(uint64, tag = "1")]
+ window_size: u64,
+}
+
+impl PluginState {
+ fn copied(local_params: &RetainParamsLocal) -> Self {
+ Self {
+ order: local_params.get_order() as u64,
+ window_size: local_params.get_window_size() as u64,
+ }
+ }
+
+ fn get_order(&self) -> u64 {
+ self.order
+ }
+
+ fn get_window_size(&self) -> u64 {
+ self.window_size
+ }
+}
+
+/// Implementation of the State extension.
+impl PluginStateImpl for RetainPluginMainThread<'_> {
+ fn save(&mut self, output: &mut OutputStream) -> Result<(), PluginError> {
+ self.params.fetch_updates(&self.shared.params);
+
+ let mut data = vec![];
+
+ let state = PluginState::copied(&self.params);
+
+ state.encode(&mut data)?;
+
+ output.write_all(&data)?;
+
+ Ok(())
+ }
+
+ fn load(&mut self, input: &mut InputStream) -> Result<(), PluginError> {
+ let mut data = vec![];
+ input.read_exact(&mut data)?;
+
+ let data = PluginState::decode(&data[..])?;
+
+ self.params.set_order(data.get_order() as usize);
+ self.params.set_window_size(data.get_window_size() as usize);
+
+ self.params.push_updates(&self.shared.params);
+
+ Ok(())
+ }
+}
+
+impl PluginMainThreadParams for RetainPluginMainThread<'_> {
+ fn count(&mut self) -> u32 {
+ 2
+ }
+
+ fn get_info(&mut self, param_index: u32, info: &mut ParamInfoWriter) {
+ if param_index != 0 {
+ return;
+ }
+
+ info.set(&ParamInfo {
+ id: RetainParamsShared::PARAM_ORDER_ID,
+ flags: ParamInfoFlags::IS_AUTOMATABLE,
+ cookie: Cookie::default(),
+ name: b"Order",
+ module: b"",
+ min_value: 0.0,
+ max_value: 100_000.0,
+ default_value: DEFAULT_ORDER as f64,
+ });
+
+ info.set(&ParamInfo {
+ id: RetainParamsShared::PARAM_WINDOW_SIZE_ID,
+ flags: ParamInfoFlags::IS_AUTOMATABLE,
+ cookie: Cookie::default(),
+ name: b"Window Size",
+ module: b"",
+ min_value: 128.0,
+ max_value: usize::MAX as f64,
+ default_value: DEFAULT_WINDOW_SIZE.value() as f64,
+ });
+ }
+
+ fn get_value(&mut self, param_id: ClapId) -> Option<f64> {
+ match param_id {
+ RetainParamsShared::PARAM_ORDER_ID => Some(self.params.get_order() as f64),
+ RetainParamsShared::PARAM_WINDOW_SIZE_ID => Some(self.params.get_window_size() as f64),
+ _ => None,
+ }
+ }
+
+ fn value_to_text(
+ &mut self,
+ param_id: ClapId,
+ value: f64,
+ writer: &mut ParamDisplayWriter,
+ ) -> std::fmt::Result {
+ if param_id == 1 {
+ write!(writer, "{0:.2} %", value * 100.0)
+ } 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 == 1 {
+ let text = text.strip_suffix('%').unwrap_or(text).trim();
+ let percentage: f64 = text.parse().ok()?;
+
+ Some(percentage / 100.0)
+ } else {
+ None
+ }
+ }
+
+ 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/window_size.rs b/src/window_size.rs
new file mode 100644
index 0000000..0c1ef08
--- /dev/null
+++ b/src/window_size.rs
@@ -0,0 +1,77 @@
+#[derive(Debug, PartialEq)]
+pub enum WindowSize {
+ Size128,
+ Size256,
+ Size512,
+ Size1024,
+ Size2048,
+ Size4096,
+ Size8192,
+ Size16384,
+ Size32768,
+ Custom(usize),
+}
+
+impl From<usize> for WindowSize {
+ fn from(item: usize) -> Self {
+ match item {
+ 128 => Self::Size128,
+ 256 => Self::Size256,
+ 512 => Self::Size512,
+ 1024 => Self::Size1024,
+ 2048 => Self::Size2048,
+ 4096 => Self::Size4096,
+ 8192 => Self::Size8192,
+ 16384 => Self::Size16384,
+ 32768 => Self::Size32768,
+ _ => Self::Custom(item),
+ }
+ }
+}
+
+impl WindowSize {
+ pub fn as_str(&self) -> &'static str {
+ match self {
+ Self::Size128 => "128",
+ Self::Size256 => "256",
+ Self::Size512 => "512",
+ Self::Size1024 => "1024",
+ Self::Size2048 => "2048",
+ Self::Size4096 => "4096",
+ Self::Size8192 => "8192",
+ Self::Size16384 => "16384",
+ Self::Size32768 => "32768",
+ Self::Custom(_) => "",
+ }
+ }
+
+ pub fn value(&self) -> usize {
+ match self {
+ Self::Size128 => 128,
+ Self::Size256 => 256,
+ Self::Size512 => 512,
+ Self::Size1024 => 1024,
+ Self::Size2048 => 2048,
+ Self::Size4096 => 4096,
+ Self::Size8192 => 8192,
+ Self::Size16384 => 16384,
+ Self::Size32768 => 32768,
+ Self::Custom(x) => *x,
+ }
+ }
+
+ pub fn into_iter() -> impl Iterator<Item = Self> {
+ [
+ Self::Size128,
+ Self::Size256,
+ Self::Size512,
+ Self::Size1024,
+ Self::Size2048,
+ Self::Size4096,
+ Self::Size8192,
+ Self::Size16384,
+ Self::Size32768,
+ ]
+ .into_iter()
+ }
+}