//! CLAP audio effect. All DSP state is owned by the processing thread. mod params; use crossbeam_queue::ArrayQueue; use nih_plug::prelude::*; use nih_plug_egui::{create_egui_editor, egui}; use params::{values, PatinaParams, COUNT}; use patina_core::{AnalyzerFrame, Engine, Monitor}; use std::sync::{ atomic::{AtomicBool, AtomicU64, Ordering}, Arc, }; struct Shared { analyzer: ArrayQueue, delta: AtomicBool, reset: AtomicU64, } pub struct Patina { params: Arc, shared: Arc, engine: Option, publish_count: usize, publish_interval: usize, } impl Default for Patina { fn default() -> Self { Self { params: Arc::new(PatinaParams::default()), shared: Arc::new(Shared { analyzer: ArrayQueue::new(3), delta: AtomicBool::new(false), reset: AtomicU64::new(0), }), engine: None, publish_count: 0, publish_interval: 1600, } } } impl Plugin for Patina { const NAME: &'static str = "Patina"; const VENDOR: &'static str = "Bass Foundry"; const URL: &'static str = ""; 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() }, AudioIOLayout { main_input_channels: NonZeroU32::new(1), main_output_channels: NonZeroU32::new(1), ..AudioIOLayout::const_default() }, ]; const SAMPLE_ACCURATE_AUTOMATION: bool = true; type SysExMessage = (); type BackgroundTask = (); fn params(&self) -> Arc { self.params.clone() } fn editor(&mut self, _: AsyncExecutor) -> Option> { let params = self.params.clone(); let shared = self.shared.clone(); create_egui_editor( params.editor.clone(), patina_ui::State::default(), |ctx, state| { state.delta = false; patina_ui::style(ctx); }, move |ctx, setter, state| { let epoch = shared.reset.load(Ordering::Acquire); if epoch != state_epoch(ctx) { state.delta = false; store_epoch(ctx, epoch); } // Delta is never retained after editor close/reopen or a host reset. shared.delta.store(state.delta, Ordering::Release); while let Some(frame) = shared.analyzer.pop() { state.accept_frame(frame); } let before = params.settings(); let mut edited = before; nih_plug_egui::resizable_window::ResizableWindow::new("resonance-editor") .min_size(egui::vec2(1040., 700.)) .show(ctx, ¶ms.editor, |ui| { patina_ui::show(ui, &mut edited, state) }); let old = values(&before); let new = values(&edited); let floats = params.floats(); for i in 0..COUNT { if new[i] != old[i] { if !state.gestures[i] { setter.begin_set_parameter(floats[i]); state.gestures[i] = true; } setter.set_parameter(floats[i], new[i]); } if !state.gesture_active && state.gestures[i] { setter.end_set_parameter(floats[i]); state.gestures[i] = false; } } if edited.knee != before.knee { let knee = match edited.knee { patina_core::Knee::Soft => params::KneeParam::Soft, patina_core::Knee::Hard => params::KneeParam::Hard, }; setter.begin_set_parameter(¶ms.knee); setter.set_parameter(¶ms.knee, knee); setter.end_set_parameter(¶ms.knee); } if edited.bypass != before.bypass { setter.begin_set_parameter(¶ms.bypass); setter.set_parameter(¶ms.bypass, edited.bypass); setter.end_set_parameter(¶ms.bypass); } shared.delta.store(state.delta, Ordering::Release); }, ) } fn initialize( &mut self, layout: &AudioIOLayout, config: &BufferConfig, context: &mut impl InitContext, ) -> bool { let Ok(mut engine) = Engine::new( config.sample_rate, layout.main_input_channels.map_or(0, |v| v.get() as usize), ) else { return false; }; if engine.set_settings(self.params.settings()).is_err() { return false; } context.set_latency_samples(engine.latency_samples()); self.publish_interval = (config.sample_rate / 30.) as usize; self.engine = Some(engine); self.shared.delta.store(false, Ordering::Release); self.shared.reset.fetch_add(1, Ordering::AcqRel); true } fn reset(&mut self) { if let Some(e) = self.engine.as_mut() { e.reset(); } self.publish_count = 0; self.shared.delta.store(false, Ordering::Release); self.shared.reset.fetch_add(1, Ordering::AcqRel); } fn process( &mut self, buffer: &mut Buffer, _: &mut AuxiliaryBuffers, _: &mut impl ProcessContext, ) -> ProcessStatus { let Some(engine) = self.engine.as_mut() else { return ProcessStatus::Normal; }; let _ = engine.set_settings(self.params.settings()); let delta = self.shared.delta.load(Ordering::Acquire) && self.params.editor.is_open(); for mut sample in buffer.iter_samples() { let x = [ *sample.get_mut(0).unwrap(), if engine.channels() == 2 { *sample.get_mut(1).unwrap() } else { 0. }, ]; let out = engine.process_sample(x, Monitor { delta }); for (c, value) in sample.into_iter().enumerate() { *value = out[c]; } self.publish_count += 1; if self.publish_count >= self.publish_interval { if let Some(frame) = engine.take_analyzer() { self.shared.analyzer.force_push(frame); } self.publish_count = 0; } } // The scheduler must drain its overlap buffers on silent input. ProcessStatus::KeepAlive } } fn state_epoch(ctx: &egui::Context) -> u64 { ctx.data(|d| d.get_temp(egui::Id::new("reset-epoch")).unwrap_or(0)) } fn store_epoch(ctx: &egui::Context, e: u64) { ctx.data_mut(|d| d.insert_temp(egui::Id::new("reset-epoch"), e)); } impl ClapPlugin for Patina { const CLAP_ID: &'static str = "io.narl.bass-foundry.patina"; const CLAP_DESCRIPTION: Option<&'static str> = Some("Dynamic resonance attenuation with explicit tone protection"); const CLAP_MANUAL_URL: Option<&'static str> = None; const CLAP_SUPPORT_URL: Option<&'static str> = None; const CLAP_FEATURES: &'static [ClapFeature] = &[ ClapFeature::AudioEffect, ClapFeature::Compressor, ClapFeature::Stereo, ClapFeature::Mono, ]; } nih_export_clap!(Patina);