//! CLAP audio effect. All DSP state is owned by the processing thread. mod params; use crossbeam_queue::ArrayQueue; use lathe_core::{AnalyzerFrame, Engine, HARMONICS}; use nih_plug::prelude::*; use nih_plug_egui::{create_egui_editor, egui}; use params::LatheParams; use std::sync::Arc; struct EditorData { ui: lathe_ui::State, gestures: [bool; 8], } pub struct Lathe { params: Arc, analyzer: Arc>, engine: Option, gains: [f32; HARMONICS], publish_count: usize, publish_interval: usize, } impl Default for Lathe { fn default() -> Self { Self { params: Arc::new(LatheParams::default()), analyzer: Arc::new(ArrayQueue::new(2)), engine: None, gains: [0.; HARMONICS], publish_count: 0, publish_interval: 1600, } } } impl Plugin for Lathe { const NAME: &'static str = "Lathe"; 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 MIDI_INPUT: MidiConfig = MidiConfig::Basic; const SAMPLE_ACCURATE_AUTOMATION: bool = false; type SysExMessage = (); type BackgroundTask = (); fn params(&self) -> Arc { self.params.clone() } fn editor(&mut self, _: AsyncExecutor) -> Option> { let params = self.params.clone(); let analyzer = self.analyzer.clone(); create_egui_editor( params.editor.clone(), EditorData { ui: lathe_ui::State::default(), gestures: [false; 8], }, |ctx, _| lathe_ui::style(ctx), move |ctx, setter, data| { while let Some(frame) = analyzer.pop() { data.ui.accept_frame(frame); } let current = params.gains.read().map(|g| *g).unwrap_or([0.; HARMONICS]); let before = params.settings(¤t); let mut edited = before; nih_plug_egui::resizable_window::ResizableWindow::new("lathe-editor") .min_size(egui::vec2( lathe_ui::WIDTH as f32 - 80., lathe_ui::HEIGHT as f32 - 40., )) .show(ctx, ¶ms.editor, |ui| { lathe_ui::show(ui, &mut edited, &mut data.ui) }); params.apply( setter, &before, &edited, data.ui.gesture_active, &mut data.gestures, ); // Harmonic levels follow the audio. ctx.request_repaint_after(std::time::Duration::from_millis(40)); }, ) } 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; }; let settings = self.params.settings(&self.gains); self.gains = settings.gains; if engine.set_settings(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); true } fn reset(&mut self) { if let Some(e) = self.engine.as_mut() { e.reset(); } self.publish_count = 0; } fn process( &mut self, buffer: &mut Buffer, _: &mut AuxiliaryBuffers, context: &mut impl ProcessContext, ) -> ProcessStatus { let Some(engine) = self.engine.as_mut() else { return ProcessStatus::Normal; }; let settings = self.params.settings(&self.gains); self.gains = settings.gains; let _ = engine.set_settings(settings); let mut next = context.next_event(); for (index, mut sample) in buffer.iter_samples().enumerate() { while let Some(event) = next { if event.timing() as usize > index { break; } match event { NoteEvent::NoteOn { note, velocity, .. } if velocity > 0. => { engine.note_on(note) } NoteEvent::NoteOn { note, .. } | NoteEvent::NoteOff { note, .. } => { engine.note_off(note) } NoteEvent::Choke { note, .. } => engine.note_off(note), _ => {} } next = context.next_event(); } let x = [ *sample.get_mut(0).unwrap(), if engine.channels() == 2 { *sample.get_mut(1).unwrap() } else { 0. }, ]; let out = engine.process_sample(x); for (c, value) in sample.into_iter().enumerate() { *value = out[c]; } self.publish_count += 1; if self.publish_count >= self.publish_interval { self.analyzer.force_push(engine.take_analyzer()); self.publish_count = 0; } } // The lookahead delay and filter tails must drain after input stops. ProcessStatus::KeepAlive } } impl ClapPlugin for Lathe { const CLAP_ID: &'static str = "io.storyboard.harmonic-eq"; const CLAP_DESCRIPTION: Option<&'static str> = Some("EQ the harmonics of the played note: a cut on harmonic 3 stays on harmonic 3"); const CLAP_MANUAL_URL: Option<&'static str> = None; const CLAP_SUPPORT_URL: Option<&'static str> = None; const CLAP_FEATURES: &'static [ClapFeature] = &[ ClapFeature::AudioEffect, ClapFeature::Equalizer, ClapFeature::Stereo, ClapFeature::Mono, ]; } nih_export_clap!(Lathe);