Restructured project for separate plugins
This commit is contained in:
17
plug-airfreshener/Cargo.toml
Normal file
17
plug-airfreshener/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "airfreshener"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
dsp = { version = "0.1.0", path = "../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", 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"] }
|
||||
63
plug-airfreshener/src/editor.rs
Normal file
63
plug-airfreshener/src/editor.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
|
||||
use baseview::WindowHandle;
|
||||
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`.
|
||||
#[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.0 as f32 * EditorWindow::VIRTUAL_SCALE) as u32,
|
||||
(size.1 as f32 * EditorWindow::VIRTUAL_SCALE) as u32,
|
||||
)),
|
||||
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();
|
||||
}
|
||||
}
|
||||
266
plug-airfreshener/src/gui.rs
Normal file
266
plug-airfreshener/src/gui.rs
Normal file
@@ -0,0 +1,266 @@
|
||||
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: &AtomicF32,
|
||||
) -> 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.load(Ordering::Relaxed));
|
||||
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) {
|
||||
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());
|
||||
|
||||
let canvas_width = canvas.width();
|
||||
let editor_width = params.editor_state.size.load().0;
|
||||
if canvas_width != editor_width {
|
||||
this.scaling_factor
|
||||
.store(canvas_width as f32 / editor_width as f32, Ordering::Release);
|
||||
}
|
||||
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::Relaxed);
|
||||
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 {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
let scaling_factor = self.scaling_factor.load(Ordering::Acquire);
|
||||
match event {
|
||||
Event::Window(WindowEvent::Resized(size)) => {
|
||||
let phys_size = size.physical_size();
|
||||
if let Some(canvas) = self.canvas.as_mut() {
|
||||
canvas.set_size(phys_size.width, phys_size.height, 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
|
||||
}
|
||||
}
|
||||
218
plug-airfreshener/src/lib.rs
Normal file
218
plug-airfreshener/src/lib.rs
Normal file
@@ -0,0 +1,218 @@
|
||||
mod editor;
|
||||
mod gui;
|
||||
mod parameters;
|
||||
mod window;
|
||||
|
||||
use 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 {
|
||||
//dsp::init();
|
||||
|
||||
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
plug-airfreshener/src/parameters.rs
Normal file
70
plug-airfreshener/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"),
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
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) {}
|
||||
}
|
||||
Reference in New Issue
Block a user