23 Commits
0.1.0 ... dev

Author SHA1 Message Date
Ebu
b593a39c1b Fix incorrectly platform dependent dependency 2025-12-04 12:32:26 +01:00
Ebu
74428b272d Add VST3 option 2025-12-04 12:11:29 +01:00
Ebu
a9bd69e461 Fucking finally 2025-12-04 10:12:08 +01:00
Ebu
ea10ce208a Unify scaling factor names 2025-12-04 10:09:00 +01:00
Ebu
d158acd752 Update scale factor when resizing window 2025-12-04 09:50:27 +01:00
Ebu
28f14ba713 Restructured project for separate plugins 2025-12-04 09:34:55 +01:00
Ebu
3cf22b7189 Improve screen scaling behavior 2025-12-03 12:37:38 +01:00
Ebu
d8e9e12515 Grr 2025-12-02 14:19:25 +01:00
Ebu
d6894d20b7 Scaling fixes are getting annoying 2025-12-02 14:16:46 +01:00
Ebu
6e47c185ca Forgot a letter 2025-12-02 14:12:34 +01:00
Ebu
654f23f14f More scren scaling fixes 2025-12-02 14:11:40 +01:00
Ebu
52ad48ed41 Add scaling factor debug info 2025-12-02 13:50:38 +01:00
Ebu
7fed49b250 More screen scaling fix attempts 2025-12-02 13:43:36 +01:00
Ebu
e093875c4f Fix attempt for screen scaling 2025-12-02 13:37:33 +01:00
Ebu
80c522095c Remove macOS specific debugging code 2025-12-02 13:20:54 +01:00
Ebu
c730cc1b62 Fix debug print of error values 2025-12-02 12:41:29 +01:00
Ebu
8c28433bc8 Add debug text for missing assets 2025-12-02 12:38:10 +01:00
Ebu
c46ae5b298 Change default background color 2025-12-02 12:29:13 +01:00
Ebu
160aecc7a9 Make UI components optional 2025-12-02 12:26:11 +01:00
Ebu
64c0d94fbc macOS fix attempt, the next 2025-12-02 12:01:47 +01:00
Ebu
4680644c54 macOS fix attempt 2025-12-02 11:35:30 +01:00
Ebu
2d47fa6c47 macOS crash fix attempt 2025-12-02 11:24:04 +01:00
Ebu
e170c0a99e Add panic logging test 2025-12-02 10:36:07 +01:00
30 changed files with 900 additions and 394 deletions

3
.cargo/config.toml Normal file
View File

@@ -0,0 +1,3 @@
[unstable]
unstable-options = true
bindeps = true

1
.gitignore vendored
View File

@@ -1 +1,2 @@
/target
clap-host

27
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,27 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in library 'dsp'",
"cargo": {
"args": [
"test",
"--no-run",
"--lib",
"--package=ebu-dsp"
],
"filter": {
"name": "dsp",
"kind": "lib"
}
},
"args": [],
"cwd": "${workspaceFolder}"
}
]
}

74
Cargo.lock generated
View File

@@ -17,6 +17,20 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "airfreshener"
version = "0.1.0"
dependencies = [
"baseview",
"crossbeam",
"dsp",
"femtovg",
"image",
"nih_plug",
"parking_lot",
"serde",
]
[[package]]
name = "anyhow"
version = "1.0.100"
@@ -82,7 +96,7 @@ dependencies = [
[[package]]
name = "baseview"
version = "0.1.0"
source = "git+https://github.com/RustAudio/baseview#f7326ceab58c8dd75f3e10bc815ecf7d098e2efc"
source = "git+https://github.com/RustAudio/baseview?rev=237d323c729f3aa99476ba3efa50129c5e86cad3#237d323c729f3aa99476ba3efa50129c5e86cad3"
dependencies = [
"cocoa",
"core-foundation",
@@ -321,24 +335,19 @@ dependencies = [
]
[[package]]
name = "ebu-dsp"
name = "dsp"
version = "0.1.0"
dependencies = [
"femtovg",
"image",
"nih_plug",
]
[[package]]
name = "ebu-plug-core"
name = "ebu-plugs"
version = "0.1.0"
dependencies = [
"baseview",
"crossbeam",
"ebu-dsp",
"femtovg",
"image",
"nih_plug",
"parking_lot",
"serde",
"airfreshener",
]
[[package]]
@@ -368,9 +377,9 @@ dependencies = [
[[package]]
name = "femtovg"
version = "0.18.1"
version = "0.19.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0530af3119be5658d8c1f7e69248d46e2c59e500dc2ef373cf25b355158ef101"
checksum = "be5d925785ad33d7b0ae2b445d9f157c3ab42ff3c515fff0b46d53d4a86c43c5"
dependencies = [
"bitflags 2.10.0",
"bytemuck",
@@ -384,6 +393,7 @@ dependencies = [
"rgb",
"rustybuzz",
"slotmap",
"ttf-parser",
"unicode-bidi",
"unicode-segmentation",
"wasm-bindgen",
@@ -656,6 +666,7 @@ dependencies = [
"raw-window-handle",
"serde",
"serde_json",
"vst3-sys",
"widestring",
"windows",
]
@@ -1085,6 +1096,43 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "vst3-com"
version = "0.1.0"
source = "git+https://github.com/robbert-vdh/vst3-sys.git?branch=fix%2Fdrop-box-from-raw#b3ff4d775940f5b476b9d1cca02a90e07e1922a2"
dependencies = [
"vst3-com-macros",
]
[[package]]
name = "vst3-com-macros"
version = "0.2.0"
source = "git+https://github.com/robbert-vdh/vst3-sys.git?branch=fix%2Fdrop-box-from-raw#b3ff4d775940f5b476b9d1cca02a90e07e1922a2"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"vst3-com-macros-support",
]
[[package]]
name = "vst3-com-macros-support"
version = "0.2.0"
source = "git+https://github.com/robbert-vdh/vst3-sys.git?branch=fix%2Fdrop-box-from-raw#b3ff4d775940f5b476b9d1cca02a90e07e1922a2"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "vst3-sys"
version = "0.1.0"
source = "git+https://github.com/robbert-vdh/vst3-sys.git?branch=fix%2Fdrop-box-from-raw#b3ff4d775940f5b476b9d1cca02a90e07e1922a2"
dependencies = [
"vst3-com",
]
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"

View File

@@ -1,20 +1,11 @@
[package]
name = "ebu-plug-core"
name = "ebu-plugs"
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"] }
airfreshener = { path = "plug-airfreshener", lib = true, artifact = "cdylib" }
[workspace]
members = ["ebu-dsp"]
members = ["dsp", "plug-airfreshener"]

BIN
assets/DroidSans.ttf Normal file

Binary file not shown.

56
build.rs Normal file
View File

@@ -0,0 +1,56 @@
use std::{env, fs, path::Path};
#[cfg(debug_assertions)]
const CONFIG: &str = "debug";
#[cfg(not(debug_assertions))]
const CONFIG: &str = "release";
#[cfg(target_os = "macos")]
const OS_LIB_EXT: &str = "dylib";
#[cfg(target_os = "windows")]
const OS_LIB_EXT: &str = "dll";
#[cfg(target_os = "linux")]
const OS_LIB_EXT: &str = "so";
#[cfg(target_os = "macos")]
const OS_NAME: &str = "macOS";
#[cfg(target_os = "windows")]
const OS_NAME: &str = "Windows";
#[cfg(target_os = "linux")]
const OS_NAME: &str = "Linux";
fn get_output_lib_name(lib_name: &str) -> &'static str {
match lib_name {
"airfreshener" => "AirFreshener",
_ => "Unknown",
}
}
fn main() {
let target_dir = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap_or("".to_owned()))
.join(format!("target/{CONFIG}"));
let artifact_dir = target_dir.join("deps/artifact");
let artifact_dirs =
fs::read_dir(artifact_dir).expect("Failed to read build artifact directory");
for dir_result in artifact_dirs {
if let Ok(dir) = dir_result {
if let Some(lib_name) = dir.file_name().to_string_lossy().split('-').next() {
let lib_path = dir
.path()
.join(format!("cdylib/lib{lib_name}.{OS_LIB_EXT}"));
if fs::exists(&lib_path).unwrap_or_default() {
let out_lib_name = get_output_lib_name(lib_name);
let out_lib_path = target_dir.join(format!("{out_lib_name}_{OS_NAME}.clap"));
if let Err(err) = fs::copy(&lib_path, &out_lib_path) {
println!(
"cargo::error=Output library lib{lib_name:?}.{OS_LIB_EXT} could not be copied to target: {err}"
);
}
} else {
println!("cargo::error=Output library lib{lib_name:?}.{OS_LIB_EXT} not found");
}
}
}
}
}

View File

@@ -1,7 +1,9 @@
[package]
name = "ebu-dsp"
name = "dsp"
version = "0.1.0"
edition = "2024"
[dependencies]
femtovg = "0.19.3"
image = { version = "0.25.9", default-features = false, features = ["png"] }
nih_plug = { git = "https://github.com/robbert-vdh/nih-plug", version = "0.0.0", default-features = false }

View File

@@ -1,4 +1,4 @@
use crate::{BiquadFilter, BiquadFilterState, Processor};
use crate::{BiquadFilter, Processor};
#[derive(Debug, Default, Clone)]
pub struct EqualizerState {

186
dsp/src/gui/mod.rs Normal file
View File

@@ -0,0 +1,186 @@
use std::sync::{Arc, atomic::Ordering};
use femtovg::{Canvas, ImageFlags, ImageId, Paint, Path, renderer::OpenGl};
use nih_plug::prelude::AtomicF32;
use crate::{Rect, ScaledRect};
pub struct SpriteSheet {
scaling_factor: Arc<AtomicF32>,
image: Result<ImageId, String>,
width: usize,
height: usize,
frame_width: usize,
frame_height: usize,
frames_x: usize,
}
impl SpriteSheet {
pub fn empty() -> Self {
Self {
scaling_factor: Arc::new(AtomicF32::new(1.0)),
image: Err("No image loaded".to_owned()),
width: 0,
height: 0,
frame_width: 0,
frame_height: 0,
frames_x: 0,
}
}
pub fn new(
canvas: &mut Canvas<OpenGl>,
data: &[u8],
scaling_factor: Arc<AtomicF32>,
frame_width: usize,
frame_height: usize,
) -> Self {
let image = canvas
.load_image_mem(data, ImageFlags::empty())
.map_err(|e| format!("{e:?}"));
if let Ok(image) = image {
let (width, height) = canvas.image_size(image).unwrap_or_default();
Self {
scaling_factor,
image: Ok(image),
width,
height,
frame_width,
frame_height,
frames_x: width / frame_width,
}
} else {
Self {
scaling_factor,
image,
width: 0,
height: 0,
frame_width,
frame_height,
frames_x: 0,
}
}
}
pub fn draw(&self, canvas: &mut Canvas<OpenGl>, x: f32, y: f32, frame: usize) {
let factor = self.scaling_factor.load(Ordering::Relaxed);
let frame_x = frame % self.frames_x;
let frame_y = frame / self.frames_x;
let screen_rect = Rect {
x: x * factor,
y: y * factor,
width: self.frame_width as f32 * factor,
height: self.frame_height as f32 * factor,
};
let image_rect = Rect {
x: screen_rect.x - (frame_x * self.frame_width) as f32 * factor,
y: screen_rect.y - (frame_y * self.frame_height) as f32 * factor,
width: self.width as f32 * factor,
height: self.height as f32 * factor,
};
let mut screen_path = Path::new();
screen_path.rect(
screen_rect.x,
screen_rect.y,
screen_rect.width,
screen_rect.height,
);
if let Ok(image) = self.image {
canvas.fill_path(
&screen_path,
&Paint::image(
image,
image_rect.x,
image_rect.y,
image_rect.width,
image_rect.height,
0.0,
1.0,
),
);
}
}
pub fn screen_bounds(&self, x: f32, y: f32) -> ScaledRect {
ScaledRect::new_from(
self.scaling_factor.clone(),
(x, y, self.frame_width as f32, self.frame_height as f32),
)
}
}
pub struct Sprite {
scaling_factor: Arc<AtomicF32>,
image: Result<ImageId, String>,
width: usize,
height: usize,
}
impl Sprite {
pub fn empty() -> Self {
Self {
scaling_factor: Arc::new(AtomicF32::new(1.0)),
image: Err("No image loaded".to_owned()),
width: 0,
height: 0,
}
}
pub fn new(canvas: &mut Canvas<OpenGl>, data: &[u8], scaling_factor: Arc<AtomicF32>) -> Self {
let image = canvas
.load_image_mem(data, ImageFlags::empty())
.map_err(|e| format!("{e:?}"));
if let Ok(image) = image {
let (width, height) = canvas.image_size(image).unwrap_or_default();
Self {
scaling_factor,
image: Ok(image),
width,
height,
}
} else {
Self {
scaling_factor,
image,
width: 0,
height: 0,
}
}
}
pub fn draw(&self, canvas: &mut Canvas<OpenGl>, x: f32, y: f32, alpha: f32) {
let factor = self.scaling_factor.load(Ordering::Relaxed);
let screen_rect = Rect {
x: x * factor,
y: y * factor,
width: self.width as f32 * factor,
height: self.height as f32 * factor,
};
let mut screen_path = Path::new();
screen_path.rect(
screen_rect.x,
screen_rect.y,
screen_rect.width,
screen_rect.height,
);
if let Ok(image) = self.image {
canvas.fill_path(
&screen_path,
&Paint::image(
image,
screen_rect.x,
screen_rect.y,
screen_rect.width,
screen_rect.height,
0.0,
alpha,
),
);
}
}
pub fn screen_bounds(&self, x: f32, y: f32) -> ScaledRect {
ScaledRect::new_from(
self.scaling_factor.clone(),
(x, y, self.width as f32, self.height as f32),
)
}
}

View File

@@ -2,61 +2,179 @@ mod amplitude;
mod biquad;
mod comp;
mod decibel;
mod eq;
mod freq_split;
mod gui;
mod meter;
mod ring_buffer;
mod smoother;
mod traits;
mod eq;
use std::{
fmt::Debug,
ops::Add,
sync::{Arc, atomic::Ordering},
time::Duration,
};
pub use amplitude::Amplitude;
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 use freq_split::{FreqSplitter, FreqSplitterState};
pub use gui::{SpriteSheet, Sprite};
use nih_plug::prelude::AtomicF32;
pub use traits::{FloatFormatter, IntFormatter, Lerp, Processor};
pub struct Rect<T> {
pub x: T,
pub y: T,
pub width: T,
pub height: T,
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub struct Point {
x: f32,
y: f32,
}
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 Point {
pub const fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
}
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 From<(f32, f32)> for Point {
fn from(value: (f32, f32)) -> Self {
Point {
x: value.0,
y: value.1,
}
}
}
impl<T> Default for Rect<T>
where
T: Default,
{
fn default() -> Self {
#[derive(Clone, Debug, Default)]
pub struct ScaledPoint {
pub factor: Arc<AtomicF32>,
pub x: f32,
pub y: f32,
}
impl PartialEq for ScaledPoint {
fn eq(&self, other: &Self) -> bool {
self.factor.load(Ordering::Acquire) == other.factor.load(Ordering::Acquire)
&& self.x == other.x
&& self.y == other.y
}
}
impl ScaledPoint {
pub fn new(factor: Arc<AtomicF32>) -> Self {
Self {
x: Default::default(),
y: Default::default(),
width: Default::default(),
height: Default::default(),
factor,
..Default::default()
}
}
pub fn new_from<P>(factor: Arc<AtomicF32>, point: P) -> Self
where
P: Into<Point>,
{
let point = point.into();
Self {
factor,
x: point.x,
y: point.y,
}
}
pub fn set<P>(&mut self, point: P)
where
P: Into<Point>,
{
let point = point.into();
self.x = point.x;
self.y = point.y;
}
pub fn set_scaled<P>(&mut self, point: P)
where
P: Into<Point>,
{
let factor = self.factor.load(Ordering::Acquire);
let point = point.into();
self.x = point.x / factor;
self.y = point.y / factor;
}
pub fn as_point(&self) -> Point {
let factor = self.factor.load(Ordering::Acquire);
Point {
x: self.x * factor,
y: self.y * factor,
}
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub struct Rect {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
impl Rect {
pub fn contains<P>(&self, value: P) -> bool
where
P: Into<Point>,
{
let value = value.into();
value.x >= self.x
&& value.y >= self.y
&& value.x < self.x + self.width
&& value.y < self.y + self.height
}
}
impl From<(f32, f32, f32, f32)> for Rect {
fn from(value: (f32, f32, f32, f32)) -> Self {
Rect {
x: value.0,
y: value.1,
width: value.2,
height: value.3,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct ScaledRect {
pub factor: Arc<AtomicF32>,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
impl PartialEq for ScaledRect {
fn eq(&self, other: &Self) -> bool {
self.factor.load(Ordering::Acquire) == other.factor.load(Ordering::Acquire)
&& self.x == other.x
&& self.y == other.y
&& self.width == other.width
&& self.height == other.height
}
}
impl ScaledRect {
pub fn new(factor: Arc<AtomicF32>) -> Self {
Self {
factor,
..Default::default()
}
}
pub fn new_from<R>(factor: Arc<AtomicF32>, rect: R) -> Self
where
R: Into<Rect>,
{
let rect = rect.into();
Self {
factor,
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
}
}
pub fn as_rect(&self) -> Rect {
let factor = self.factor.load(Ordering::Acquire);
Rect {
x: self.x * factor,
y: self.y * factor,
width: self.width * factor,
height: self.height * factor,
}
}
}

View File

@@ -0,0 +1,22 @@
[package]
name = "airfreshener"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
dsp = { version = "0.1.0", path = "../dsp" }
parking_lot = "0.12.5"
baseview = { git = "https://github.com/RustAudio/baseview", rev = "237d323c729f3aa99476ba3efa50129c5e86cad3", features = [
"opengl",
] }
crossbeam = "0.8.4"
femtovg = "0.19.3"
serde = { version = "1.0.228", features = ["derive"] }
image = { version = "0.25.9", default-features = false, features = ["png"] }
nih_plug = { git = "https://github.com/robbert-vdh/nih-plug", version = "0.0.0", default-features = false }
[features]
vst3 = ["nih_plug/vst3"]

View File

@@ -8,9 +8,11 @@ use crossbeam::atomic::AtomicCell;
use nih_plug::params::persist::PersistentField;
use serde::{Deserialize, Serialize};
use crate::window::EditorWindow;
#[derive(Debug, Serialize, Deserialize)]
pub struct EditorState {
/// The window's size in logical pixels before applying `scale_factor`.
/// The window's size in logical pixels before applying `scaling_factor`.
#[serde(with = "nih_plug::params::persist::serialize_atomic_cell")]
pub size: AtomicCell<(u32, u32)>,
/// Whether the editor's window is currently open.
@@ -20,7 +22,10 @@ pub struct EditorState {
impl EditorState {
pub fn from_size(size: (u32, u32)) -> Arc<Self> {
Arc::new(Self {
size: AtomicCell::new(size),
size: AtomicCell::new((
(size.0 as f32 * EditorWindow::VIRTUAL_SCALE) as u32,
(size.1 as f32 * EditorWindow::VIRTUAL_SCALE) as u32,
)),
open: AtomicBool::new(false),
})
}
@@ -47,7 +52,7 @@ impl<'a> PersistentField<'a, EditorState> for Arc<EditorState> {
pub struct EditorHandle {
pub state: Arc<EditorState>,
pub window: WindowHandle
pub window: WindowHandle,
}
unsafe impl Send for EditorHandle {}
impl Drop for EditorHandle {
@@ -55,4 +60,4 @@ impl Drop for EditorHandle {
self.state.open.store(false, Ordering::Release);
self.window.close();
}
}
}

View File

@@ -0,0 +1,270 @@
use crate::parameters::PluginParams;
use baseview::{
Event, EventStatus, MouseButton, MouseEvent, WindowEvent, WindowHandler, gl::GlContext,
};
use dsp::{ScaledPoint, ScaledRect, Sprite, SpriteSheet};
use femtovg::{Canvas, Color, FontId, Paint, Path, renderer::OpenGl};
use nih_plug::prelude::*;
use std::sync::{Arc, atomic::Ordering};
const DROID_SANS_FONT: &'static [u8] = include_bytes!("../../assets/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: usize = 256;
const FRESHENER_FRAME_WIDTH: usize = 73;
const FRESHENER_FRAME_HEIGHT: usize = 144;
const FRESHENER_SCREEN_X: f32 = 120.0;
const FRESHENER_SCREEN_Y: f32 = 20.0;
pub struct PluginGui {
font: Result<FontId, String>,
params: Arc<PluginParams>,
canvas: Option<Canvas<OpenGl>>,
_gui_context: Arc<dyn GuiContext>,
scaling_factor: Arc<AtomicF32>,
freshener_image: SpriteSheet,
freshener_image_bounds: ScaledRect,
not_so_fresh_image: Sprite,
fresh_dumbledore_image: Sprite,
dirty: bool,
mouse_position: ScaledPoint,
drag_start_mouse_pos: ScaledPoint,
drag_start_parameter_value: f32,
dragging: bool,
}
fn create_canvas(
context: &GlContext,
params: &PluginParams,
scaling_factor: f32,
) -> Result<Canvas<OpenGl>, &'static str> {
let renderer = unsafe { OpenGl::new_from_function(|s| context.get_proc_address(s)) }
.map_err(|_| "Failed to create OpenGL renderer")?;
let mut canvas = Canvas::new(renderer).map_err(|_| "Failed to create femtovg canvas")?;
let (width, height) = params.editor_state.size();
canvas.set_size(width, height, scaling_factor);
Ok(canvas)
}
impl PluginGui {
pub fn new(
window: &mut baseview::Window<'_>,
gui_context: Arc<dyn GuiContext>,
params: Arc<PluginParams>,
scaling_factor: Arc<AtomicF32>,
) -> Self {
let mut this = Self {
font: Err("Not loaded".to_owned()),
params: params.clone(),
canvas: None,
_gui_context: gui_context,
scaling_factor: scaling_factor.clone(),
dirty: true,
mouse_position: ScaledPoint::new(scaling_factor.clone()),
drag_start_mouse_pos: ScaledPoint::new(scaling_factor.clone()),
drag_start_parameter_value: 0.0,
dragging: false,
freshener_image: SpriteSheet::empty(),
freshener_image_bounds: ScaledRect::new(scaling_factor.clone()),
fresh_dumbledore_image: Sprite::empty(),
not_so_fresh_image: Sprite::empty(),
};
if let Some(context) = window.gl_context() {
unsafe {
context.make_current();
}
if let Ok(mut canvas) = create_canvas(context, &params, scaling_factor.load(Ordering::Acquire)) {
this.font = canvas
.add_font_mem(DROID_SANS_FONT)
.map_err(|err| format!("{:?}", err));
this.freshener_image = SpriteSheet::new(
&mut canvas,
FRESHENER_IMAGE,
scaling_factor.clone(),
FRESHENER_FRAME_WIDTH,
FRESHENER_FRAME_HEIGHT,
);
this.freshener_image_bounds = this
.freshener_image
.screen_bounds(FRESHENER_SCREEN_X, FRESHENER_SCREEN_Y);
this.fresh_dumbledore_image = Sprite::new(
&mut canvas,
FRESH_DUMBLEDORE_BG_IMAGE,
scaling_factor.clone(),
);
this.not_so_fresh_image =
Sprite::new(&mut canvas, NOT_SO_FRESH_BG_IMAGE, scaling_factor.clone());
this.canvas = Some(canvas);
}
unsafe {
context.make_not_current();
}
};
/*
if let Some(canvas) = canvas {
this.freshener_image = canvas
.load_image_mem(FRESHENER_IMAGE, ImageFlags::empty())
.map_err(|_| "Failed to load le fresh")?;
let not_so_fresh_image = canvas
.load_image_mem(NOT_SO_FRESH_BG_IMAGE, ImageFlags::empty())
.map_err(|_| "Failed to load not so fresh")?;
let fresh_dumbledore_image = canvas
.load_image_mem(FRESH_DUMBLEDORE_BG_IMAGE, ImageFlags::empty())
.map_err(|_| "Failed to load fresh dumbledore")?;
}
*/
this
}
}
impl WindowHandler for PluginGui {
fn on_frame(&mut self, window: &mut baseview::Window) {
if self.canvas.is_none() {
return;
}
let scaling_factor = self.scaling_factor.load(Ordering::Acquire);
let canvas = self.canvas.as_mut().unwrap();
if !self.dirty {
//return;
}
let font_size = 12.0 * scaling_factor;
let context = match window.gl_context() {
None => {
nih_error!("No OpenGL context");
return;
}
Some(ctx) => ctx,
};
unsafe {
context.make_current();
}
let (width, height) = (canvas.width() as f32, canvas.height() as f32);
canvas.reset();
canvas.clear_rect(
0,
0,
width as u32,
height as u32,
Color::rgbaf(0.5, 0.5, 0.6, 1.0),
);
let mut full_window_path = Path::new();
full_window_path.rect(0.0, 0.0, width, height);
let freshness = self.params.freshness.unmodulated_normalized_value();
let frame_index = (freshness * (FRESHENER_FRAMES - 1) as f32).floor() as usize;
self.not_so_fresh_image.draw(canvas, 0.0, 0.0, 1.0);
self.fresh_dumbledore_image
.draw(canvas, 0.0, 0.0, freshness);
self.freshener_image
.draw(canvas, FRESHENER_SCREEN_X, FRESHENER_SCREEN_Y, frame_index);
#[cfg(debug_assertions)]
{
use femtovg::Baseline;
if let Ok(font) = self.font {
use crate::window::EditorWindow;
let mut y = 5.0;
let mut print = |str: &str| {
for s in str.lines() {
canvas
.fill_text(
5.0,
y,
s,
&Paint::color(Color::white())
.with_font(&[font])
.with_font_size(font_size)
.with_text_baseline(Baseline::Top),
)
.ok();
y += font_size;
}
};
print("Debug version");
print(&format!("scaling_factor {:?}", scaling_factor));
print(&format!("mouse_pos {:?}", self.mouse_position));
print(&format!("frame_index {:?}", frame_index));
print(&format!("windows_size {:?}", EditorWindow::WINDOW_SIZE));
print(&format!("canvas_size {:?}", (width, height)));
}
}
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();
if let Some(canvas) = self.canvas.as_mut() {
let editor_width = self.params.editor_state.size.load().0;
let new_scaling_factor = phys_size.width as f32 / editor_width as f32;
self.scaling_factor
.store(new_scaling_factor, Ordering::Release);
canvas.set_size(phys_size.width, phys_size.height, new_scaling_factor);
}
self.dirty = true;
}
Event::Mouse(MouseEvent::CursorMoved { position, .. }) => {
self.mouse_position
.set((position.x as f32, position.y as f32));
if self.dragging {
let delta = self.mouse_position.y - self.drag_start_mouse_pos.y;
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_image_bounds
.as_rect()
.contains(self.mouse_position.as_point())
&& button == MouseButton::Left;
if self.dragging {
setter.begin_set_parameter(&self.params.freshness);
self.drag_start_mouse_pos = self.mouse_position.clone();
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
}
}

View File

@@ -3,7 +3,7 @@ mod gui;
mod parameters;
mod window;
use ebu_dsp::{
use dsp::{
BiquadFilter, BiquadFilterState, Compressor, CompressorState, Decibel, Equalizer,
EqualizerState, FilterMode, FreqSplitter, FreqSplitterState, Lerp, Processor, Ratio,
SampleRate,
@@ -95,12 +95,14 @@ impl Plugin for AirFreshener {
fn initialize(
&mut self,
_audio_io_layout: &AudioIOLayout,
audio_io_layout: &AudioIOLayout,
buffer_config: &BufferConfig,
_context: &mut impl InitContext<Self>,
) -> bool {
//dsp::init();
let sample_rate = SampleRate(buffer_config.sample_rate as f64);
for _ in 0.._audio_io_layout
for _ in 0..audio_io_layout
.main_input_channels
.unwrap_or(NonZero::new(2).unwrap())
.get()
@@ -150,6 +152,7 @@ impl Plugin for AirFreshener {
},
));
}
true
}
@@ -200,6 +203,7 @@ impl Plugin for AirFreshener {
fn deactivate(&mut self) {}
}
#[cfg(not(feature = "vst3"))]
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");
@@ -212,4 +216,16 @@ impl ClapPlugin for AirFreshener {
];
}
#[cfg(feature = "vst3")]
impl Vst3Plugin for AirFreshener {
const VST3_CLASS_ID: [u8; 16] = *b"AirFreshener____";
const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] =
&[Vst3SubCategory::Fx, Vst3SubCategory::Tools];
}
#[cfg(not(feature = "vst3"))]
nih_export_clap!(AirFreshener);
#[cfg(feature = "vst3")]
nih_export_vst3!(AirFreshener);

View File

@@ -0,0 +1,85 @@
use crate::{AirFreshener, editor::EditorHandle, gui::PluginGui, parameters::PluginParams};
use baseview::{Window, WindowOpenOptions, WindowScalePolicy, gl::GlConfig};
use nih_plug::prelude::AtomicF32;
use nih_plug::{editor::Editor, plugin::Plugin};
use std::sync::{Arc, atomic::Ordering};
pub struct EditorWindow {
params: Arc<PluginParams>,
scaling_factor: Arc<AtomicF32>,
}
impl EditorWindow {
pub const VIRTUAL_SCALE: f32 = 1.0;
pub const WINDOW_SIZE: (u32, u32) = (230, 320);
pub fn new(params: Arc<PluginParams>) -> Self {
Self {
params,
scaling_factor: Arc::new(AtomicF32::new(EditorWindow::VIRTUAL_SCALE)),
}
}
}
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(Ordering::Acquire);
let move_scaling_factor = self.scaling_factor.clone();
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,
},
#[cfg(target_os = "macos")]
scale: WindowScalePolicy::SystemScaleFactor,
#[cfg(not(target_os = "macos"))]
scale: WindowScalePolicy::ScaleFactor(scaling_factor as f64),
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, move_scaling_factor)
},
);
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(factor, Ordering::Release);
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) {}
}

1
rust-toolchain Normal file
View File

@@ -0,0 +1 @@
nightly

View File

@@ -1,250 +0,0 @@
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
}
}

1
src/main.rs Normal file
View File

@@ -0,0 +1 @@
fn main() {}

View File

@@ -1,76 +0,0 @@
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) {}
}