Upward and downward compression in four bands whose edges are harmonic numbers of the tracked note (default 1, 2-4, 5-16, 17 and up), so the same setting squashes the same harmonics on every note. A fixed-band OTT puts harmonic 2 of F1 and of C2 in different bands. - Three Linkwitz-Riley splits at sqrt(k(k+1)) x f0 with allpass compensation; at Depth 0 the bands sum within 0.05 dB. - Band level is the RMS over one period of the note, so heavy settings do not ripple at f0: a 40 Hz sine at Drive +12 dB, Time 10 % gets no harmonics above -110 dB. - At Depth 100 % on a saw, harmonics 1-12 change by the same amount on F1, C2 and G2 (within 0.2 dB). - Depth, Drive (on the detector only), Upward, Downward, Time, a gain per band; edges Low 1-8, Mid 2-16, High 3-48 with four splits. - Pitch from audio with about 67 ms lookahead, or from MIDI. - Editor: harmonic ruler with draggable edges, level and gain per band. - Catalog, workflows, editor-check, docs, signal chain and site copy.
186 lines
6.4 KiB
Rust
186 lines
6.4 KiB
Rust
//! 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::{PlanishParams, FLOATS};
|
|
use planish_core::{AnalyzerFrame, Engine};
|
|
use std::sync::Arc;
|
|
|
|
struct EditorData {
|
|
ui: planish_ui::State,
|
|
gestures: [bool; FLOATS],
|
|
}
|
|
pub struct Planish {
|
|
params: Arc<PlanishParams>,
|
|
analyzer: Arc<ArrayQueue<AnalyzerFrame>>,
|
|
engine: Option<Engine>,
|
|
publish_count: usize,
|
|
publish_interval: usize,
|
|
}
|
|
impl Default for Planish {
|
|
fn default() -> Self {
|
|
Self {
|
|
params: Arc::new(PlanishParams::default()),
|
|
analyzer: Arc::new(ArrayQueue::new(2)),
|
|
engine: None,
|
|
publish_count: 0,
|
|
publish_interval: 1600,
|
|
}
|
|
}
|
|
}
|
|
impl Plugin for Planish {
|
|
const NAME: &'static str = "Planish";
|
|
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: planish_ui::State::default(),
|
|
gestures: [false; FLOATS],
|
|
},
|
|
|ctx, _| planish_ui::style(ctx),
|
|
move |ctx, setter, data| {
|
|
while let Some(frame) = analyzer.pop() {
|
|
data.ui.accept_frame(frame);
|
|
}
|
|
let before = params.settings();
|
|
let mut edited = before;
|
|
nih_plug_egui::resizable_window::ResizableWindow::new("planish-editor")
|
|
.min_size(egui::vec2(
|
|
planish_ui::WIDTH as f32 - 80.,
|
|
planish_ui::HEIGHT as f32 - 40.,
|
|
))
|
|
.show(ctx, ¶ms.editor, |ui| {
|
|
planish_ui::show(ui, &mut edited, &mut data.ui)
|
|
});
|
|
params.apply(
|
|
setter,
|
|
&before,
|
|
&edited,
|
|
data.ui.gesture_active,
|
|
&mut data.gestures,
|
|
);
|
|
// Band meters 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();
|
|
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();
|
|
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 compressor release must drain after input stops.
|
|
ProcessStatus::KeepAlive
|
|
}
|
|
}
|
|
impl ClapPlugin for Planish {
|
|
const CLAP_ID: &'static str = "io.narl.bass-foundry.planish";
|
|
const CLAP_DESCRIPTION: Option<&'static str> = Some(
|
|
"Upward and downward compression in bands set by harmonic number, the same on every note",
|
|
);
|
|
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!(Planish);
|