feat: add Lathe harmonic EQ and Temper tracked distortion
Check and Windows preview / check (anvil) (push) Successful in 2m8s
Check and Windows preview / check (crucible) (push) Successful in 1m32s
Check and Windows preview / check (lathe) (push) Successful in 2m13s
Check and Windows preview / check (patina) (push) Successful in 2m5s
Check and Windows preview / check (rivet) (push) Successful in 1m34s
Check and Windows preview / check (temper) (push) Successful in 2m12s
Check and Windows preview / previews (push) Successful in 1m19s
Check and Windows preview / windows (push) Successful in 2m13s
Independent plugin release / publish (push) Successful in 8s
Independent plugin release / plan (push) Successful in 6s
Independent plugin release / build (push) Successful in 2m57s
Check and Windows preview / check (anvil) (push) Successful in 2m8s
Check and Windows preview / check (crucible) (push) Successful in 1m32s
Check and Windows preview / check (lathe) (push) Successful in 2m13s
Check and Windows preview / check (patina) (push) Successful in 2m5s
Check and Windows preview / check (rivet) (push) Successful in 1m34s
Check and Windows preview / check (temper) (push) Successful in 2m12s
Check and Windows preview / previews (push) Successful in 1m19s
Check and Windows preview / windows (push) Successful in 2m13s
Independent plugin release / publish (push) Successful in 8s
Independent plugin release / plan (push) Successful in 6s
Independent plugin release / build (push) Successful in 2m57s
Lathe places EQ bands on harmonics 1-32 of the played note, so a cut on harmonic 3 stays on harmonic 3 on every note. Temper normalises each note's level before an oversampled shaper and tunes its floor, push and bright filters to the note, so every note distorts the same. The note tracker moves from Anvil into common-dsp, together with a state-variable filter, 4x oversampler and harmonic meter. Anvil's behaviour and editor render are unchanged. docs/ideas.md collects all plugin ideas and their status.
This commit is contained in:
1 parent
f36ff0780c
commit
b3ebe4d952
48 files changed
+9862
-339
No files matched your search
@@ -0,0 +1,183 @@
|
||||
//! 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::TemperParams;
|
||||
use std::sync::Arc;
|
||||
use temper_core::{AnalyzerFrame, Engine};
|
||||
|
||||
struct EditorData {
|
||||
ui: temper_ui::State,
|
||||
gestures: [bool; 9],
|
||||
}
|
||||
pub struct Temper {
|
||||
params: Arc<TemperParams>,
|
||||
analyzer: Arc<ArrayQueue<AnalyzerFrame>>,
|
||||
engine: Option<Engine>,
|
||||
publish_count: usize,
|
||||
publish_interval: usize,
|
||||
}
|
||||
impl Default for Temper {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
params: Arc::new(TemperParams::default()),
|
||||
analyzer: Arc::new(ArrayQueue::new(2)),
|
||||
engine: None,
|
||||
publish_count: 0,
|
||||
publish_interval: 1600,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Plugin for Temper {
|
||||
const NAME: &'static str = "Temper";
|
||||
const VENDOR: &'static str = "Rivet & Patina";
|
||||
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: temper_ui::State::default(),
|
||||
gestures: [false; 9],
|
||||
},
|
||||
|ctx, _| temper_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("temper-editor")
|
||||
.min_size(egui::vec2(
|
||||
temper_ui::WIDTH as f32 - 80.,
|
||||
temper_ui::HEIGHT as f32 - 40.,
|
||||
))
|
||||
.show(ctx, ¶ms.editor, |ui| {
|
||||
temper_ui::show(ui, &mut edited, &mut data.ui)
|
||||
});
|
||||
params.apply(
|
||||
setter,
|
||||
&before,
|
||||
&edited,
|
||||
data.ui.gesture_active,
|
||||
&mut data.gestures,
|
||||
);
|
||||
// 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;
|
||||
};
|
||||
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);
|
||||
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 _ = engine.set_settings(self.params.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, oversampling filters and tails must drain after input stops.
|
||||
ProcessStatus::KeepAlive
|
||||
}
|
||||
}
|
||||
impl ClapPlugin for Temper {
|
||||
const CLAP_ID: &'static str = "io.storyboard.tracked-distortion";
|
||||
const CLAP_DESCRIPTION: Option<&'static str> = Some(
|
||||
"Distortion whose drive and tone filters follow the note, so every note distorts the same",
|
||||
);
|
||||
const CLAP_MANUAL_URL: Option<&'static str> = None;
|
||||
const CLAP_SUPPORT_URL: Option<&'static str> = None;
|
||||
const CLAP_FEATURES: &'static [ClapFeature] = &[
|
||||
ClapFeature::AudioEffect,
|
||||
ClapFeature::Distortion,
|
||||
ClapFeature::Stereo,
|
||||
ClapFeature::Mono,
|
||||
];
|
||||
}
|
||||
nih_export_clap!(Temper);
|
||||
@@ -0,0 +1,207 @@
|
||||
use nih_plug::prelude::*;
|
||||
use nih_plug_egui::EguiState;
|
||||
use std::sync::Arc;
|
||||
use temper_core::{Curve, Settings, Source};
|
||||
|
||||
#[derive(Enum, Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum SourceParam {
|
||||
#[id = "audio"]
|
||||
Audio,
|
||||
#[id = "midi"]
|
||||
#[name = "MIDI"]
|
||||
Midi,
|
||||
}
|
||||
#[derive(Enum, Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum CurveParam {
|
||||
#[id = "soft"]
|
||||
Soft,
|
||||
#[id = "hard"]
|
||||
Hard,
|
||||
#[id = "fold"]
|
||||
Fold,
|
||||
#[id = "tube"]
|
||||
Tube,
|
||||
}
|
||||
fn linear(name: &str, v: f32, min: f32, max: f32, unit: &'static str) -> FloatParam {
|
||||
FloatParam::new(name, v, FloatRange::Linear { min, max }).with_unit(unit)
|
||||
}
|
||||
const FLOATS: usize = 9;
|
||||
|
||||
#[derive(Params)]
|
||||
pub struct TemperParams {
|
||||
#[persist = "editor"]
|
||||
pub editor: Arc<EguiState>,
|
||||
#[id = "source"]
|
||||
pub source: EnumParam<SourceParam>,
|
||||
#[id = "curve"]
|
||||
pub curve: EnumParam<CurveParam>,
|
||||
#[id = "drive"]
|
||||
pub drive: FloatParam,
|
||||
#[id = "bias"]
|
||||
pub bias: FloatParam,
|
||||
#[id = "normalize"]
|
||||
pub normalize: FloatParam,
|
||||
#[id = "floor"]
|
||||
pub floor: FloatParam,
|
||||
#[id = "push"]
|
||||
pub push_gain: FloatParam,
|
||||
#[id = "bright"]
|
||||
pub bright: FloatParam,
|
||||
#[id = "gate"]
|
||||
pub gate: FloatParam,
|
||||
#[id = "mix"]
|
||||
pub mix: FloatParam,
|
||||
#[id = "output"]
|
||||
pub output: FloatParam,
|
||||
#[id = "push-at"]
|
||||
pub push_harmonic: IntParam,
|
||||
#[id = "auto-gain"]
|
||||
pub auto_gain: BoolParam,
|
||||
#[id = "bypass"]
|
||||
pub bypass: BoolParam,
|
||||
}
|
||||
impl Default for TemperParams {
|
||||
fn default() -> Self {
|
||||
let d = Settings::default();
|
||||
Self {
|
||||
editor: EguiState::from_size(temper_ui::WIDTH, temper_ui::HEIGHT),
|
||||
source: EnumParam::new("Pitch source", SourceParam::Audio),
|
||||
curve: EnumParam::new("Curve", CurveParam::Soft),
|
||||
drive: linear("Drive", d.drive, 0., 36., " dB"),
|
||||
bias: linear("Bias", d.bias, 0., 100., " %"),
|
||||
normalize: linear("Normalize", d.normalize, 0., 100., " %"),
|
||||
floor: linear("Floor", d.floor, 0., 2., " ×f0"),
|
||||
push_gain: linear("Push", d.push_gain, -12., 12., " dB"),
|
||||
bright: linear("Bright", d.bright, 0., 64., " ×f0"),
|
||||
gate: linear("Gate", d.gate, -60., -20., " dB"),
|
||||
mix: linear("Mix", d.mix, 0., 100., " %"),
|
||||
output: linear("Output", d.output, -12., 12., " dB"),
|
||||
push_harmonic: IntParam::new(
|
||||
"Push at",
|
||||
d.push_harmonic as i32,
|
||||
IntRange::Linear { min: 1, max: 16 },
|
||||
),
|
||||
auto_gain: BoolParam::new("Auto gain", d.auto_gain),
|
||||
bypass: BoolParam::new("Bypass", false).make_bypass(),
|
||||
}
|
||||
}
|
||||
}
|
||||
fn curve_to(c: CurveParam) -> Curve {
|
||||
match c {
|
||||
CurveParam::Soft => Curve::Soft,
|
||||
CurveParam::Hard => Curve::Hard,
|
||||
CurveParam::Fold => Curve::Fold,
|
||||
CurveParam::Tube => Curve::Tube,
|
||||
}
|
||||
}
|
||||
fn curve_from(c: Curve) -> CurveParam {
|
||||
match c {
|
||||
Curve::Soft => CurveParam::Soft,
|
||||
Curve::Hard => CurveParam::Hard,
|
||||
Curve::Fold => CurveParam::Fold,
|
||||
Curve::Tube => CurveParam::Tube,
|
||||
}
|
||||
}
|
||||
impl TemperParams {
|
||||
pub fn settings(&self) -> Settings {
|
||||
// Floor and Bright are continuous for automation; below their ranges they are off.
|
||||
let floor = self.floor.value();
|
||||
let bright = self.bright.value();
|
||||
Settings {
|
||||
source: match self.source.value() {
|
||||
SourceParam::Audio => Source::Audio,
|
||||
SourceParam::Midi => Source::Midi,
|
||||
},
|
||||
curve: curve_to(self.curve.value()),
|
||||
drive: self.drive.value(),
|
||||
bias: self.bias.value(),
|
||||
normalize: self.normalize.value(),
|
||||
floor: if floor < 0.25 { 0. } else { floor },
|
||||
push_harmonic: self.push_harmonic.value() as u32,
|
||||
push_gain: self.push_gain.value(),
|
||||
bright: if bright < 4. { 0. } else { bright },
|
||||
auto_gain: self.auto_gain.value(),
|
||||
gate: self.gate.value(),
|
||||
mix: self.mix.value(),
|
||||
output: self.output.value(),
|
||||
bypass: self.bypass.value(),
|
||||
}
|
||||
}
|
||||
fn floats(&self) -> [&FloatParam; FLOATS] {
|
||||
[
|
||||
&self.drive,
|
||||
&self.bias,
|
||||
&self.normalize,
|
||||
&self.floor,
|
||||
&self.push_gain,
|
||||
&self.bright,
|
||||
&self.gate,
|
||||
&self.mix,
|
||||
&self.output,
|
||||
]
|
||||
}
|
||||
/// Pushes every changed editor value to the host. Continuous controls hold one gesture
|
||||
/// while the pointer is down; switches are set in a single gesture.
|
||||
pub fn apply(
|
||||
&self,
|
||||
setter: &ParamSetter,
|
||||
old: &Settings,
|
||||
new: &Settings,
|
||||
dragging: bool,
|
||||
open: &mut [bool; FLOATS],
|
||||
) {
|
||||
let values = [
|
||||
(old.drive, new.drive),
|
||||
(old.bias, new.bias),
|
||||
(old.normalize, new.normalize),
|
||||
(old.floor, new.floor),
|
||||
(old.push_gain, new.push_gain),
|
||||
(old.bright, new.bright),
|
||||
(old.gate, new.gate),
|
||||
(old.mix, new.mix),
|
||||
(old.output, new.output),
|
||||
];
|
||||
for (i, (p, (a, b))) in self.floats().into_iter().zip(values).enumerate() {
|
||||
if a != b {
|
||||
if !open[i] {
|
||||
open[i] = true;
|
||||
setter.begin_set_parameter(p);
|
||||
}
|
||||
setter.set_parameter(p, b);
|
||||
}
|
||||
if !dragging && open[i] {
|
||||
setter.end_set_parameter(p);
|
||||
open[i] = false;
|
||||
}
|
||||
}
|
||||
if new.push_harmonic != old.push_harmonic {
|
||||
setter.begin_set_parameter(&self.push_harmonic);
|
||||
setter.set_parameter(&self.push_harmonic, new.push_harmonic as i32);
|
||||
setter.end_set_parameter(&self.push_harmonic);
|
||||
}
|
||||
for (p, a, b) in [
|
||||
(&self.auto_gain, old.auto_gain, new.auto_gain),
|
||||
(&self.bypass, old.bypass, new.bypass),
|
||||
] {
|
||||
if a != b {
|
||||
setter.begin_set_parameter(p);
|
||||
setter.set_parameter(p, b);
|
||||
setter.end_set_parameter(p);
|
||||
}
|
||||
}
|
||||
if new.curve != old.curve {
|
||||
setter.begin_set_parameter(&self.curve);
|
||||
setter.set_parameter(&self.curve, curve_from(new.curve));
|
||||
setter.end_set_parameter(&self.curve);
|
||||
}
|
||||
if new.source != old.source {
|
||||
let v = match new.source {
|
||||
Source::Audio => SourceParam::Audio,
|
||||
Source::Midi => SourceParam::Midi,
|
||||
};
|
||||
setter.begin_set_parameter(&self.source);
|
||||
setter.set_parameter(&self.source, v);
|
||||
setter.end_set_parameter(&self.source);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user