Initial Air Freshener plugin implementation
This commit is contained in:
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
*.blend filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.kra filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.exr filter=lfs diff=lfs merge=lfs -text
|
||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
16
.vscode/tasks.json
vendored
Normal file
16
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "cargo",
|
||||||
|
"command": "post",
|
||||||
|
"args": ["build", "--package", "ebu-plug-core"],
|
||||||
|
"problemMatcher": ["$rustc"],
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
},
|
||||||
|
"label": "rust: cargo build"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
1301
Cargo.lock
generated
Normal file
1301
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
Cargo.toml
Normal file
20
Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
[package]
|
||||||
|
name = "ebu-plug-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
ebu-dsp = { version = "0.1.0", path = "ebu-dsp" }
|
||||||
|
nih_plug = { git = "https://github.com/robbert-vdh/nih-plug", version = "0.0.0", default-features = false }
|
||||||
|
parking_lot = "0.12.5"
|
||||||
|
baseview = { git = "https://github.com/RustAudio/baseview", version = "0.1.0", features = ["opengl"]}
|
||||||
|
crossbeam = "0.8.4"
|
||||||
|
femtovg = "0.18.1"
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
image = { version = "0.25.9", default-features = false, features = ["png"] }
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["ebu-dsp"]
|
||||||
BIN
assets/AirFreshener/Fresh.blend
LFS
Normal file
BIN
assets/AirFreshener/Fresh.blend
LFS
Normal file
Binary file not shown.
BIN
assets/AirFreshener/backgrounds.kra
LFS
Normal file
BIN
assets/AirFreshener/backgrounds.kra
LFS
Normal file
Binary file not shown.
BIN
assets/AirFreshener/bg0.png
LFS
Normal file
BIN
assets/AirFreshener/bg0.png
LFS
Normal file
Binary file not shown.
BIN
assets/AirFreshener/bg1.png
LFS
Normal file
BIN
assets/AirFreshener/bg1.png
LFS
Normal file
Binary file not shown.
BIN
assets/AirFreshener/sheet.png
LFS
Normal file
BIN
assets/AirFreshener/sheet.png
LFS
Normal file
Binary file not shown.
BIN
assets/studio_small_09_4k.exr
LFS
Normal file
BIN
assets/studio_small_09_4k.exr
LFS
Normal file
Binary file not shown.
7
ebu-dsp/Cargo.toml
Normal file
7
ebu-dsp/Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[package]
|
||||||
|
name = "ebu-dsp"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
nih_plug = { git = "https://github.com/robbert-vdh/nih-plug", version = "0.0.0", default-features = false }
|
||||||
74
ebu-dsp/src/amplitude.rs
Normal file
74
ebu-dsp/src/amplitude.rs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
use std::fmt::{Debug, Display};
|
||||||
|
|
||||||
|
use crate::{decibel::Decibel, math_ops_impl};
|
||||||
|
|
||||||
|
#[derive(Default, Copy, Clone, PartialEq, PartialOrd)]
|
||||||
|
pub struct Amplitude(pub f64);
|
||||||
|
impl From<Amplitude> for Decibel {
|
||||||
|
fn from(value: Amplitude) -> Self {
|
||||||
|
Decibel::from(20.0 * value.0.abs().log10())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Amplitude {
|
||||||
|
pub fn value(&self) -> f64 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Amplitude {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(&format!("{:.2}", self.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Debug for Amplitude {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(&format!("{}", self.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
math_ops_impl!(Amplitude);
|
||||||
|
|
||||||
|
impl std::ops::Add for Amplitude {
|
||||||
|
type Output = Amplitude;
|
||||||
|
fn add(self, rhs: Amplitude) -> Self::Output {
|
||||||
|
Self(self.0 + rhs.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::Sub for Amplitude {
|
||||||
|
type Output = Amplitude;
|
||||||
|
fn sub(self, rhs: Amplitude) -> Self::Output {
|
||||||
|
Self(self.0 - rhs.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::Mul for Amplitude {
|
||||||
|
type Output = Amplitude;
|
||||||
|
fn mul(self, rhs: Amplitude) -> Self::Output {
|
||||||
|
Self(self.0 * rhs.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::Div for Amplitude {
|
||||||
|
type Output = Amplitude;
|
||||||
|
fn div(self, rhs: Amplitude) -> Self::Output {
|
||||||
|
Self(self.0 / rhs.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::AddAssign for Amplitude {
|
||||||
|
fn add_assign(&mut self, rhs: Amplitude) {
|
||||||
|
self.0 += rhs.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::SubAssign for Amplitude {
|
||||||
|
fn sub_assign(&mut self, rhs: Amplitude) {
|
||||||
|
self.0 -= rhs.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::MulAssign for Amplitude {
|
||||||
|
fn mul_assign(&mut self, rhs: Amplitude) {
|
||||||
|
self.0 *= rhs.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::DivAssign for Amplitude {
|
||||||
|
fn div_assign(&mut self, rhs: Amplitude) {
|
||||||
|
self.0 /= rhs.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
348
ebu-dsp/src/biquad.rs
Normal file
348
ebu-dsp/src/biquad.rs
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
use std::{
|
||||||
|
f64::{self, consts::SQRT_2},
|
||||||
|
fmt::Display,
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
decibel::Decibel, SampleRate,
|
||||||
|
traits::{IntFormatter, Processor},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum Slope {
|
||||||
|
#[default]
|
||||||
|
DB12,
|
||||||
|
DB24,
|
||||||
|
DB36,
|
||||||
|
DB48,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum FilterMode {
|
||||||
|
Peak,
|
||||||
|
HighPass,
|
||||||
|
HighShelf,
|
||||||
|
LowPass,
|
||||||
|
LowShelf,
|
||||||
|
AllPass,
|
||||||
|
BandPass,
|
||||||
|
BandReject,
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
impl TryFrom<i32> for FilterMode {
|
||||||
|
type Error = ();
|
||||||
|
fn try_from(value: i32) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
0 => Ok(FilterMode::Peak),
|
||||||
|
1 => Ok(FilterMode::HighPass),
|
||||||
|
2 => Ok(FilterMode::HighShelf),
|
||||||
|
3 => Ok(FilterMode::LowPass),
|
||||||
|
4 => Ok(FilterMode::LowShelf),
|
||||||
|
5 => Ok(FilterMode::AllPass),
|
||||||
|
6 => Ok(FilterMode::BandPass),
|
||||||
|
7 => Ok(FilterMode::BandReject),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<FilterMode> for i32 {
|
||||||
|
fn from(value: FilterMode) -> Self {
|
||||||
|
match value {
|
||||||
|
FilterMode::Peak => 0,
|
||||||
|
FilterMode::HighPass => 1,
|
||||||
|
FilterMode::HighShelf => 2,
|
||||||
|
FilterMode::LowPass => 3,
|
||||||
|
FilterMode::LowShelf => 4,
|
||||||
|
FilterMode::AllPass => 5,
|
||||||
|
FilterMode::BandPass => 6,
|
||||||
|
FilterMode::BandReject => 7,
|
||||||
|
FilterMode::None => 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl TryFrom<&str> for FilterMode {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
"Peak" => Ok(FilterMode::Peak),
|
||||||
|
"Highpass" => Ok(FilterMode::HighPass),
|
||||||
|
"High Shelf" => Ok(FilterMode::HighShelf),
|
||||||
|
"Lowpass" => Ok(FilterMode::LowPass),
|
||||||
|
"Low Shelf" => Ok(FilterMode::LowShelf),
|
||||||
|
"Allpass" => Ok(FilterMode::AllPass),
|
||||||
|
"Bandpass" => Ok(FilterMode::BandPass),
|
||||||
|
"Notch" => Ok(FilterMode::BandReject),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Display for FilterMode {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
FilterMode::Peak => "Peak",
|
||||||
|
FilterMode::HighPass => "Highpass",
|
||||||
|
FilterMode::HighShelf => "High Shelf",
|
||||||
|
FilterMode::LowPass => "Lowpass",
|
||||||
|
FilterMode::LowShelf => "Low Shelf",
|
||||||
|
FilterMode::AllPass => "Allpass",
|
||||||
|
FilterMode::BandPass => "Bandpass",
|
||||||
|
FilterMode::BandReject => "Notch",
|
||||||
|
FilterMode::None => "None",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntFormatter for FilterMode {
|
||||||
|
fn v2s() -> Arc<dyn Fn(i32) -> String + Send + Sync>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
Arc::new(move |value| FilterMode::try_from(value).unwrap_or_default().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn s2v() -> Arc<dyn Fn(&str) -> Option<i32> + Send + Sync>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
Arc::new(move |value| FilterMode::try_from(value).map(i32::from).ok())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Copy)]
|
||||||
|
struct FilterCoefficients {
|
||||||
|
a1: f64,
|
||||||
|
a2: f64,
|
||||||
|
b0: f64,
|
||||||
|
b1: f64,
|
||||||
|
b2: f64,
|
||||||
|
}
|
||||||
|
impl FilterCoefficients {
|
||||||
|
fn calculate(
|
||||||
|
&mut self,
|
||||||
|
mode: FilterMode,
|
||||||
|
sample_rate: SampleRate,
|
||||||
|
freq: f64,
|
||||||
|
q: f64,
|
||||||
|
gain_db: Decibel,
|
||||||
|
) {
|
||||||
|
let v = 10f64.powf(gain_db.value().abs() / 20.0);
|
||||||
|
let k = (f64::consts::PI * freq / sample_rate.0).tan();
|
||||||
|
|
||||||
|
match mode {
|
||||||
|
FilterMode::Peak => {
|
||||||
|
if gain_db.value() >= 0.0 {
|
||||||
|
let norm = 1.0 / (1.0 + 1.0 / q * k + k * k);
|
||||||
|
self.b0 = (1.0 + v / q * k + k * k) * norm;
|
||||||
|
self.b1 = 2.0 * (k * k - 1.0) * norm;
|
||||||
|
self.b2 = (1.0 - v / q * k + k * k) * norm;
|
||||||
|
self.a1 = self.b1;
|
||||||
|
self.a2 = (1.0 - 1.0 / q * k + k * k) * norm;
|
||||||
|
} else {
|
||||||
|
let norm = 1.0 / (1.0 + v / q * k + k * k);
|
||||||
|
self.b0 = (1.0 + 1.0 / q * k + k * k) * norm;
|
||||||
|
self.b1 = 2.0 * (k * k - 1.0) * norm;
|
||||||
|
self.b2 = (1.0 - 1.0 / q * k + k * k) * norm;
|
||||||
|
self.a1 = self.b1;
|
||||||
|
self.a2 = (1.0 - v / q * k + k * k) * norm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FilterMode::HighPass => {
|
||||||
|
let norm = 1.0 / (1.0 + k / q + k * k);
|
||||||
|
self.b0 = 1.0 * norm;
|
||||||
|
self.b1 = -2.0 * self.b0;
|
||||||
|
self.b2 = self.b0;
|
||||||
|
self.a1 = 2.0 * (k * k - 1.0) * norm;
|
||||||
|
self.a2 = (1.0 - k / q + k * k) * norm;
|
||||||
|
}
|
||||||
|
FilterMode::HighShelf => {
|
||||||
|
if gain_db.value() >= 0.0 {
|
||||||
|
let norm = 1.0 / (1.0 + SQRT_2 * k + k * k);
|
||||||
|
self.b0 = (v + (2.0 * v).sqrt() * k + k * k) * norm;
|
||||||
|
self.b1 = 2.0 * (k * k - v) * norm;
|
||||||
|
self.b2 = (v - (2.0 * v).sqrt() * k + k * k) * norm;
|
||||||
|
self.a1 = 2.0 * (k * k - 1.0) * norm;
|
||||||
|
self.a2 = (1.0 - SQRT_2 * k + k * k) * norm;
|
||||||
|
} else {
|
||||||
|
let norm = 1.0 / (v + (2.0 * v).sqrt() * k + k * k);
|
||||||
|
self.b0 = (1.0 + SQRT_2 * k + k * k) * norm;
|
||||||
|
self.b1 = 2.0 * (k * k - 1.0) * norm;
|
||||||
|
self.b2 = (1.0 - SQRT_2 * k + k * k) * norm;
|
||||||
|
self.a1 = 2.0 * (k * k - v) * norm;
|
||||||
|
self.a2 = (v - (2.0 * v).sqrt() * k + k * k) * norm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FilterMode::LowPass => {
|
||||||
|
let norm = 1.0 / (1.0 + k / q + k * k);
|
||||||
|
self.b0 = k * k * norm;
|
||||||
|
self.b1 = 2.0 * self.b0;
|
||||||
|
self.b2 = self.b0;
|
||||||
|
self.a1 = 2.0 * (k * k - 1.0) * norm;
|
||||||
|
self.a2 = (1.0 - k / q + k * k) * norm;
|
||||||
|
}
|
||||||
|
FilterMode::LowShelf => {
|
||||||
|
if gain_db.value() >= 0.0 {
|
||||||
|
let norm = 1.0 / (1.0 + SQRT_2 * k + k * k);
|
||||||
|
self.b0 = (1.0 + (2.0 * v).sqrt() * k + v * k * k) * norm;
|
||||||
|
self.b1 = 2.0 * (v * k * k - 1.0) * norm;
|
||||||
|
self.b2 = (1.0 - (2.0 * v).sqrt() * k + v * k * k) * norm;
|
||||||
|
self.a1 = 2.0 * (k * k - 1.0) * norm;
|
||||||
|
self.a2 = (1.0 - SQRT_2 * k + k * k) * norm;
|
||||||
|
} else {
|
||||||
|
let norm = 1.0 / (1.0 + (2.0 * v).sqrt() * k + v * k * k);
|
||||||
|
self.b0 = (1.0 + SQRT_2 * k + k * k) * norm;
|
||||||
|
self.b1 = 2.0 * (k * k - 1.0) * norm;
|
||||||
|
self.b2 = (1.0 - SQRT_2 * k + k * k) * norm;
|
||||||
|
self.a1 = 2.0 * (v * k * k - 1.0) * norm;
|
||||||
|
self.a2 = (1.0 - (2.0 * v).sqrt() * k + v * k * k) * norm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FilterMode::AllPass => {
|
||||||
|
let norm = 1.0 / (1.0 + k / q + k * k);
|
||||||
|
self.b0 = (1.0 - k / q + k * k) * norm;
|
||||||
|
self.b1 = 2.0 * (k * k - 1.0) * norm;
|
||||||
|
self.b2 = 1.0;
|
||||||
|
self.a1 = self.b1;
|
||||||
|
self.a2 = self.b0;
|
||||||
|
}
|
||||||
|
FilterMode::BandPass => {
|
||||||
|
let norm = 1.0 / (1.0 + k / q + k * k);
|
||||||
|
self.b0 = k / q * norm;
|
||||||
|
self.b1 = 0.0;
|
||||||
|
self.b2 = -self.b0;
|
||||||
|
self.a1 = 2.0 * (k * k - 1.0) * norm;
|
||||||
|
self.a2 = (1.0 - k / q + k * k) * norm;
|
||||||
|
}
|
||||||
|
FilterMode::BandReject => {
|
||||||
|
let norm = 1.0 / (1.0 + k / q + k * k);
|
||||||
|
self.b0 = (1.0 + k * k) * norm;
|
||||||
|
self.b1 = 2.0 * (k * k - 1.0) * norm;
|
||||||
|
self.b2 = self.b0;
|
||||||
|
self.a1 = self.b1;
|
||||||
|
self.a2 = (1.0 - k / q + k * k) * norm;
|
||||||
|
}
|
||||||
|
FilterMode::None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy)]
|
||||||
|
struct State {
|
||||||
|
s1: f64,
|
||||||
|
s2: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct BiquadFilterState {
|
||||||
|
pub mode: FilterMode,
|
||||||
|
pub cutoff: f64,
|
||||||
|
pub q: f64,
|
||||||
|
pub gain_db: Decibel,
|
||||||
|
pub slope: Slope,
|
||||||
|
}
|
||||||
|
impl Default for BiquadFilterState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
mode: FilterMode::None,
|
||||||
|
cutoff: 2000.0,
|
||||||
|
q: 0.707,
|
||||||
|
gain_db: Decibel::from(0.0),
|
||||||
|
slope: Slope::DB12,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BiquadFilter {
|
||||||
|
pub state: BiquadFilterState,
|
||||||
|
sample_rate: SampleRate,
|
||||||
|
coefficients: FilterCoefficients,
|
||||||
|
slope_states: [State; 4],
|
||||||
|
dirty: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BiquadFilter {
|
||||||
|
pub fn new(sample_rate: SampleRate) -> Self {
|
||||||
|
Self::from_state(sample_rate, BiquadFilterState::default())
|
||||||
|
}
|
||||||
|
pub fn from_state(sample_rate: SampleRate, state: BiquadFilterState) -> Self {
|
||||||
|
Self {
|
||||||
|
sample_rate,
|
||||||
|
coefficients: FilterCoefficients::default(),
|
||||||
|
slope_states: [State::default(); 4],
|
||||||
|
dirty: true,
|
||||||
|
state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn set_mode(&mut self, mode: FilterMode) {
|
||||||
|
self.dirty = true;
|
||||||
|
self.state.mode = mode;
|
||||||
|
}
|
||||||
|
pub fn set_cutoff(&mut self, cutoff: f64) {
|
||||||
|
self.dirty = true;
|
||||||
|
self.state.cutoff = cutoff;
|
||||||
|
}
|
||||||
|
pub fn set_slope(&mut self, slope: Slope) {
|
||||||
|
self.dirty = true;
|
||||||
|
self.state.slope = slope;
|
||||||
|
}
|
||||||
|
pub fn set_q(&mut self, q: f64) {
|
||||||
|
self.dirty = true;
|
||||||
|
self.state.q = q;
|
||||||
|
}
|
||||||
|
pub fn set_gain<T>(&mut self, gain_db: T)
|
||||||
|
where
|
||||||
|
T: Into<Decibel>,
|
||||||
|
{
|
||||||
|
self.dirty = true;
|
||||||
|
self.state.gain_db = gain_db.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_coefficients(&mut self) {
|
||||||
|
if !self.dirty {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.dirty = false;
|
||||||
|
|
||||||
|
self.coefficients.calculate(
|
||||||
|
self.state.mode,
|
||||||
|
self.sample_rate,
|
||||||
|
self.state.cutoff,
|
||||||
|
self.state.q,
|
||||||
|
self.state.gain_db,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Processor for BiquadFilter {
|
||||||
|
fn process(&mut self, sample: f64) -> f64 {
|
||||||
|
self.update_coefficients();
|
||||||
|
|
||||||
|
let iterations = match self.state.slope {
|
||||||
|
Slope::DB12 => 1,
|
||||||
|
Slope::DB24 => 2,
|
||||||
|
Slope::DB36 => 3,
|
||||||
|
Slope::DB48 => 4,
|
||||||
|
};
|
||||||
|
let mut current = sample;
|
||||||
|
|
||||||
|
for i in 0..iterations {
|
||||||
|
let result = self.coefficients.b0 * current + self.slope_states[i].s1;
|
||||||
|
let s1 = self.coefficients.b1 * current - self.coefficients.a1 * result
|
||||||
|
+ self.slope_states[i].s2;
|
||||||
|
let s2 = self.coefficients.b2 * current - self.coefficients.a2 * result;
|
||||||
|
|
||||||
|
self.slope_states[i].s1 = s1;
|
||||||
|
self.slope_states[i].s2 = s2;
|
||||||
|
current = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if current.is_nan() || current.is_infinite() {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
127
ebu-dsp/src/comp.rs
Normal file
127
ebu-dsp/src/comp.rs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
use crate::{
|
||||||
|
Ratio, SampleRate, amplitude::Amplitude, decibel::Decibel, ring_buffer::RingBuffer, rms, smoother::Smoother, traits::Processor
|
||||||
|
};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy)]
|
||||||
|
pub enum CompressorPeakMode {
|
||||||
|
#[default]
|
||||||
|
Sample,
|
||||||
|
RMS,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy)]
|
||||||
|
pub struct CompressorState {
|
||||||
|
pub peak_mode: CompressorPeakMode,
|
||||||
|
pub threshold: Decibel,
|
||||||
|
pub ratio: Ratio,
|
||||||
|
pub gain: Decibel,
|
||||||
|
pub attack: Duration,
|
||||||
|
pub release: Duration,
|
||||||
|
pub last_rms: f64,
|
||||||
|
pub input_db: Decibel,
|
||||||
|
pub output_static_db: Decibel,
|
||||||
|
pub gain_db: Decibel,
|
||||||
|
pub gain_smoothed_db: Decibel,
|
||||||
|
pub final_gain_db: Decibel,
|
||||||
|
}
|
||||||
|
impl CompressorState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
peak_mode: CompressorPeakMode::Sample,
|
||||||
|
threshold: Decibel::from(-24.0),
|
||||||
|
ratio: Ratio(1.0, 3.0),
|
||||||
|
gain: Decibel::from(0.0),
|
||||||
|
attack: Duration::from_millis(25),
|
||||||
|
release: Duration::from_millis(100),
|
||||||
|
last_rms: 0.0,
|
||||||
|
input_db: Decibel::from(0.0),
|
||||||
|
output_static_db: Decibel::from(0.0),
|
||||||
|
gain_db: Decibel::from(0.0),
|
||||||
|
gain_smoothed_db: Decibel::from(0.0),
|
||||||
|
final_gain_db: Decibel::from(0.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Compressor {
|
||||||
|
pub state: CompressorState,
|
||||||
|
sample_buffer: RingBuffer<f64, 2048>,
|
||||||
|
smoother: Smoother,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Compressor {
|
||||||
|
pub fn new(sample_rate: SampleRate) -> Self {
|
||||||
|
Self::from_state(sample_rate, CompressorState::default())
|
||||||
|
}
|
||||||
|
pub fn from_state(sample_rate: SampleRate, state: CompressorState) -> Self {
|
||||||
|
Self {
|
||||||
|
sample_buffer: RingBuffer::new(0.0),
|
||||||
|
smoother: Smoother::new(sample_rate),
|
||||||
|
state,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn set_threshold<T>(&mut self, threshold: T)
|
||||||
|
where
|
||||||
|
T: Into<Decibel>,
|
||||||
|
{
|
||||||
|
self.state.threshold = threshold.into();
|
||||||
|
}
|
||||||
|
pub fn set_attack<T>(&mut self, attack: T)
|
||||||
|
where
|
||||||
|
T: Into<Duration>,
|
||||||
|
{
|
||||||
|
self.smoother.set_attack(attack);
|
||||||
|
}
|
||||||
|
pub fn set_release<T>(&mut self, release: T)
|
||||||
|
where
|
||||||
|
T: Into<Duration>,
|
||||||
|
{
|
||||||
|
self.smoother.set_release(release);
|
||||||
|
}
|
||||||
|
pub fn set_ratio<R>(&mut self, ratio: R)
|
||||||
|
where
|
||||||
|
R: Into<Ratio>,
|
||||||
|
{
|
||||||
|
self.state.ratio = ratio.into();
|
||||||
|
}
|
||||||
|
pub fn set_gain<T>(&mut self, gain: T)
|
||||||
|
where
|
||||||
|
T: Into<Decibel>,
|
||||||
|
{
|
||||||
|
self.state.gain = gain.into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Processor for Compressor {
|
||||||
|
fn process(&mut self, sample: f64) -> f64 {
|
||||||
|
self.sample_buffer[0] = sample;
|
||||||
|
|
||||||
|
self.state.input_db = Decibel::from(Amplitude(match self.state.peak_mode {
|
||||||
|
CompressorPeakMode::Sample => sample.abs() + f64::EPSILON,
|
||||||
|
CompressorPeakMode::RMS => {
|
||||||
|
self.state.last_rms = rms(&self.sample_buffer, self.state.last_rms);
|
||||||
|
self.state.last_rms.abs().sqrt() + f64::EPSILON
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
self.state.output_static_db = if self.state.input_db < self.state.threshold {
|
||||||
|
self.state.input_db
|
||||||
|
} else {
|
||||||
|
self.state.threshold
|
||||||
|
+ (self.state.input_db - self.state.threshold) * self.state.ratio.multiplier()
|
||||||
|
};
|
||||||
|
self.state.gain_db = self.state.output_static_db - self.state.input_db;
|
||||||
|
|
||||||
|
self.state.gain_smoothed_db = if self.state.gain_db < self.state.gain_smoothed_db {
|
||||||
|
self.smoother.attack(self.state.gain_db)
|
||||||
|
} else {
|
||||||
|
self.smoother.release(self.state.gain_db)
|
||||||
|
};
|
||||||
|
|
||||||
|
self.state.final_gain_db = self.state.gain_smoothed_db + self.state.gain;
|
||||||
|
self.sample_buffer.shift();
|
||||||
|
|
||||||
|
sample * self.state.final_gain_db
|
||||||
|
}
|
||||||
|
}
|
||||||
141
ebu-dsp/src/decibel.rs
Normal file
141
ebu-dsp/src/decibel.rs
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
use std::fmt::{Debug, Display};
|
||||||
|
|
||||||
|
use crate::{amplitude::Amplitude, math_ops_impl};
|
||||||
|
|
||||||
|
#[derive(Default, Copy, Clone, PartialEq, PartialOrd)]
|
||||||
|
pub struct Decibel(f64);
|
||||||
|
impl Decibel {
|
||||||
|
pub fn value(&self) -> f64 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<Decibel> for Amplitude {
|
||||||
|
fn from(value: Decibel) -> Self {
|
||||||
|
Amplitude(10f64.powf(value.0 / 20.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Decibel {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if self.0 >= 0.0 {
|
||||||
|
f.write_str(&format!("+{:.2}dB", self.0))
|
||||||
|
} else {
|
||||||
|
f.write_str(&format!("{:.2}dB", self.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Debug for Decibel {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if self.0 >= 0.0 {
|
||||||
|
f.write_str(&format!("+{}dB", self.0))
|
||||||
|
} else {
|
||||||
|
f.write_str(&format!("{}dB", self.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<f32> for Decibel {
|
||||||
|
fn from(value: f32) -> Self {
|
||||||
|
Decibel(value as f64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<Decibel> for f32 {
|
||||||
|
fn from(value: Decibel) -> Self {
|
||||||
|
value.0 as f32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<f64> for Decibel {
|
||||||
|
fn from(value: f64) -> Self {
|
||||||
|
let mut value = value;
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
use nih_plug::nih_warn;
|
||||||
|
if value.is_nan() {
|
||||||
|
nih_warn!("Tried making dB from NaN");
|
||||||
|
value = 0.0;
|
||||||
|
} else if value.is_infinite() {
|
||||||
|
nih_warn!("Tried making dB from Inf");
|
||||||
|
value = 0.0;
|
||||||
|
} else if value >= 120.0 {
|
||||||
|
nih_warn!("Impossibly large dB {value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Decibel(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<Decibel> for f64 {
|
||||||
|
fn from(value: Decibel) -> Self {
|
||||||
|
value.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
math_ops_impl!(Decibel);
|
||||||
|
|
||||||
|
impl std::ops::Add for Decibel {
|
||||||
|
type Output = Decibel;
|
||||||
|
fn add(self, rhs: Decibel) -> Self::Output {
|
||||||
|
Self(self.0 + rhs.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::Sub for Decibel {
|
||||||
|
type Output = Decibel;
|
||||||
|
fn sub(self, rhs: Decibel) -> Self::Output {
|
||||||
|
Self(self.0 - rhs.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::AddAssign for Decibel {
|
||||||
|
fn add_assign(&mut self, rhs: Decibel) {
|
||||||
|
self.0 += rhs.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::SubAssign for Decibel {
|
||||||
|
fn sub_assign(&mut self, rhs: Decibel) {
|
||||||
|
self.0 -= rhs.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Mul<Decibel> for f32 {
|
||||||
|
type Output = f32;
|
||||||
|
fn mul(self, rhs: Decibel) -> Self::Output {
|
||||||
|
((self as f64) * rhs) as f32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::Mul<Decibel> for f64 {
|
||||||
|
type Output = f64;
|
||||||
|
fn mul(self, rhs: Decibel) -> Self::Output {
|
||||||
|
self * Amplitude::from(rhs).0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::Div<Decibel> for f32 {
|
||||||
|
type Output = f32;
|
||||||
|
fn div(self, rhs: Decibel) -> Self::Output {
|
||||||
|
((self as f64) / rhs) as f32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::Div<Decibel> for f64 {
|
||||||
|
type Output = f64;
|
||||||
|
fn div(self, rhs: Decibel) -> Self::Output {
|
||||||
|
self / Amplitude::from(rhs).0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::MulAssign<Decibel> for f32 {
|
||||||
|
fn mul_assign(&mut self, rhs: Decibel) {
|
||||||
|
*self = ((*self as f64) * rhs) as f32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::MulAssign<Decibel> for f64 {
|
||||||
|
fn mul_assign(&mut self, rhs: Decibel) {
|
||||||
|
*self *= Amplitude::from(rhs).0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::DivAssign<Decibel> for f32 {
|
||||||
|
fn div_assign(&mut self, rhs: Decibel) {
|
||||||
|
*self = ((*self as f64) / rhs) as f32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::DivAssign<Decibel> for f64 {
|
||||||
|
fn div_assign(&mut self, rhs: Decibel) {
|
||||||
|
*self /= Amplitude::from(rhs).0
|
||||||
|
}
|
||||||
|
}
|
||||||
31
ebu-dsp/src/eq.rs
Normal file
31
ebu-dsp/src/eq.rs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
use crate::{BiquadFilter, BiquadFilterState, Processor};
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct EqualizerState {
|
||||||
|
pub nodes: [Option<BiquadFilter>; 6],
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Equalizer {
|
||||||
|
pub state: EqualizerState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Equalizer {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::from_state(EqualizerState::default())
|
||||||
|
}
|
||||||
|
pub fn from_state(state: EqualizerState) -> Self {
|
||||||
|
Self { state }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Processor for Equalizer {
|
||||||
|
fn process(&mut self, sample: f64) -> f64 {
|
||||||
|
let mut sample = sample;
|
||||||
|
for node in &mut self.state.nodes {
|
||||||
|
if let Some(filter) = node {
|
||||||
|
sample = filter.process(sample);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sample
|
||||||
|
}
|
||||||
|
}
|
||||||
56
ebu-dsp/src/freq_split.rs
Normal file
56
ebu-dsp/src/freq_split.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
use crate::{BiquadFilter, FilterMode, SampleRate, biquad::Slope, traits::Processor};
|
||||||
|
|
||||||
|
pub struct FreqSplitterState {
|
||||||
|
pub split_frequency: f64,
|
||||||
|
pub slope: Slope,
|
||||||
|
}
|
||||||
|
impl Default for FreqSplitterState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
split_frequency: 2000.0,
|
||||||
|
slope: Slope::DB24,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FreqSplitter {
|
||||||
|
pub state: FreqSplitterState,
|
||||||
|
low_band: BiquadFilter,
|
||||||
|
high_band: BiquadFilter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FreqSplitter {
|
||||||
|
pub fn new(sample_rate: SampleRate) -> Self {
|
||||||
|
Self::from_state(sample_rate, FreqSplitterState::default())
|
||||||
|
}
|
||||||
|
pub fn from_state(sample_rate: SampleRate, state: FreqSplitterState) -> Self {
|
||||||
|
let mut this = Self {
|
||||||
|
state,
|
||||||
|
low_band: BiquadFilter::new(sample_rate),
|
||||||
|
high_band: BiquadFilter::new(sample_rate),
|
||||||
|
};
|
||||||
|
this.low_band.set_mode(FilterMode::LowPass);
|
||||||
|
this.high_band.set_mode(FilterMode::HighPass);
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Processor<f64, (f64, f64)> for FreqSplitter {
|
||||||
|
fn process(&mut self, sample: f64) -> (f64, f64) {
|
||||||
|
self.low_band.set_cutoff(self.state.split_frequency);
|
||||||
|
self.low_band.set_slope(self.state.slope);
|
||||||
|
self.high_band.set_cutoff(self.state.split_frequency);
|
||||||
|
self.high_band.set_slope(self.state.slope);
|
||||||
|
(
|
||||||
|
self.low_band.process(sample),
|
||||||
|
self.high_band.process(sample),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Processor<f32, (f32, f32)> for FreqSplitter {
|
||||||
|
fn process(&mut self, sample: f32) -> (f32, f32) {
|
||||||
|
let (low, high) = self.process(sample as f64);
|
||||||
|
(low as f32, high as f32)
|
||||||
|
}
|
||||||
|
}
|
||||||
198
ebu-dsp/src/lib.rs
Normal file
198
ebu-dsp/src/lib.rs
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
mod amplitude;
|
||||||
|
mod biquad;
|
||||||
|
mod comp;
|
||||||
|
mod decibel;
|
||||||
|
mod freq_split;
|
||||||
|
mod meter;
|
||||||
|
mod ring_buffer;
|
||||||
|
mod smoother;
|
||||||
|
mod traits;
|
||||||
|
mod eq;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
fmt::Debug,
|
||||||
|
ops::Add,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub use biquad::{BiquadFilter, BiquadFilterState, FilterMode, Slope};
|
||||||
|
pub use comp::{Compressor, CompressorState};
|
||||||
|
pub use freq_split::{FreqSplitter, FreqSplitterState};
|
||||||
|
pub use traits::{FloatFormatter, IntFormatter, Lerp, Processor};
|
||||||
|
pub use decibel::Decibel;
|
||||||
|
pub use amplitude::Amplitude;
|
||||||
|
pub use eq::{Equalizer, EqualizerState};
|
||||||
|
|
||||||
|
pub struct Rect<T> {
|
||||||
|
pub x: T,
|
||||||
|
pub y: T,
|
||||||
|
pub width: T,
|
||||||
|
pub height: T,
|
||||||
|
}
|
||||||
|
impl<T: Debug> Debug for Rect<T> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("Rect")
|
||||||
|
.field("x", &self.x)
|
||||||
|
.field("y", &self.y)
|
||||||
|
.field("width", &self.width)
|
||||||
|
.field("height", &self.height)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T: PartialOrd + Add<Output = T> + Copy> Rect<T> {
|
||||||
|
pub fn contains(&self, value: (T, T)) -> bool {
|
||||||
|
value.0 >= self.x
|
||||||
|
&& value.1 >= self.y
|
||||||
|
&& value.0 < self.x + self.width
|
||||||
|
&& value.1 < self.y + self.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T> Default for Rect<T>
|
||||||
|
where
|
||||||
|
T: Default,
|
||||||
|
{
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
x: Default::default(),
|
||||||
|
y: Default::default(),
|
||||||
|
width: Default::default(),
|
||||||
|
height: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use crate::ring_buffer::RingBuffer;
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! math_ops_impl {
|
||||||
|
($T:ty) => {
|
||||||
|
/* impls for f32 */
|
||||||
|
impl std::ops::Add<f32> for $T {
|
||||||
|
type Output = $T;
|
||||||
|
fn add(self, rhs: f32) -> Self::Output {
|
||||||
|
Self(self.0 + rhs as f64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::Sub<f32> for $T {
|
||||||
|
type Output = $T;
|
||||||
|
fn sub(self, rhs: f32) -> Self::Output {
|
||||||
|
Self(self.0 - rhs as f64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::Mul<f32> for $T {
|
||||||
|
type Output = $T;
|
||||||
|
fn mul(self, rhs: f32) -> Self::Output {
|
||||||
|
Self(self.0 * rhs as f64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::Div<f32> for $T {
|
||||||
|
type Output = $T;
|
||||||
|
fn div(self, rhs: f32) -> Self::Output {
|
||||||
|
Self(self.0 / rhs as f64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::AddAssign<f32> for $T {
|
||||||
|
fn add_assign(&mut self, rhs: f32) {
|
||||||
|
self.0 += rhs as f64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::SubAssign<f32> for $T {
|
||||||
|
fn sub_assign(&mut self, rhs: f32) {
|
||||||
|
self.0 -= rhs as f64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::MulAssign<f32> for $T {
|
||||||
|
fn mul_assign(&mut self, rhs: f32) {
|
||||||
|
self.0 *= rhs as f64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::DivAssign<f32> for $T {
|
||||||
|
fn div_assign(&mut self, rhs: f32) {
|
||||||
|
self.0 /= rhs as f64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* impls for f64 */
|
||||||
|
impl std::ops::Add<f64> for $T {
|
||||||
|
type Output = $T;
|
||||||
|
fn add(self, rhs: f64) -> Self::Output {
|
||||||
|
Self(self.0 + rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::Sub<f64> for $T {
|
||||||
|
type Output = $T;
|
||||||
|
fn sub(self, rhs: f64) -> Self::Output {
|
||||||
|
Self(self.0 - rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::Mul<f64> for $T {
|
||||||
|
type Output = $T;
|
||||||
|
fn mul(self, rhs: f64) -> Self::Output {
|
||||||
|
Self(self.0 * rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::Div<f64> for $T {
|
||||||
|
type Output = $T;
|
||||||
|
fn div(self, rhs: f64) -> Self::Output {
|
||||||
|
Self(self.0 / rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::AddAssign<f64> for $T {
|
||||||
|
fn add_assign(&mut self, rhs: f64) {
|
||||||
|
self.0 += rhs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::SubAssign<f64> for $T {
|
||||||
|
fn sub_assign(&mut self, rhs: f64) {
|
||||||
|
self.0 -= rhs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::MulAssign<f64> for $T {
|
||||||
|
fn mul_assign(&mut self, rhs: f64) {
|
||||||
|
self.0 *= rhs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::DivAssign<f64> for $T {
|
||||||
|
fn div_assign(&mut self, rhs: f64) {
|
||||||
|
self.0 /= rhs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)]
|
||||||
|
pub struct SampleRate(pub f64);
|
||||||
|
impl SampleRate {
|
||||||
|
pub fn as_samples<D>(&self, time: D) -> f64
|
||||||
|
where
|
||||||
|
D: Into<Duration>,
|
||||||
|
{
|
||||||
|
self.0 * time.into().as_secs_f64()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Copy, Clone, PartialEq, PartialOrd)]
|
||||||
|
pub struct Ratio(pub f64, pub f64);
|
||||||
|
impl Ratio {
|
||||||
|
pub fn multiplier(&self) -> f64 {
|
||||||
|
self.0 / self.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Display for Ratio {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(&format!("{}:{} ({})", self.0, self.1, self.multiplier()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Debug for Ratio {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(&format!("{}:{} ({})", self.0, self.1, self.multiplier()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rms<const N: usize>(sample_buffer: &RingBuffer<f64, N>, last_rms: f64) -> f64 {
|
||||||
|
// RMS_0 = sqrt((1/n) * (x0^2 + x1^2 + x2^2 + ... + xN^2))
|
||||||
|
// RMS_-1 = sqrt((1/n) * ( x1^2 + x2^2 + ... + xN^2 + x(N+1)^2))
|
||||||
|
last_rms.powf(2.0)
|
||||||
|
+ (1.0 / N as f64) * (sample_buffer[0].powf(2.0) - sample_buffer[N - 1].powf(2.0))
|
||||||
|
}
|
||||||
0
ebu-dsp/src/meter.rs
Normal file
0
ebu-dsp/src/meter.rs
Normal file
38
ebu-dsp/src/ring_buffer.rs
Normal file
38
ebu-dsp/src/ring_buffer.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
use std::ops::{Index, IndexMut};
|
||||||
|
|
||||||
|
pub struct RingBuffer<T, const N: usize> {
|
||||||
|
buffer: [T; N],
|
||||||
|
index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, const N: usize> RingBuffer<T, N>
|
||||||
|
where
|
||||||
|
T: Copy,
|
||||||
|
{
|
||||||
|
pub const fn new(value: T) -> Self {
|
||||||
|
Self {
|
||||||
|
buffer: [value; N],
|
||||||
|
index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub const fn len(&self) -> usize {
|
||||||
|
N
|
||||||
|
}
|
||||||
|
pub fn shift(&mut self) {
|
||||||
|
self.index = (self.index + 1) % N;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
impl<T, const N: usize> Index<usize> for RingBuffer<T, N> {
|
||||||
|
type Output = T;
|
||||||
|
|
||||||
|
fn index(&self, index: usize) -> &Self::Output {
|
||||||
|
&self.buffer[(self.index + index) % N]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, const N: usize> IndexMut<usize> for RingBuffer<T, N> {
|
||||||
|
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
|
||||||
|
&mut self.buffer[(self.index + index) % N]
|
||||||
|
}
|
||||||
|
}
|
||||||
50
ebu-dsp/src/smoother.rs
Normal file
50
ebu-dsp/src/smoother.rs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::SampleRate;
|
||||||
|
|
||||||
|
pub struct Smoother {
|
||||||
|
sample_rate: SampleRate,
|
||||||
|
attack: Duration,
|
||||||
|
release: Duration,
|
||||||
|
last_value: f64,
|
||||||
|
}
|
||||||
|
impl Smoother {
|
||||||
|
pub const fn new(sample_rate: SampleRate) -> Self {
|
||||||
|
Self {
|
||||||
|
sample_rate,
|
||||||
|
attack: Duration::ZERO,
|
||||||
|
release: Duration::ZERO,
|
||||||
|
last_value: 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn set_attack<D>(&mut self, duration: D)
|
||||||
|
where
|
||||||
|
D: Into<Duration>,
|
||||||
|
{
|
||||||
|
self.attack = duration.into();
|
||||||
|
}
|
||||||
|
pub fn attack<T>(&mut self, value: T) -> T
|
||||||
|
where
|
||||||
|
T: From<f64>,
|
||||||
|
T: Into<f64>,
|
||||||
|
{
|
||||||
|
let a = (-2.2 / (self.attack.as_secs_f64() * self.sample_rate.0) + f64::EPSILON).exp();
|
||||||
|
self.last_value = a * self.last_value + (1.0 - a) * value.into();
|
||||||
|
(self.last_value + f64::EPSILON).into()
|
||||||
|
}
|
||||||
|
pub fn set_release<D>(&mut self, duration: D)
|
||||||
|
where
|
||||||
|
D: Into<Duration>,
|
||||||
|
{
|
||||||
|
self.release = duration.into();
|
||||||
|
}
|
||||||
|
pub fn release<T>(&mut self, value: T) -> T
|
||||||
|
where
|
||||||
|
T: From<f64>,
|
||||||
|
T: Into<f64>,
|
||||||
|
{
|
||||||
|
let a = (-2.2 / (self.release.as_secs_f64() * self.sample_rate.0) + f64::EPSILON).exp();
|
||||||
|
self.last_value = a * self.last_value + (1.0 - a) * value.into();
|
||||||
|
(self.last_value + f64::EPSILON).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
68
ebu-dsp/src/traits.rs
Normal file
68
ebu-dsp/src/traits.rs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
use std::{
|
||||||
|
ops::{Add, Mul},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub trait FloatFormatter {
|
||||||
|
fn v2s(digits: usize) -> Arc<dyn Fn(f32) -> String + Send + Sync>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
fn s2v() -> Arc<dyn Fn(&str) -> Option<f32> + Send + Sync>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
}
|
||||||
|
pub trait IntFormatter {
|
||||||
|
fn v2s() -> Arc<dyn Fn(i32) -> String + Send + Sync>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
fn s2v() -> Arc<dyn Fn(&str) -> Option<i32> + Send + Sync>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Processor<I = f64, O = f64> {
|
||||||
|
fn process(&mut self, sample: I) -> O;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I> Processor<f32, f32> for I
|
||||||
|
where
|
||||||
|
I: Processor<f64, f64>,
|
||||||
|
{
|
||||||
|
fn process(&mut self, sample: f32) -> f32 {
|
||||||
|
self.process(sample as f64) as f32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Lerp<F> {
|
||||||
|
type Output;
|
||||||
|
fn lerp(&self, to: Self, t: F) -> Self::Output;
|
||||||
|
fn lerp_unbounded(&self, to: Self, t: F) -> Self::Output;
|
||||||
|
}
|
||||||
|
impl<T> Lerp<f64> for T
|
||||||
|
where
|
||||||
|
T: Mul<f64, Output = T>,
|
||||||
|
<T as Mul<f64>>::Output: Add,
|
||||||
|
Self: Copy,
|
||||||
|
{
|
||||||
|
type Output = <T as Add<Self>>::Output;
|
||||||
|
fn lerp(&self, to: Self, t: f64) -> Self::Output {
|
||||||
|
self.lerp_unbounded(to, t.clamp(0.0, 1.0))
|
||||||
|
}
|
||||||
|
fn lerp_unbounded(&self, to: Self, t: f64) -> Self::Output {
|
||||||
|
*self * (1.0 - t) + to * t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T> Lerp<f32> for T
|
||||||
|
where
|
||||||
|
T: Mul<f32, Output = T>,
|
||||||
|
<T as Mul<f32>>::Output: Add,
|
||||||
|
Self: Copy,
|
||||||
|
{
|
||||||
|
type Output = <T as Add<Self>>::Output;
|
||||||
|
fn lerp(&self, to: Self, t: f32) -> Self::Output {
|
||||||
|
self.lerp_unbounded(to, t.clamp(0.0, 1.0))
|
||||||
|
}
|
||||||
|
fn lerp_unbounded(&self, to: Self, t: f32) -> Self::Output {
|
||||||
|
*self * (1.0 - t) + to * t
|
||||||
|
}
|
||||||
|
}
|
||||||
3
post_build.rs
Normal file
3
post_build.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
std::fs::copy("target/debug/libebu_plug_core.so", "target/debug/air_freshener.clap").unwrap();
|
||||||
|
}
|
||||||
58
src/editor.rs
Normal file
58
src/editor.rs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
use std::sync::{
|
||||||
|
Arc,
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
};
|
||||||
|
|
||||||
|
use baseview::WindowHandle;
|
||||||
|
use crossbeam::atomic::AtomicCell;
|
||||||
|
use nih_plug::params::persist::PersistentField;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct EditorState {
|
||||||
|
/// The window's size in logical pixels before applying `scale_factor`.
|
||||||
|
#[serde(with = "nih_plug::params::persist::serialize_atomic_cell")]
|
||||||
|
pub size: AtomicCell<(u32, u32)>,
|
||||||
|
/// Whether the editor's window is currently open.
|
||||||
|
#[serde(skip)]
|
||||||
|
pub open: AtomicBool,
|
||||||
|
}
|
||||||
|
impl EditorState {
|
||||||
|
pub fn from_size(size: (u32, u32)) -> Arc<Self> {
|
||||||
|
Arc::new(Self {
|
||||||
|
size: AtomicCell::new(size),
|
||||||
|
open: AtomicBool::new(false),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn size(&self) -> (u32, u32) {
|
||||||
|
self.size.load()
|
||||||
|
}
|
||||||
|
pub fn is_open(&self) -> bool {
|
||||||
|
self.open.load(Ordering::Acquire)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> PersistentField<'a, EditorState> for Arc<EditorState> {
|
||||||
|
fn set(&self, new_value: EditorState) {
|
||||||
|
self.size.store(new_value.size.load());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map<F, R>(&self, f: F) -> R
|
||||||
|
where
|
||||||
|
F: Fn(&EditorState) -> R,
|
||||||
|
{
|
||||||
|
f(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EditorHandle {
|
||||||
|
pub state: Arc<EditorState>,
|
||||||
|
pub window: WindowHandle
|
||||||
|
}
|
||||||
|
unsafe impl Send for EditorHandle {}
|
||||||
|
impl Drop for EditorHandle {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.state.open.store(false, Ordering::Release);
|
||||||
|
self.window.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
250
src/gui.rs
Normal file
250
src/gui.rs
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
use crate::{parameters::PluginParams, window::EditorWindow};
|
||||||
|
use baseview::{Event, EventStatus, MouseButton, MouseEvent, WindowEvent, WindowHandler};
|
||||||
|
use ebu_dsp::Rect;
|
||||||
|
use femtovg::{Canvas, Color, ImageFlags, ImageId, Paint, Path, renderer::OpenGl};
|
||||||
|
use nih_plug::prelude::*;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
//const DROID_SANS_FONT: &'static [u8] = include_bytes!("../resources/fonts/DroidSans.ttf");
|
||||||
|
|
||||||
|
const FRESHENER_IMAGE: &'static [u8] = include_bytes!("../assets/AirFreshener/sheet.png");
|
||||||
|
const NOT_SO_FRESH_BG_IMAGE: &'static [u8] = include_bytes!("../assets/AirFreshener/bg0.png");
|
||||||
|
const FRESH_DUMBLEDORE_BG_IMAGE: &'static [u8] =
|
||||||
|
include_bytes!("../assets/AirFreshener/bg1.png");
|
||||||
|
const FRESHENER_FRAMES: u32 = 256;
|
||||||
|
const FRESHENER_FRAMES_X: u32 = 20;
|
||||||
|
const FRESHENER_FRAMES_Y: u32 = 13;
|
||||||
|
const FRESHENER_FRAME_WIDTH: f32 = 73.0;
|
||||||
|
const FRESHENER_FRAME_HEIGHT: f32 = 144.0;
|
||||||
|
|
||||||
|
pub struct PluginGui {
|
||||||
|
// font: FontId,
|
||||||
|
params: Arc<PluginParams>,
|
||||||
|
canvas: Canvas<OpenGl>,
|
||||||
|
_gui_context: Arc<dyn GuiContext>,
|
||||||
|
scaling_factor: f32,
|
||||||
|
|
||||||
|
freshener_image: ImageId,
|
||||||
|
not_so_fresh_image: ImageId,
|
||||||
|
fresh_dumbledore_image: ImageId,
|
||||||
|
|
||||||
|
freshener_bounds: Rect<f32>,
|
||||||
|
|
||||||
|
dirty: bool,
|
||||||
|
mouse_position: (f32, f32),
|
||||||
|
drag_start_mouse_pos: (f32, f32),
|
||||||
|
drag_start_parameter_value: f32,
|
||||||
|
dragging: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginGui {
|
||||||
|
pub fn new(
|
||||||
|
window: &mut baseview::Window<'_>,
|
||||||
|
gui_context: Arc<dyn GuiContext>,
|
||||||
|
params: Arc<PluginParams>,
|
||||||
|
scaling_factor: f32,
|
||||||
|
) -> Self {
|
||||||
|
let context = window
|
||||||
|
.gl_context()
|
||||||
|
.expect("Failed to get window OpenGL context");
|
||||||
|
unsafe {
|
||||||
|
context.make_current();
|
||||||
|
}
|
||||||
|
let renderer = unsafe { OpenGl::new_from_function(|s| context.get_proc_address(s)) }
|
||||||
|
.expect("Failed to create femtovg renderer");
|
||||||
|
let mut canvas = Canvas::new(renderer).expect("Failed to create femtovg canvas");
|
||||||
|
let (width, height) = params.editor_state.size();
|
||||||
|
|
||||||
|
canvas.set_size(width, height, scaling_factor);
|
||||||
|
let le_fresh = canvas
|
||||||
|
.load_image_mem(FRESHENER_IMAGE, ImageFlags::empty())
|
||||||
|
.expect("Failed to load le fresh");
|
||||||
|
let not_so_fresh_image = canvas
|
||||||
|
.load_image_mem(NOT_SO_FRESH_BG_IMAGE, ImageFlags::empty())
|
||||||
|
.expect("Failed to load not so fresh");
|
||||||
|
let fresh_dumbledore_image = canvas
|
||||||
|
.load_image_mem(FRESH_DUMBLEDORE_BG_IMAGE, ImageFlags::empty())
|
||||||
|
.expect("Failed to load fresh dumbledore");
|
||||||
|
//let font = canvas
|
||||||
|
// .add_font_mem(DROID_SANS_FONT)
|
||||||
|
// .expect("Failed to load font");
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
context.make_not_current();
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
//font,
|
||||||
|
params,
|
||||||
|
canvas,
|
||||||
|
_gui_context: gui_context,
|
||||||
|
scaling_factor,
|
||||||
|
dirty: true,
|
||||||
|
mouse_position: (0.0, 0.0),
|
||||||
|
drag_start_mouse_pos: (0.0, 0.0),
|
||||||
|
drag_start_parameter_value: 0.0,
|
||||||
|
dragging: false,
|
||||||
|
freshener_image: le_fresh,
|
||||||
|
fresh_dumbledore_image,
|
||||||
|
not_so_fresh_image,
|
||||||
|
freshener_bounds: Rect {
|
||||||
|
x: 120.0,
|
||||||
|
y: 20.0,
|
||||||
|
width: FRESHENER_FRAME_WIDTH,
|
||||||
|
height: FRESHENER_FRAME_HEIGHT,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowHandler for PluginGui {
|
||||||
|
fn on_frame(&mut self, window: &mut baseview::Window) {
|
||||||
|
const WINDOW_WIDTH: f32 = EditorWindow::WINDOW_SIZE.0 as f32;
|
||||||
|
const WINDOW_HEIGHT: f32 = EditorWindow::WINDOW_SIZE.1 as f32;
|
||||||
|
|
||||||
|
if !self.dirty {
|
||||||
|
//return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let context = window
|
||||||
|
.gl_context()
|
||||||
|
.expect("Failed to get window OpenGL context");
|
||||||
|
unsafe {
|
||||||
|
context.make_current();
|
||||||
|
}
|
||||||
|
let (width, height) = (self.canvas.width(), self.canvas.height());
|
||||||
|
self.canvas.reset();
|
||||||
|
self.canvas
|
||||||
|
.clear_rect(0, 0, width, height, Color::rgbaf(0.0, 0.0, 0.0, 1.0));
|
||||||
|
|
||||||
|
let mut full_window_path = Path::new();
|
||||||
|
full_window_path.rect(
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
EditorWindow::WINDOW_SIZE.0 as f32,
|
||||||
|
EditorWindow::WINDOW_SIZE.1 as f32,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut freshener_path = Path::new();
|
||||||
|
freshener_path.rect(
|
||||||
|
self.freshener_bounds.x,
|
||||||
|
self.freshener_bounds.y,
|
||||||
|
self.freshener_bounds.width,
|
||||||
|
self.freshener_bounds.height,
|
||||||
|
);
|
||||||
|
|
||||||
|
let bbox = self.canvas.path_bbox(&freshener_path);
|
||||||
|
|
||||||
|
let frame_index = (self.params.freshness.unmodulated_normalized_value()
|
||||||
|
* (FRESHENER_FRAMES - 1) as f32)
|
||||||
|
.floor();
|
||||||
|
let frame_x = (frame_index % FRESHENER_FRAMES_X as f32).floor();
|
||||||
|
let frame_y = (frame_index / FRESHENER_FRAMES_X as f32).floor();
|
||||||
|
|
||||||
|
self.canvas.fill_path(
|
||||||
|
&full_window_path,
|
||||||
|
&Paint::image(
|
||||||
|
self.not_so_fresh_image,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
WINDOW_WIDTH,
|
||||||
|
WINDOW_HEIGHT,
|
||||||
|
0.0,
|
||||||
|
1.0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
self.canvas.fill_path(
|
||||||
|
&full_window_path,
|
||||||
|
&Paint::image(
|
||||||
|
self.fresh_dumbledore_image,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
WINDOW_WIDTH,
|
||||||
|
WINDOW_HEIGHT,
|
||||||
|
0.0,
|
||||||
|
self.params.freshness.unmodulated_normalized_value(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
self.canvas.fill_path(
|
||||||
|
&freshener_path,
|
||||||
|
&Paint::image(
|
||||||
|
self.freshener_image,
|
||||||
|
bbox.minx - frame_x * FRESHENER_FRAME_WIDTH,
|
||||||
|
bbox.miny - frame_y * FRESHENER_FRAME_HEIGHT,
|
||||||
|
self.freshener_bounds.width * FRESHENER_FRAMES_X as f32,
|
||||||
|
self.freshener_bounds.height * FRESHENER_FRAMES_Y as f32,
|
||||||
|
0.0,
|
||||||
|
1.0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
/*
|
||||||
|
let debug = format!("{:#?}", self.params.comp_state.load());
|
||||||
|
for (i, line) in debug.split('\n').enumerate() {
|
||||||
|
self.canvas
|
||||||
|
.fill_text(
|
||||||
|
10.0,
|
||||||
|
10.0 + i as f32 * 12.5,
|
||||||
|
line,
|
||||||
|
&Paint::color(Color::rgbf(1.0, 1.0, 1.0))
|
||||||
|
.with_font(&[self.font])
|
||||||
|
.with_font_size(12.5)
|
||||||
|
.with_text_baseline(femtovg::Baseline::Top),
|
||||||
|
)
|
||||||
|
.expect("Failed to render font");
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
self.canvas.flush();
|
||||||
|
context.swap_buffers();
|
||||||
|
unsafe {
|
||||||
|
context.make_not_current();
|
||||||
|
}
|
||||||
|
self.dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_event(
|
||||||
|
&mut self,
|
||||||
|
_window: &mut baseview::Window,
|
||||||
|
event: baseview::Event,
|
||||||
|
) -> baseview::EventStatus {
|
||||||
|
let setter = ParamSetter::new(self._gui_context.as_ref());
|
||||||
|
match event {
|
||||||
|
Event::Window(WindowEvent::Resized(size)) => {
|
||||||
|
let phys_size = size.physical_size();
|
||||||
|
self.canvas
|
||||||
|
.set_size(phys_size.width, phys_size.height, self.scaling_factor);
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
Event::Mouse(MouseEvent::CursorMoved { position, .. }) => {
|
||||||
|
self.mouse_position = (position.x as f32, position.y as f32);
|
||||||
|
if self.dragging {
|
||||||
|
let delta = self.mouse_position.1 - self.drag_start_mouse_pos.1;
|
||||||
|
let new_value =
|
||||||
|
(self.drag_start_parameter_value - delta * 0.01).clamp(0.0, 1.0);
|
||||||
|
setter.set_parameter_normalized(&self.params.freshness, new_value);
|
||||||
|
}
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
Event::Mouse(MouseEvent::ButtonPressed { button, .. }) => {
|
||||||
|
self.dragging = self.freshener_bounds.contains(self.mouse_position)
|
||||||
|
&& button == MouseButton::Left;
|
||||||
|
if self.dragging {
|
||||||
|
setter.begin_set_parameter(&self.params.freshness);
|
||||||
|
self.drag_start_mouse_pos = self.mouse_position;
|
||||||
|
self.drag_start_parameter_value =
|
||||||
|
self.params.freshness.unmodulated_normalized_value();
|
||||||
|
}
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
Event::Mouse(MouseEvent::ButtonReleased { .. }) => {
|
||||||
|
if self.dragging {
|
||||||
|
setter.end_set_parameter(&self.params.freshness);
|
||||||
|
}
|
||||||
|
self.dragging = false;
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
_ => return EventStatus::Ignored,
|
||||||
|
}
|
||||||
|
EventStatus::Captured
|
||||||
|
}
|
||||||
|
}
|
||||||
215
src/lib.rs
Normal file
215
src/lib.rs
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
mod editor;
|
||||||
|
mod gui;
|
||||||
|
mod parameters;
|
||||||
|
mod window;
|
||||||
|
|
||||||
|
use ebu_dsp::{
|
||||||
|
BiquadFilter, BiquadFilterState, Compressor, CompressorState, Decibel, Equalizer,
|
||||||
|
EqualizerState, FilterMode, FreqSplitter, FreqSplitterState, Lerp, Processor, Ratio,
|
||||||
|
SampleRate,
|
||||||
|
};
|
||||||
|
use nih_plug::prelude::*;
|
||||||
|
use std::{num::NonZero, sync::Arc};
|
||||||
|
|
||||||
|
use crate::{parameters::PluginParams, window::EditorWindow};
|
||||||
|
|
||||||
|
struct AirFreshener {
|
||||||
|
params: Arc<PluginParams>,
|
||||||
|
eqs: Vec<Equalizer>,
|
||||||
|
compressors: Vec<Compressor>,
|
||||||
|
splitters: Vec<FreqSplitter>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AirFreshener {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
params: Arc::new(PluginParams::default()),
|
||||||
|
eqs: Vec::new(),
|
||||||
|
compressors: Vec::new(),
|
||||||
|
splitters: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plugin for AirFreshener {
|
||||||
|
const NAME: &'static str = "Air Freshener";
|
||||||
|
const VENDOR: &'static str = "Simon Elberich";
|
||||||
|
// You can use `env!("CARGO_PKG_HOMEPAGE")` to reference the homepage field from the
|
||||||
|
// `Cargo.toml` file here
|
||||||
|
const URL: &'static str = "https://youtu.be/dQw4w9WgXcQ";
|
||||||
|
const EMAIL: &'static str = "info@example.com";
|
||||||
|
|
||||||
|
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
// The first audio IO layout is used as the default. The other layouts may be selected either
|
||||||
|
// explicitly or automatically by the host or the user depending on the plugin API/backend.
|
||||||
|
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: &[],
|
||||||
|
|
||||||
|
// Individual ports and the layout as a whole can be named here. By default these names
|
||||||
|
// are generated as needed. This layout will be called 'Stereo', while the other one is
|
||||||
|
// given the name 'Mono' based no the number of input and output channels.
|
||||||
|
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;
|
||||||
|
// Setting this to `true` will tell the wrapper to split the buffer up into smaller blocks
|
||||||
|
// whenever there are inter-buffer parameter changes. This way no changes to the plugin are
|
||||||
|
// required to support sample accurate automation and the wrapper handles all of the boring
|
||||||
|
// stuff like making sure transport and other timing information stays consistent between the
|
||||||
|
// splits.
|
||||||
|
const SAMPLE_ACCURATE_AUTOMATION: bool = true;
|
||||||
|
|
||||||
|
// If the plugin can send or receive SysEx messages, it can define a type to wrap around those
|
||||||
|
// messages here. The type implements the `SysExMessage` trait, which allows conversion to and
|
||||||
|
// from plain byte buffers.
|
||||||
|
type SysExMessage = ();
|
||||||
|
// More advanced plugins can use this to run expensive background tasks. See the field's
|
||||||
|
// documentation for more information. `()` means that the plugin does not have any background
|
||||||
|
// tasks.
|
||||||
|
type BackgroundTask = ();
|
||||||
|
|
||||||
|
fn params(&self) -> Arc<dyn Params> {
|
||||||
|
self.params.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn editor(&mut self, _async_executor: AsyncExecutor<Self>) -> Option<Box<dyn Editor>> {
|
||||||
|
Some(Box::new(EditorWindow::new(self.params.clone())))
|
||||||
|
}
|
||||||
|
|
||||||
|
// This plugin doesn't need any special initialization, but if you need to do anything expensive
|
||||||
|
// then this would be the place. State is kept around when the host reconfigures the
|
||||||
|
// plugin. If we do need special initialization, we could implement the `initialize()` and/or
|
||||||
|
// `reset()` methods
|
||||||
|
|
||||||
|
fn initialize(
|
||||||
|
&mut self,
|
||||||
|
_audio_io_layout: &AudioIOLayout,
|
||||||
|
buffer_config: &BufferConfig,
|
||||||
|
_context: &mut impl InitContext<Self>,
|
||||||
|
) -> bool {
|
||||||
|
let sample_rate = SampleRate(buffer_config.sample_rate as f64);
|
||||||
|
for _ in 0.._audio_io_layout
|
||||||
|
.main_input_channels
|
||||||
|
.unwrap_or(NonZero::new(2).unwrap())
|
||||||
|
.get()
|
||||||
|
{
|
||||||
|
self.eqs.push(Equalizer::from_state(EqualizerState {
|
||||||
|
nodes: [
|
||||||
|
Some(BiquadFilter::from_state(
|
||||||
|
sample_rate,
|
||||||
|
BiquadFilterState {
|
||||||
|
gain_db: Decibel::from(8.0),
|
||||||
|
cutoff: 1900.0,
|
||||||
|
q: 0.5,
|
||||||
|
mode: FilterMode::HighShelf,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
Some(BiquadFilter::from_state(
|
||||||
|
sample_rate,
|
||||||
|
BiquadFilterState {
|
||||||
|
gain_db: Decibel::from(-3.0),
|
||||||
|
cutoff: 2600.0,
|
||||||
|
mode: FilterMode::Peak,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
|
self.compressors.push(Compressor::from_state(
|
||||||
|
sample_rate,
|
||||||
|
CompressorState {
|
||||||
|
threshold: Decibel::from(-30.0),
|
||||||
|
ratio: Ratio(1.0, 0.9),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
self.splitters.push(FreqSplitter::from_state(
|
||||||
|
sample_rate,
|
||||||
|
FreqSplitterState {
|
||||||
|
split_frequency: 3050.0,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(
|
||||||
|
&mut self,
|
||||||
|
buffer: &mut Buffer,
|
||||||
|
_aux: &mut AuxiliaryBuffers,
|
||||||
|
_context: &mut impl ProcessContext<Self>,
|
||||||
|
) -> ProcessStatus {
|
||||||
|
_context.set_latency_samples(6);
|
||||||
|
for mut channel_samples in buffer.iter_samples() {
|
||||||
|
// Smoothing is optionally built into the parameters themselves
|
||||||
|
let freshness = self.params.freshness.smoothed.next() as f64;
|
||||||
|
|
||||||
|
/*let frequency = self.params.frequency.smoothed.next() as f64;
|
||||||
|
let ratio = self.params.ratio.smoothed.next() as f64;
|
||||||
|
let threshold = self.params.threshold.smoothed.next() as f64;
|
||||||
|
|
||||||
|
for compressor in &mut self.compressors {
|
||||||
|
compressor.set_ratio(Ratio(1.0, ratio));
|
||||||
|
compressor.set_threshold(Decibel::from(threshold));
|
||||||
|
}
|
||||||
|
for splitter in &mut self.splitters {
|
||||||
|
splitter.state.split_frequency = frequency;
|
||||||
|
}
|
||||||
|
for filter in &mut self.filters {
|
||||||
|
filter.state.cutoff = frequency;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
let fresh_lerp_of_bel_air = freshness as f32;
|
||||||
|
for (channel, sample) in channel_samples.iter_mut().enumerate() {
|
||||||
|
*sample = sample.lerp(self.eqs[channel].process(*sample), fresh_lerp_of_bel_air);
|
||||||
|
let (low, high) = self.splitters[channel].process(*sample);
|
||||||
|
*sample = low
|
||||||
|
+ high.lerp(
|
||||||
|
self.compressors[channel].process(high),
|
||||||
|
fresh_lerp_of_bel_air,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessStatus::Normal
|
||||||
|
}
|
||||||
|
|
||||||
|
// This can be used for cleaning up special resources like socket connections whenever the
|
||||||
|
// plugin is deactivated. Most plugins won't need to do anything here.
|
||||||
|
fn deactivate(&mut self) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClapPlugin for AirFreshener {
|
||||||
|
const CLAP_ID: &'static str = "com.moist-plugins-gmbh.gain";
|
||||||
|
const CLAP_DESCRIPTION: Option<&'static str> = Some("A smoothed gain parameter example plugin");
|
||||||
|
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,
|
||||||
|
ClapFeature::Utility,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
nih_export_clap!(AirFreshener);
|
||||||
70
src/parameters.rs
Normal file
70
src/parameters.rs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
use crate::{editor::EditorState, window::EditorWindow};
|
||||||
|
use nih_plug::{
|
||||||
|
formatters,
|
||||||
|
params::{FloatParam, Params},
|
||||||
|
prelude::FloatRange,
|
||||||
|
};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// The [`Params`] derive macro gathers all of the information needed for the wrapper to know about
|
||||||
|
/// the plugin's parameters, persistent serializable fields, and nested parameter groups. You can
|
||||||
|
/// also easily implement [`Params`] by hand if you want to, for instance, have multiple instances
|
||||||
|
/// of a parameters struct for multiple identical oscillators/filters/envelopes.
|
||||||
|
#[derive(Params)]
|
||||||
|
pub struct PluginParams {
|
||||||
|
#[id = "freshness"]
|
||||||
|
pub freshness: FloatParam,
|
||||||
|
/*
|
||||||
|
#[id = "frequency"]
|
||||||
|
pub frequency: FloatParam,
|
||||||
|
#[id = "ratio"]
|
||||||
|
pub ratio: FloatParam,
|
||||||
|
#[id = "threshold"]
|
||||||
|
pub threshold: FloatParam,
|
||||||
|
*/
|
||||||
|
#[persist = "editor-state"]
|
||||||
|
pub editor_state: Arc<EditorState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PluginParams {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
editor_state: EditorState::from_size(EditorWindow::WINDOW_SIZE),
|
||||||
|
|
||||||
|
freshness: FloatParam::new("Freshness", 0.0, FloatRange::Linear { min: 0.0, max: 1.0 })
|
||||||
|
.with_value_to_string(formatters::v2s_f32_percentage(2))
|
||||||
|
.with_string_to_value(formatters::s2v_f32_percentage()),
|
||||||
|
/*
|
||||||
|
frequency: FloatParam::new(
|
||||||
|
"Frequency",
|
||||||
|
2000.0,
|
||||||
|
FloatRange::Skewed {
|
||||||
|
min: 10.0,
|
||||||
|
max: 22050.0,
|
||||||
|
factor: FloatRange::skew_factor(-1.5),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_value_to_string(formatters::v2s_f32_hz_then_khz(2))
|
||||||
|
.with_string_to_value(formatters::s2v_f32_hz_then_khz()),
|
||||||
|
ratio: FloatParam::new(
|
||||||
|
"Ratio",
|
||||||
|
0.707,
|
||||||
|
FloatRange::Skewed {
|
||||||
|
min: 0.01,
|
||||||
|
max: 20.0,
|
||||||
|
factor: FloatRange::skew_factor(-1.5),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
threshold: FloatParam::new(
|
||||||
|
"Threshold",
|
||||||
|
0.0,
|
||||||
|
FloatRange::Linear {
|
||||||
|
min: -60.0,
|
||||||
|
max: 12.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_unit(" dB"),
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/window.rs
Normal file
76
src/window.rs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
use std::sync::{Arc, atomic::Ordering};
|
||||||
|
use baseview::{Window, WindowOpenOptions, WindowScalePolicy, gl::GlConfig};
|
||||||
|
use crossbeam::atomic::AtomicCell;
|
||||||
|
use nih_plug::{editor::Editor, plugin::Plugin};
|
||||||
|
use crate::{AirFreshener, editor::EditorHandle, parameters::PluginParams, gui::PluginGui};
|
||||||
|
|
||||||
|
pub struct EditorWindow {
|
||||||
|
params: Arc<PluginParams>,
|
||||||
|
scaling_factor: AtomicCell<Option<f32>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EditorWindow {
|
||||||
|
pub const WINDOW_SIZE: (u32, u32) = (230, 320);
|
||||||
|
pub fn new(params: Arc<PluginParams>) -> Self {
|
||||||
|
Self {
|
||||||
|
params,
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
scaling_factor: AtomicCell::new(None),
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
scaling_factor: AtomicCell::new(Some(1.0)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Editor for EditorWindow {
|
||||||
|
fn spawn(
|
||||||
|
&self,
|
||||||
|
parent: nih_plug::prelude::ParentWindowHandle,
|
||||||
|
context: Arc<dyn nih_plug::prelude::GuiContext>,
|
||||||
|
) -> Box<dyn std::any::Any + Send> {
|
||||||
|
let (unscaled_width, unscaled_height) = self.params.editor_state.size();
|
||||||
|
let scaling_factor = self.scaling_factor.load();
|
||||||
|
let gui_context = context.clone();
|
||||||
|
let params = self.params.clone();
|
||||||
|
let window = Window::open_parented(&parent, WindowOpenOptions {
|
||||||
|
title: AirFreshener::NAME.to_owned(),
|
||||||
|
size: baseview::Size { width: unscaled_width as f64, height: unscaled_height as f64 },
|
||||||
|
scale: scaling_factor.map(|factor| WindowScalePolicy::ScaleFactor(factor as f64)).unwrap_or(WindowScalePolicy::SystemScaleFactor),
|
||||||
|
gl_config: Some(GlConfig {
|
||||||
|
version: (3, 2),
|
||||||
|
red_bits: 8,
|
||||||
|
green_bits: 8,
|
||||||
|
blue_bits: 8,
|
||||||
|
alpha_bits: 8,
|
||||||
|
samples: None,
|
||||||
|
srgb: true,
|
||||||
|
double_buffer: true,
|
||||||
|
vsync: false,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
}, move |window: &mut baseview::Window<'_>| -> PluginGui {
|
||||||
|
PluginGui::new(window, gui_context, params, scaling_factor.unwrap_or(1.0))
|
||||||
|
});
|
||||||
|
self.params.editor_state.open.store(true, Ordering::Release);
|
||||||
|
Box::new(EditorHandle {
|
||||||
|
state: self.params.editor_state.clone(),
|
||||||
|
window
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> (u32, u32) {
|
||||||
|
self.params.editor_state.size()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_scale_factor(&self, factor: f32) -> bool {
|
||||||
|
if self.params.editor_state.is_open() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.scaling_factor.store(Some(factor));
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn param_value_changed(&self, _id: &str, _normalized_value: f32) {}
|
||||||
|
fn param_modulation_changed(&self, _id: &str, _modulation_offset: f32) {}
|
||||||
|
fn param_values_changed(&self) {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user