Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use baseview::dpi::LogicalSize;use iced_audio::{Gesture, Knob, param::nice_to_iced};use nice_plug::prelude::*;use nice_plug_iced::{ IcedEditor, IcedEditorState, IcedNiceContext, PersistentState, application, create_iced_editor, iced::{ self, Alignment, Element, Font, Length, PollSubNotifier, Theme, font, widget::{Text, button, checkbox, column, container, row, rule, space, text}, },};use std::{f32::consts::TAU, sync::Arc};
const DEFAULT_FONT: Font = Font::with_name("Open Sans");
pub struct Ring6Plugin { editor_state: Arc<IcedEditorState>,
params: Arc<Ring6PluginParams>, notifier: PollSubNotifier,
sample_rate: f32, channel_state: [LadderEffectState; 2],}
#[derive(Params)]struct Ring6PluginParams { #[id = "pre-gain"] pre_gain: FloatParam, #[id = "post-gain"] post_gain: FloatParam, #[id = "effect-gain"] effect_gain: FloatParam,
#[id = "pz-offs"] pz_offs: FloatParam, #[id = "neg-offs"] neg_offs: FloatParam,
#[id = "threshold"] threshold: FloatParam,
#[id = "effect-lp-cutoff"] effect_lp_cutoff: FloatParam, #[id = "post-lp"] post_lp: BoolParam, #[id = "post-lp-cutoff"] post_lp_cutoff: FloatParam,
#[id = "effect-adaa"] effect_adaa: BoolParam,
#[id = "release-ms"] release_ms: FloatParam,
#[id = "effect-balance"] effect_balance: FloatParam,}
fn gain_param(name: &str) -> FloatParam { FloatParam::new( name, util::db_to_gain(0.0), FloatRange::Skewed { min: util::db_to_gain(-30.0), max: util::db_to_gain(12.0), factor: FloatRange::gain_skew_factor(-30.0, 12.0), }, ) .with_smoother(SmoothingStyle::Logarithmic(50.0)) .with_unit(" dB") .with_value_to_string(formatters::v2s_f32_gain_to_db(2)) .with_string_to_value(formatters::s2v_f32_gain_to_db())}
fn offset_param(name: &str, ticks: f32, bound: f32) -> FloatParam { FloatParam::new( name, ticks / bound, FloatRange::Linear { min: 0.0, max: 16.0 / bound, }, ) .with_smoother(SmoothingStyle::Linear(50.0)) // TODO: use formatters here? .with_value_to_string(Arc::new(move |v| format!("{:.2}", v * bound))) .with_string_to_value(Arc::new(move |s| s.parse::<f32>().ok().map(|v| v / bound)))}
fn cutoff_param(name: &str, default: f32, min: f32, max: f32) -> FloatParam { FloatParam::new( name, default, FloatRange::Skewed { min, max, factor: FloatRange::skew_factor(-2.0), }, ) .with_smoother(SmoothingStyle::Logarithmic(50.0)) .with_value_to_string(formatters::v2s_f32_hz_then_khz(2)) .with_string_to_value(formatters::s2v_f32_hz_then_khz())}
impl Default for Ring6PluginParams { fn default() -> Self { Self { pre_gain: gain_param("Pre-Gain"), post_gain: gain_param("Post-Gain"), effect_gain: gain_param("Effect Gain"), pz_offs: offset_param("Positive/0 Offset", 4.0, 256.0), neg_offs: offset_param("Negative Offset", 3.0, 256.0), threshold: offset_param("Effect Threshold", 1.0, 256.0), effect_lp_cutoff: cutoff_param("Effect Low-Pass", 3390.0, 100.0, 20000.0), effect_adaa: BoolParam::new("Anti-Alias", false), post_lp_cutoff: cutoff_param("Post Low-Pass", 20000.0, 100.0, 20000.0), post_lp: BoolParam::new("Post Low-Pass Enabled", false), release_ms: FloatParam::new( "Release", 15.0, FloatRange::Linear { min: 0.0, max: 100.0, }, ) .with_smoother(SmoothingStyle::Linear(50.0)) .with_unit(" ms") .with_value_to_string(formatters::v2s_f32_rounded(2)) .with_string_to_value(Arc::new(|s| { s.strip_suffix(" ms").unwrap_or(s).parse::<f32>().ok() })), effect_balance: FloatParam::new( "Balance", 0.0, FloatRange::Linear { min: -1.0, max: 1.0, }, ) .with_smoother(SmoothingStyle::Linear(50.0)) .with_value_to_string(formatters::v2s_f32_panning()) .with_string_to_value(formatters::s2v_f32_panning()), } }}
#[derive(Debug, Clone, Copy)]enum BoolParamKey { EffectADAA, PostLp,}
#[derive(Debug, Clone, Copy)]enum FloatParamKey { PreGain, PostGain, EffectGain, PzOffs, NegOffs, Threshold, EffectLpCutoff, PostLpCutoff, // TODO: should this actually be customizable? // ReleaseMs, EffectBalance,}
impl Ring6PluginParams { fn get_bool(&self, key: BoolParamKey) -> &BoolParam { match key { BoolParamKey::EffectADAA => &self.effect_adaa, BoolParamKey::PostLp => &self.post_lp, } }
fn get_float(&self, key: FloatParamKey) -> &FloatParam { match key { FloatParamKey::PreGain => &self.pre_gain, FloatParamKey::PostGain => &self.post_gain, FloatParamKey::EffectGain => &self.effect_gain, FloatParamKey::PzOffs => &self.pz_offs, FloatParamKey::NegOffs => &self.neg_offs, FloatParamKey::Threshold => &self.threshold, FloatParamKey::EffectLpCutoff => &self.effect_lp_cutoff, FloatParamKey::PostLpCutoff => &self.post_lp_cutoff, // FloatParamKey::ReleaseMs => &self.release_ms, FloatParamKey::EffectBalance => &self.effect_balance, } }}
impl Default for Ring6Plugin { fn default() -> Self { Self { editor_state: IcedEditorState::from_size(LogicalSize::new(540.0, 360.0), 1.0), params: Arc::new(Ring6PluginParams::default()), notifier: PollSubNotifier::new(), sample_rate: 0.0, channel_state: Default::default(), } }}
#[derive(Debug, Default)]struct LadderEffectState { base_sample: f32, effect_lp: f32, post_lp: f32, pz_release_level: f32,}
#[derive(Debug)]struct LadderEffectParams<'s> { pre_gain: f32, post_gain: f32, effect_gain: f32,
pz_offs: f32, neg_offs: f32,
threshold: f32, effect_adaa: bool, effect_lp_cutoff: f32, post_lp_cutoff: Option<f32>,
pz_release_ms: f32,
sample_rate: f32, state: &'s mut LadderEffectState,}
#[inline(always)]fn ladder_effect(samples: &mut [f32], params: &mut LadderEffectParams<'_>) { let prev_sample = &mut params.state.base_sample; let lp_offs = &mut params.state.effect_lp; let lp_post = &mut params.state.post_lp; let lp_offs_a = 1.0 - (-TAU * params.effect_lp_cutoff / params.sample_rate).exp(); let lp_post_a = params .post_lp_cutoff .map(|cutoff| 1.0 - (-TAU * cutoff / params.sample_rate).exp()); let pz_release_level = &mut params.state.pz_release_level; let pz_release_factor = (-1.0 / (params.pz_release_ms / 1000.0 * params.sample_rate)).exp();
for sample in samples { // Apply a slow release during quiet periods to clean up used headroom. // TODO: should this fall back if threshold==0? if sample.abs() >= params.threshold { *pz_release_level = 1.0; } else if *pz_release_level <= 1e-6 { *pz_release_level = 0.0; } else { *pz_release_level *= pz_release_factor; }
let pz_offs = params.pz_offs * *pz_release_level;
let offs = if params.effect_adaa && (*sample - *prev_sample).abs() > 1e-8 { // ADAA let f = |s: f32| { if s < -params.threshold { -params.neg_offs * s } else { // Use C to make the lines for the negative & positive sides // continuous. let adaa_c = (-params.neg_offs - pz_offs) * -params.threshold; pz_offs * s + adaa_c } };
(f(*sample) - f(*prev_sample)) / (*sample - *prev_sample) } else if *sample < -params.threshold { -params.neg_offs } else { pz_offs }; *prev_sample = *sample;
// LP on the effect offset. *lp_offs += lp_offs_a * (offs - *lp_offs);
*sample *= params.pre_gain; *sample += *lp_offs * params.effect_gain;
// LP on the final sample. *lp_post = if let Some(a) = lp_post_a { *lp_post + (a * (*sample - *lp_post)) } else { *sample }; *sample = *lp_post;
*sample *= params.post_gain; }}
impl Plugin for Ring6Plugin { const NAME: &'static str = "Ring 6"; const VENDOR: &'static str = "TODO"; const URL: &'static str = "TODO"; const EMAIL: &'static str = "info@example.com"; 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), aux_input_ports: &[], aux_output_ports: &[], names: PortNames::const_default(), }, AudioIOLayout { main_input_channels: NonZeroU32::new(1), main_output_channels: NonZeroU32::new(1), ..AudioIOLayout::const_default() }, ];
const MIDI_INPUT: MidiConfig = MidiConfig::None; const SAMPLE_ACCURATE_AUTOMATION: bool = true;
type Editor = IcedEditor; type SysExMessage = (); type BackgroundTask = ();
fn activate( &mut self, _audio_io_layout: &AudioIOLayout, buffer_config: &BufferConfig, _context: &mut impl ActivateContext<Self>, ) -> bool { self.sample_rate = buffer_config.sample_rate; true }
fn reset(&mut self) { self.channel_state = Default::default(); }
fn params(&self) -> Arc<dyn Params> { self.params.clone() }
fn editor(&mut self, _async_executor: AsyncExecutor<Self>) -> Option<IcedEditor> { let params = self.params.clone();
create_iced_editor( self.editor_state.clone(), Ring6EditorState { params }, self.notifier.clone(), Default::default(), |editor_state, nice_ctx| { Ok(application( editor_state, nice_ctx, Ring6Editor::new, Ring6Editor::update, Ring6Editor::view, ) .theme(Ring6Editor::theme) .default_font(DEFAULT_FONT) .font(include_bytes!("../fonts/OpenSans-Regular.subset.ttf")) .font(include_bytes!("../fonts/OpenSans-Bold.subset.ttf")) .subscription(|_| iced::poll_events().map(|_| Message::Poll)) .run()) }, ) }
fn process( &mut self, buffer: &mut Buffer, _aux: &mut AuxiliaryBuffers, _context: &mut impl ProcessContext<Self>, ) -> ProcessStatus { for (i, channel) in buffer.as_slice().iter_mut().enumerate() { let balance = self.params.effect_balance.value();
let mut effect_params = LadderEffectParams { pre_gain: self.params.pre_gain.value(), post_gain: self.params.post_gain.value(), effect_gain: self.params.effect_gain.value() * if (balance > 0.0 && i == 0) || (balance < 0.0 && i == 1) { (1.0 - balance.abs()).powf(2.0) } else { 1.0 }, pz_offs: self.params.pz_offs.value(), neg_offs: self.params.neg_offs.value(), threshold: self.params.threshold.value(), effect_lp_cutoff: self.params.effect_lp_cutoff.value(), effect_adaa: self.params.effect_adaa.value(), post_lp_cutoff: if self.params.post_lp.value() { Some(self.params.post_lp_cutoff.value()) } else { None }, pz_release_ms: self.params.release_ms.value(), sample_rate: self.sample_rate, state: &mut self.channel_state[i], };
ladder_effect(channel, &mut effect_params); }
ProcessStatus::Normal }
fn deactivate(&mut self) {}}
#[derive(Debug, Clone)]enum Message { Poll, FloatParamGesture(FloatParamKey, Gesture), FloatParamReset(FloatParamKey), BoolParam(BoolParamKey, bool),}
struct Ring6EditorState { params: Arc<Ring6PluginParams>,}
struct Ring6Editor { state: PersistentState<Ring6EditorState>, nice_ctx: IcedNiceContext,}
impl Ring6Editor { fn new(state: PersistentState<Ring6EditorState>, nice_ctx: IcedNiceContext) -> Self { Self { state, nice_ctx } }
fn theme(&self) -> Option<Theme> { Some(Theme::Oxocarbon) }
fn update(&mut self, message: Message) { let params = &self.state.params; let setter = self.nice_ctx.nice_context.param_setter();
match message { Message::Poll => {} Message::FloatParamGesture(key, gesture) => { iced_audio::param::set_nice_param(params.get_float(key), gesture, &setter); } Message::FloatParamReset(key) => { let p = params.get_float(key); setter.begin_set_parameter(p); setter.set_parameter(p, p.default_plain_value()); setter.end_set_parameter(p); } Message::BoolParam(key, value) => { let p = params.get_bool(key); setter.begin_set_parameter(p); setter.set_parameter(p, value); setter.end_set_parameter(p); } } }
fn view<'a>(&'a self) -> Element<'a, Message> { let params = &self.state.params;
let bold = Font { weight: font::Weight::Bold, ..DEFAULT_FONT };
let heading = |title: &'static str, size: u32| -> Text<'a, _, _> { text(title).font(bold).size(size) };
let param_control_custom_hdr = |header: Element<'a, Message>, key: FloatParamKey| -> Element<'_, Message> { let p = params.get_float(key); column![ header, Knob::new(nice_to_iced(p)).on_gesture(move |g| Message::FloatParamGesture(key, g)), text(p.normalized_value_to_string(p.modulated_normalized_value(), true)).size(10), button(text("Reset").size(12)) .style(button::secondary) .on_press(Message::FloatParamReset(key)), ] .padding(8) .spacing(4) .height(Length::Fill) .width(Length::Fill) .align_x(Alignment::Center) .into() };
let param_control_text_hdr = |title: &'static str, key: FloatParamKey| -> Element<'a, Message> { param_control_custom_hdr(text(title).size(12).font(bold).into(), key) };
let param_control_toggle_hdr = |title: &'static str, toggle_key: BoolParamKey, float_key: FloatParamKey| -> Element<'a, Message> { let toggle_p = params.get_bool(toggle_key); param_control_custom_hdr( checkbox(toggle_p.value()) .on_toggle(move |b| Message::BoolParam(toggle_key, b)) .size(12) .label(title) .text_size(12) .font(bold) .into(), float_key, ) };
column![ row![heading("ring ", 18), heading("6", 18).style(text::primary)], row![ column![ heading("Pre", 16), column![ param_control_text_hdr("Gain", FloatParamKey::PreGain), space().height(Length::Fixed(16.0)) ], ] .spacing(8) .height(Length::Fill) .width(Length::Fill) .align_x(Alignment::Center), rule::vertical(2), column![ heading("Effect", 16), column![ row![ column![ param_control_text_hdr("Offset A", FloatParamKey::PzOffs), param_control_text_hdr("Offset B", FloatParamKey::NegOffs), ], column![ param_control_text_hdr("Threshold", FloatParamKey::Threshold), param_control_text_hdr("Balance", FloatParamKey::EffectBalance), // param_control_text_hdr("Decay", FloatParamKey::DecayMs), ], column![ param_control_text_hdr("Gain", FloatParamKey::EffectGain), param_control_text_hdr("Low-Pass", FloatParamKey::EffectLpCutoff), ], ], container( checkbox(params.effect_adaa.value()) .size(12) .label("Anti-Alias") .text_size(12) .font(bold) .on_toggle(|b| Message::BoolParam(BoolParamKey::EffectADAA, b)) ) .center_x(Length::Fill), ], ] .spacing(8) .height(Length::Fill) .width(Length::FillPortion(3)) .align_x(Alignment::Center), rule::vertical(2), column![ heading("Post", 16), column![ param_control_text_hdr("Gain", FloatParamKey::PostGain), param_control_toggle_hdr( "Low-Pass", BoolParamKey::PostLp, FloatParamKey::PostLpCutoff ), space().height(Length::Fixed(16.0)) ], ] .spacing(8) .height(Length::Fill) .width(Length::Fill) .align_x(Alignment::Center), ] .spacing(8) .height(Length::Fill) .width(Length::Fill), ] .height(Length::Fill) .width(Length::Fill) .padding(16) .spacing(16) .align_x(Alignment::Center) .into() }}
impl ClapPlugin for Ring6Plugin { const CLAP_ID: &'static str = "dev.refi64.ring6"; const CLAP_DESCRIPTION: Option<&'static str> = Some("TODO"); const CLAP_MANUAL_URL: Option<&'static str> = Some(Self::URL); const CLAP_SUPPORT_URL: Option<&'static str> = None; const CLAP_FEATURES: &'static [ClapFeature] = &[ClapFeature::AudioEffect, ClapFeature::Stereo];}
nice_export_clap!(Ring6Plugin);