Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b593a39c1b | |||
| 74428b272d | |||
| a9bd69e461 | |||
| ea10ce208a | |||
| d158acd752 | |||
| 28f14ba713 | |||
| 3cf22b7189 | |||
| d8e9e12515 | |||
| d6894d20b7 | |||
| 6e47c185ca | |||
| 654f23f14f | |||
| 52ad48ed41 | |||
| 7fed49b250 | |||
| e093875c4f | |||
| 80c522095c | |||
| c730cc1b62 | |||
| 8c28433bc8 | |||
| c46ae5b298 | |||
| 160aecc7a9 | |||
| 64c0d94fbc | |||
| 4680644c54 | |||
| 2d47fa6c47 | |||
| e170c0a99e |
3
.cargo/config.toml
Normal file
3
.cargo/config.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[unstable]
|
||||
unstable-options = true
|
||||
bindeps = true
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
/target
|
||||
clap-host
|
||||
27
.vscode/launch.json
vendored
Normal file
27
.vscode/launch.json
vendored
Normal 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
74
Cargo.lock
generated
@@ -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"
|
||||
|
||||
17
Cargo.toml
17
Cargo.toml
@@ -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
BIN
assets/DroidSans.ttf
Normal file
Binary file not shown.
56
build.rs
Normal file
56
build.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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
186
dsp/src/gui/mod.rs
Normal 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),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
22
plug-airfreshener/Cargo.toml
Normal file
22
plug-airfreshener/Cargo.toml
Normal 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"]
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
270
plug-airfreshener/src/gui.rs
Normal file
270
plug-airfreshener/src/gui.rs
Normal 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, ¶ms, 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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
85
plug-airfreshener/src/window.rs
Normal file
85
plug-airfreshener/src/window.rs
Normal 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
1
rust-toolchain
Normal file
@@ -0,0 +1 @@
|
||||
nightly
|
||||
250
src/gui.rs
250
src/gui.rs
@@ -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
1
src/main.rs
Normal file
@@ -0,0 +1 @@
|
||||
fn main() {}
|
||||
@@ -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) {}
|
||||
}
|
||||
Reference in New Issue
Block a user