Files
bass-foundry/plugins/lathe/plugin/src/lib.rs
T
nvrl 0a0917ac56
Check and Windows preview / check (anvil) (push) Successful in 2m5s
Check and Windows preview / check (crucible) (push) Successful in 2m11s
Check and Windows preview / check (lathe) (push) Successful in 2m22s
Check and Windows preview / check (patina) (push) Successful in 2m50s
Check and Windows preview / check (rivet) (push) Successful in 46s
Check and Windows preview / check (temper) (push) Successful in 41s
Check and Windows preview / check (weld) (push) Successful in 39s
Check and Windows preview / previews (push) Failing after 24s
Check and Windows preview / windows (push) Skipped
chore: rename the suite to Bass Foundry
Set the CLAP vendor of all seven plugins to "Bass Foundry" and name the
combined preview ZIP bass-foundry-windows-x86_64.zip. CLAP IDs and saved
state stay unchanged.

Bring development.md and releases.md up to date with all seven products,
the shared crates, the test types, the steps to add a product and the
published releases. Add texture problems and seven texture plugin ideas
to ideas.md.
2026-11-06 08:01:02 +01:00

190 lines
6.6 KiB
Rust

//! 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<LatheParams>,
analyzer: Arc<ArrayQueue<AnalyzerFrame>>,
engine: Option<Engine>,
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<dyn Params> {
self.params.clone()
}
fn editor(&mut self, _: AsyncExecutor<Self>) -> Option<Box<dyn Editor>> {
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(&current);
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, &params.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<Self>,
) -> 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<Self>,
) -> 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);